Merge "Fix incorrect GCA RSS memory calculation" into main
diff --git a/Android.bp b/Android.bp
index a7bda88..fa82c12 100644
--- a/Android.bp
+++ b/Android.bp
@@ -5481,6 +5481,7 @@
         "protos/perfetto/metrics/chrome/dropped_frames.proto",
         "protos/perfetto/metrics/chrome/frame_times.proto",
         "protos/perfetto/metrics/chrome/histogram_hashes.proto",
+        "protos/perfetto/metrics/chrome/histogram_summaries.proto",
         "protos/perfetto/metrics/chrome/long_latency.proto",
         "protos/perfetto/metrics/chrome/media_metric.proto",
         "protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -6776,6 +6777,7 @@
         "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",
@@ -6789,6 +6791,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7205,6 +7208,7 @@
         "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",
@@ -7218,6 +7222,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7296,6 +7301,7 @@
         "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",
@@ -7309,6 +7315,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.cc",
@@ -7387,6 +7394,7 @@
         "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",
@@ -7400,6 +7408,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.gen.h",
@@ -7474,6 +7483,7 @@
         "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",
@@ -7487,6 +7497,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7564,6 +7575,7 @@
         "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",
@@ -7577,6 +7589,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.cc",
@@ -7654,6 +7667,7 @@
         "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",
@@ -7667,6 +7681,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pb.h",
@@ -7741,6 +7756,7 @@
         "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",
@@ -7754,6 +7770,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -7832,6 +7849,7 @@
         "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",
@@ -7845,6 +7863,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.cc",
@@ -7923,6 +7942,7 @@
         "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",
@@ -7936,6 +7956,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/fastrpc.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/fence.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/filemap.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/fs.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h",
@@ -13214,6 +13235,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -13632,6 +13654,7 @@
         "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/latency.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",
@@ -13644,6 +13667,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",
         "src/trace_processor/perfetto_sql/stdlib/stack_trace/jit.sql",
         "src/trace_processor/perfetto_sql/stdlib/stacks/cpu_profiling.sql",
@@ -15127,6 +15151,7 @@
         "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",
@@ -15140,6 +15165,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -16467,6 +16493,7 @@
         "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",
@@ -16480,6 +16507,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -17051,6 +17079,11 @@
     cflags: [
         "-DZLIB_IMPLEMENTATION",
     ],
+    apex_available: [
+        "//apex_available:platform",
+        "com.android.profiling",
+    ],
+    min_sdk_version: "35",
 }
 
 // GN: //src/traceconv:traceconv
diff --git a/BUILD b/BUILD
index ca9ec70..64c1e49 100644
--- a/BUILD
+++ b/BUILD
@@ -502,6 +502,72 @@
     linkstatic = True,
 )
 
+# GN target: //src/traceconv:libpprofbuilder
+perfetto_cc_library(
+    name = "libpprofbuilder",
+    srcs = [
+        ":src_profiling_deobfuscator",
+        ":src_profiling_symbolizer_symbolize_database",
+        ":src_profiling_symbolizer_symbolizer",
+        ":src_trace_processor_util_build_id",
+        ":src_traceconv_pprofbuilder",
+        ":src_traceconv_utils",
+    ],
+    hdrs = [
+        ":include_perfetto_base_base",
+        ":include_perfetto_ext_base_base",
+        ":include_perfetto_profiling_pprof_builder",
+        ":include_perfetto_protozero_protozero",
+        ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_base",
+        ":include_perfetto_public_protozero",
+        ":include_perfetto_trace_processor_basic_types",
+        ":include_perfetto_trace_processor_storage",
+        ":include_perfetto_trace_processor_trace_processor",
+    ],
+    visibility = PERFETTO_CONFIG.public_visibility,
+    deps = [
+        ":protos_perfetto_common_zero",
+        ":protos_perfetto_config_android_zero",
+        ":protos_perfetto_config_ftrace_zero",
+        ":protos_perfetto_config_gpu_zero",
+        ":protos_perfetto_config_inode_file_zero",
+        ":protos_perfetto_config_interceptors_zero",
+        ":protos_perfetto_config_power_zero",
+        ":protos_perfetto_config_process_stats_zero",
+        ":protos_perfetto_config_profiling_zero",
+        ":protos_perfetto_config_statsd_zero",
+        ":protos_perfetto_config_sys_stats_zero",
+        ":protos_perfetto_config_system_info_zero",
+        ":protos_perfetto_config_track_event_zero",
+        ":protos_perfetto_config_zero",
+        ":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_zero",
+        ":protos_perfetto_trace_translation_zero",
+        ":protos_third_party_pprof_zero",
+        ":protozero",
+        ":src_trace_processor_containers_containers",
+    ] + PERFETTO_CONFIG.deps.zlib,
+    linkstatic = True,
+)
+
 # GN target: //test:client_api_example
 perfetto_cc_binary(
     name = "client_api_example",
@@ -2475,6 +2541,7 @@
         "src/trace_processor/metrics/sql/chrome/chrome_args_class_names.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_event_metadata.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_histogram_hashes.sql",
+        "src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_base.sql",
         "src/trace_processor/metrics/sql/chrome/chrome_input_to_browser_intervals_template.sql",
@@ -3100,6 +3167,7 @@
 perfetto_filegroup(
     name = "src_trace_processor_perfetto_sql_stdlib_sched_sched",
     srcs = [
+        "src/trace_processor/perfetto_sql/stdlib/sched/latency.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",
@@ -3119,6 +3187,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",
     ],
 )
@@ -5163,6 +5232,7 @@
         "protos/perfetto/metrics/chrome/dropped_frames.proto",
         "protos/perfetto/metrics/chrome/frame_times.proto",
         "protos/perfetto/metrics/chrome/histogram_hashes.proto",
+        "protos/perfetto/metrics/chrome/histogram_summaries.proto",
         "protos/perfetto/metrics/chrome/long_latency.proto",
         "protos/perfetto/metrics/chrome/media_metric.proto",
         "protos/perfetto/metrics/chrome/performance_mark_hashes.proto",
@@ -5544,6 +5614,7 @@
         "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",
@@ -5557,6 +5628,7 @@
         "protos/perfetto/trace/ftrace/fastrpc.proto",
         "protos/perfetto/trace/ftrace/fence.proto",
         "protos/perfetto/trace/ftrace/filemap.proto",
+        "protos/perfetto/trace/ftrace/fs.proto",
         "protos/perfetto/trace/ftrace/ftrace.proto",
         "protos/perfetto/trace/ftrace/ftrace_event.proto",
         "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto",
@@ -6862,74 +6934,6 @@
            PERFETTO_CONFIG.deps.demangle_wrapper,
 )
 
-# GN target: //src/traceconv:libpprofbuilder
-perfetto_cc_library(
-    name = "libpprofbuilder",
-    srcs = [
-        ":src_profiling_deobfuscator",
-        ":src_profiling_symbolizer_symbolize_database",
-        ":src_profiling_symbolizer_symbolizer",
-        ":src_trace_processor_util_build_id",
-        ":src_traceconv_pprofbuilder",
-        ":src_traceconv_utils",
-    ],
-    hdrs = [
-        ":include_perfetto_base_base",
-        ":include_perfetto_ext_base_base",
-        ":include_perfetto_profiling_pprof_builder",
-        ":include_perfetto_protozero_protozero",
-        ":include_perfetto_public_abi_base",
-        ":include_perfetto_public_base",
-        ":include_perfetto_public_protozero",
-        ":include_perfetto_trace_processor_basic_types",
-        ":include_perfetto_trace_processor_storage",
-        ":include_perfetto_trace_processor_trace_processor",
-    ],
-    visibility = [
-        "//visibility:public",
-    ],
-    deps = [
-        ":protos_perfetto_common_zero",
-        ":protos_perfetto_config_android_zero",
-        ":protos_perfetto_config_ftrace_zero",
-        ":protos_perfetto_config_gpu_zero",
-        ":protos_perfetto_config_inode_file_zero",
-        ":protos_perfetto_config_interceptors_zero",
-        ":protos_perfetto_config_power_zero",
-        ":protos_perfetto_config_process_stats_zero",
-        ":protos_perfetto_config_profiling_zero",
-        ":protos_perfetto_config_statsd_zero",
-        ":protos_perfetto_config_sys_stats_zero",
-        ":protos_perfetto_config_system_info_zero",
-        ":protos_perfetto_config_track_event_zero",
-        ":protos_perfetto_config_zero",
-        ":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_zero",
-        ":protos_perfetto_trace_translation_zero",
-        ":protos_third_party_pprof_zero",
-        ":protozero",
-        ":src_trace_processor_containers_containers",
-    ] + PERFETTO_CONFIG.deps.zlib,
-    linkstatic = True,
-)
-
 # GN target: //src/traceconv:traceconv
 perfetto_cc_binary(
     name = "traceconv",
diff --git a/DIR_METADATA b/DIR_METADATA
index 592fce2..6c28780 100644
--- a/DIR_METADATA
+++ b/DIR_METADATA
@@ -2,3 +2,6 @@
   component: "Speed>Tracing"
 }
 team_email: "tracing@chromium.org"
+buganizer_public: {
+  component_id: 1457213
+}
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/concepts/config.md b/docs/concepts/config.md
index c4326e6..1badce4 100644
--- a/docs/concepts/config.md
+++ b/docs/concepts/config.md
@@ -4,11 +4,12 @@
 in Perfetto all tracing data sources are idle by default and record data only
 when instructed to do so.
 
-Data sources record data only when one (or more) tracing sessions are active.
-A tracing session is started by invoking the `perfetto` cmdline client and
-passing a config (see QuickStart guide for
+Data sources record data only when one (or more) tracing sessions are active. A
+tracing session is started by invoking the `perfetto` cmdline client and passing
+a config (see QuickStart guide for
 [Android](/docs/quickstart/android-tracing.md),
-[Linux](/docs/quickstart/linux-tracing.md), or [Chrome on desktop](/docs/quickstart/chrome-tracing.md)).
+[Linux](/docs/quickstart/linux-tracing.md), or
+[Chrome on desktop](/docs/quickstart/chrome-tracing.md)).
 
 A simple trace config looks like this:
 
@@ -31,7 +32,7 @@
   }
 }
 
-````
+```
 
 And is used as follows:
 
@@ -43,7 +44,7 @@
 [`/test/configs/`](/test/configs/).
 
 NOTE: If you are tracing on Android using adb and experiencing problems, see
-      [the Android section](#android) below.
+[the Android section](#android) below.
 
 ## TraceConfig
 
@@ -51,38 +52,40 @@
 ([reference docs](/docs/reference/trace-config-proto.autogen)) that defines:
 
 1. The general behavior of the whole tracing system, e.g.:
-    * The max duration of the trace.
-    * The number of in-memory buffers and their size.
-    * The max size of the output trace file.
+
+   - The max duration of the trace.
+   - The number of in-memory buffers and their size.
+   - The max size of the output trace file.
 
 2. Which data sources to enable and their configuration, e.g.:
-    * For the [kernel tracing data source](/docs/data-sources/cpu-scheduling.md)
-    , which ftrace events to enable.
-    * For the [heap profiler](/docs/data-sources/native-heap-profiler.md), the
-    target process name and sampling rate.
-    
-    See the _data sources_ section of the docs for details on how to
-    configure the data sources bundled with Perfetto.
 
-3. The `{data source} x {buffer}` mappings: which buffer each data
-    source should write into (see [buffers section](#buffers) below).
+   - For the [kernel tracing data source](/docs/data-sources/cpu-scheduling.md)
+     , which ftrace events to enable.
+   - For the [heap profiler](/docs/data-sources/native-heap-profiler.md), the
+     target process name and sampling rate.
 
-The tracing service (`traced`) acts as a configuration dispatcher: it receives
-a config from the `perfetto` cmdline client (or any other
+   See the _data sources_ section of the docs for details on how to configure
+   the data sources bundled with Perfetto.
+
+3. The `{data source} x {buffer}` mappings: which buffer each data source should
+   write into (see [buffers section](#buffers) below).
+
+The tracing service (`traced`) acts as a configuration dispatcher: it receives a
+config from the `perfetto` cmdline client (or any other
 [Consumer](/docs/concepts/service-model.md#consumer)) and forwards parts of the
 config to the various [Producers](/docs/concepts/service-model.md#producer)
 connected.
 
 When a tracing session is started by a consumer, the tracing service will:
 
-* Read the outer section of the TraceConfig (e.g. `duration_ms`, `buffers`) and
+- Read the outer section of the TraceConfig (e.g. `duration_ms`, `buffers`) and
   use that to determine its own behavior.
-* Read the list of data sources in the `data_sources` section. For each data
+- Read the list of data sources in the `data_sources` section. For each data
   source listed in the config, if a corresponding name (`"linux.ftrace"` in the
   example below) was registered, the service will ask the producer process to
-  start that data source, passing it the raw bytes of the
-  [`DataSourceConfig` subsection][dss] verbatim to the data source (See
-  backward/forward compat section below).
+  start that data source, passing it the raw bytes of the [`DataSourceConfig`
+  subsection][dss] verbatim to the data source (See backward/forward compat
+  section below).
 
 ![TraceConfig diagram](/docs/images/trace_config.png)
 
@@ -109,10 +112,10 @@
 
 Each buffer has a fill policy which is either:
 
-* RING_BUFFER (default): the buffer behaves like a ring buffer and writes when
+- RING_BUFFER (default): the buffer behaves like a ring buffer and writes when
   full will wrap over and replace the oldest trace data in the buffer.
 
-* DISCARD: the buffer stops accepting data once full. Further write attempts are
+- DISCARD: the buffer stops accepting data once full. Further write attempts are
   dropped.
 
 WARNING: DISCARD can have unexpected side-effect with data sources that commit
@@ -121,36 +124,34 @@
 A trace config must define at least one buffer to be valid. In the simplest case
 all data sources will write their trace data into the same buffer.
 
- While this is
-fine for most basic cases, it can be problematic in cases where different data
-sources write at significantly different rates.
+While this is fine for most basic cases, it can be problematic in cases where
+different data sources write at significantly different rates.
 
 For instance, imagine a trace config that enables both:
 
-1. The kernel scheduler tracer. On a typical Android phone this records
-   ~10000 events/second, writing ~1 MB/s of trace data into the buffer.
+1. The kernel scheduler tracer. On a typical Android phone this records ~10000
+   events/second, writing ~1 MB/s of trace data into the buffer.
 
 2. Memory stat polling. This data source writes the contents of /proc/meminfo
-   into the trace buffer and is configured to poll every 5 seconds, writing 
-   ~100 KB per poll interval.
+   into the trace buffer and is configured to poll every 5 seconds, writing ~100
+   KB per poll interval.
 
 If both data sources are configured to write into the same buffer and such
 buffer is set to 4MB, most traces will contain only one memory snapshot. There
 are very good chances that most traces won't contain any memory snapshot at all,
-even if the 2nd data sources was working perfectly.
-This is because during the 5 s. polling interval, the scheduler data source can
-end up filling the whole buffer, pushing the memory snapshot data out of the
-buffer.
+even if the 2nd data sources was working perfectly. This is because during the 5
+s. polling interval, the scheduler data source can end up filling the whole
+buffer, pushing the memory snapshot data out of the buffer.
 
 ## Dynamic buffer mapping
 
-Data-source <> buffer mappings are dynamic in Perfetto.
-In the simplest case a tracing session can define only one buffer. By default,
-all data sources will record data into that one buffer.
+Data-source <> buffer mappings are dynamic in Perfetto. In the simplest case a
+tracing session can define only one buffer. By default, all data sources will
+record data into that one buffer.
 
 In cases like the example above, it might be preferable separating these data
-sources into different buffers.
-This can be achieved with the `target_buffer` field of the TraceConfig.
+sources into different buffers. This can be achieved with the `target_buffer`
+field of the TraceConfig.
 
 ![Buffer mapping](/docs/images/trace_config_buffer_mapping.png)
 
@@ -189,11 +190,11 @@
 
 #### Text format
 
-It is the preferred format for human-driven workflows and exploration. It
-allows to pass directly the text file in the PBTX (ProtoBuf TeXtual
-representation) syntax, for the schema defined in the
-[trace_config.proto](/protos/perfetto/config/trace_config.proto)
-(see [reference docs](/docs/reference/trace-config-proto.autogen))
+It is the preferred format for human-driven workflows and exploration. It allows
+to pass directly the text file in the PBTX (ProtoBuf TeXtual representation)
+syntax, for the schema defined in the
+[trace_config.proto](/protos/perfetto/config/trace_config.proto) (see
+[reference docs](/docs/reference/trace-config-proto.autogen))
 
 When using this mode pass the `--txt` flag to `perfetto` to indicate the config
 should be interpreted as a PBTX file:
@@ -212,10 +213,9 @@
 #### Binary format
 
 It is the preferred format for machine-to-machine (M2M) interaction. It involves
-passing the protobuf-encoded binary of the TraceConfig message.
-This can be obtained passing the PBTX in input to the protobuf's `protoc`
-compiler (which can be downloaded
-[here](https://github.com/protocolbuffers/protobuf/releases)).
+passing the protobuf-encoded binary of the TraceConfig message. This can be
+obtained passing the PBTX in input to the protobuf's `protoc` compiler (which
+can be downloaded [here](https://github.com/protocolbuffers/protobuf/releases)).
 
 ```bash
 cd ~/code/perfetto  # external/perfetto in the Android tree.
@@ -236,9 +236,9 @@
 
 By default Perfetto keeps the full trace buffer(s) in memory and writes it into
 the destination file (the `-o` cmdline argument) only at the end of the tracing
-session. This is to reduce the perf-intrusiveness of the tracing system.
-This, however, limits the max size of the trace to the physical memory size of
-the device, which is often too limiting.
+session. This is to reduce the perf-intrusiveness of the tracing system. This,
+however, limits the max size of the trace to the physical memory size of the
+device, which is often too limiting.
 
 In some cases (e.g., benchmarks, hard to repro cases) it is desirable to capture
 traces that are way larger than that, at the cost of extra I/O overhead.
@@ -246,29 +246,26 @@
 To achieve that, Perfetto allows to periodically write the trace buffers into
 the target file (or stdout) using the following TraceConfig fields:
 
-* `write_into_file (bool)`:
-When true periodically drains the trace buffers into the output
-file. When this option is enabled, the userspace buffers need to be just
-big enough to hold tracing data between two write periods.
-The buffer sizing depends on the activity of the device.
-The data rate of a typical trace is ~1-4 MB/s. So a 16MB in-memory buffer can
-hold for up write periods of ~4 seconds before starting to lose data.
+- `write_into_file (bool)`: When true periodically drains the trace buffers into
+  the output file. When this option is enabled, the userspace buffers need to be
+  just big enough to hold tracing data between two write periods. The buffer
+  sizing depends on the activity of the device. The data rate of a typical trace
+  is ~1-4 MB/s. So a 16MB in-memory buffer can hold for up write periods of ~4
+  seconds before starting to lose data.
 
-* `file_write_period_ms (uint32)`:
-Overrides the default drain period (5s). Shorter periods require a smaller
-userspace buffer but increase the performance intrusiveness of tracing. If
-the period given is less than 100ms, the tracing service will use a period
-of 100ms.
+- `file_write_period_ms (uint32)`: Overrides the default drain period (5s).
+  Shorter periods require a smaller userspace buffer but increase the
+  performance intrusiveness of tracing. If the period given is less than 100ms,
+  the tracing service will use a period of 100ms.
 
-* `max_file_size_bytes (uint64)`:
-If set, stops the tracing session after N bytes have been written. Used to
-cap the size of the trace.
+- `max_file_size_bytes (uint64)`: If set, stops the tracing session after N
+  bytes have been written. Used to cap the size of the trace.
 
 For a complete example of a working trace config in long-tracing mode see
 [`/test/configs/long_trace.cfg`](/test/configs/long_trace.cfg).
 
 Summary: to capture a long trace just set `write_into_file:true`, set a long
-         `duration_ms` and use an in-memory buffer size of 32MB or more.
+`duration_ms` and use an in-memory buffer size of 32MB or more.
 
 ## Data-source specific config
 
@@ -276,7 +273,8 @@
 data-source-specific behaviors. At the proto schema level, this is defined in
 the `DataSourceConfig` section of `TraceConfig`:
 
-From [data_source_config.proto](/protos/perfetto/config/data_source_config.proto):
+From
+[data_source_config.proto](/protos/perfetto/config/data_source_config.proto):
 
 ```protobuf
 message TraceConfig {
@@ -312,42 +310,43 @@
 implements data sources.
 
 #### A note on backwards/forward compatibility
+
 The tracing service will route the raw binary blob of the `DataSourceConfig`
 message to the data sources with a matching name, without attempting to decode
 and re-encode it. If the `DataSourceConfig` section of the trace config contains
 a new field that didn't exist at the time when the service was built, the
-service will still pass the `DataSourceConfig` through to the data source.
-This allows to introduced new data sources without needing the service to
-know anything about them upfront.
+service will still pass the `DataSourceConfig` through to the data source. This
+allows to introduced new data sources without needing the service to know
+anything about them upfront.
 
 TODO: we are aware of the fact that today extending the `DataSourceConfig` with
 a custom proto requires changing the `data_source_config.proto` in the Perfetto
-repo, which is unideal for external projects. The long-term plan is to reserve
-a range of fields for non-upstream extensions and provide generic templated
+repo, which is unideal for external projects. The long-term plan is to reserve a
+range of fields for non-upstream extensions and provide generic templated
 accessors for client code. Until then, we accept patches upstream to introduce
 ad-hoc configurations for your own data sources.
 
 ## Multi-process data sources
 
 Some data sources are singletons. E.g., in the case of scheduler tracing that
-Perfetto ships on Android, there is only data source for the whole system,
-owned by the `traced_probes` service.
+Perfetto ships on Android, there is only data source for the whole system, owned
+by the `traced_probes` service.
 
 However, in the general case multiple processes can advertise the same data
 source. This is the case, for instance, when using the
 [Perfetto SDK](/docs/instrumentation/tracing-sdk.md) for userspace
 instrumentation.
 
-If this happens, when starting a tracing session that specifies that data
-source in the trace config, Perfetto by default will ask all processes that
-advertise that data source to start it.
+If this happens, when starting a tracing session that specifies that data source
+in the trace config, Perfetto by default will ask all processes that advertise
+that data source to start it.
 
 In some cases it might be desirable to further limit the enabling of the data
 source to a specific process (or set of processes). That is possible through the
 `producer_name_filter` and `producer_name_regex_filter`.
 
 NOTE: the typical Perfetto run-time model is: one process == one Perfetto
-      Producer; one Producer typically hosts multiple data sources.
+Producer; one Producer typically hosts multiple data sources.
 
 When those filters are set, the Perfetto tracing service will activate the data
 source only in the subset of producers matching the filter.
@@ -380,8 +379,8 @@
 which is based on triggers. The overall idea is to declare in the trace config
 itself:
 
-* A set of triggers, which are just free-form strings.
-* Whether a given trigger should cause the trace to be started or stopped, and
+- A set of triggers, which are just free-form strings.
+- Whether a given trigger should cause the trace to be started or stopped, and
   the start/stop delay.
 
 Why using triggers? Why can't one just start perfetto or kill(SIGTERM) it when
@@ -393,12 +392,12 @@
 Triggers offer a way to unprivileged apps to control, in a limited fashion, the
 lifecycle of a tracing session. The conceptual model is:
 
-* The privileged Consumer (see
-  [_Service model_](/docs/concepts/service-model.md)), i.e. the entity
-  that is normally authorized to start tracing (e.g., adb shell in Android),
-  declares upfront what are the possible trigger names for the trace and what
-  they will do.
-* Unprivileged entities (any random app process) can activate those triggers.
+- The privileged Consumer (see
+  [_Service model_](/docs/concepts/service-model.md)), i.e. the entity that is
+  normally authorized to start tracing (e.g., adb shell in Android), declares
+  upfront what are the possible trigger names for the trace and what they will
+  do.
+- Unprivileged entities (any random app process) can activate those triggers.
   Unprivileged entities don't get a say on what the triggers will do, they only
   communicate that an event happened.
 
@@ -417,12 +416,13 @@
 
 Start triggers allow activating a tracing session only after some significant
 event has happened. Passing a trace config that has `START_TRACING` trigger
-causes the tracing session to stay idle (i.e. not recording any data) until either
-the trigger is hit or the `trigger_timeout_ms` timeout is hit.
+causes the tracing session to stay idle (i.e. not recording any data) until
+either the trigger is hit or the `trigger_timeout_ms` timeout is hit.
 
 `trace_duration_ms` and triggered traces can not be used at the same time.
 
 Example config:
+
 ```protobuf
 # If the "myapp_is_slow" is hit, the trace starts recording data and will be
 # stopped after 5s.
@@ -450,12 +450,13 @@
 signal.
 
 This can be used to use perfetto in flight-recorder mode. By starting a trace
-with buffers configured in `RING_BUFFER` mode and `STOP_TRACING` triggers,
-the trace will be recorded in a loop and finalized when the culprit event is
+with buffers configured in `RING_BUFFER` mode and `STOP_TRACING` triggers, the
+trace will be recorded in a loop and finalized when the culprit event is
 detected. This is key for events where the root cause is in the recent past
 (e.g., the app detects a slow scroll or a missing frame).
 
 Example config:
+
 ```protobuf
 # If no trigger is hit, the trace will end after 30s.
 trigger_timeout_ms: 30000
@@ -478,21 +479,20 @@
 
 On Android, there are some caveats around using `adb shell`
 
-* Ctrl+C, which normally causes a graceful termination of the trace, is not
+- Ctrl+C, which normally causes a graceful termination of the trace, is not
   propagated by ADB when using `adb shell perfetto` but only when using an
   interactive PTY-based session via `adb shell`.
-* On non-rooted devices before Android 12, the config can only be passed as
+- On non-rooted devices before Android 12, the config can only be passed as
   `cat config | adb shell perfetto -c -` (-: stdin) because of over-restrictive
   SELinux rules. Since Android 12 `/data/misc/perfetto-configs` can be used for
   storing configs.
-* On devices before Android 10, adb cannot directly pull
+- On devices before Android 10, adb cannot directly pull
   `/data/misc/perfetto-traces`. Use
   `adb shell cat /data/misc/perfetto-traces/trace > trace` to work around.
-* When capturing longer traces, e.g. in the context of benchmarks or CI, use
+- When capturing longer traces, e.g. in the context of benchmarks or CI, use
   `PID=$(perfetto --background)` and then `kill $PID` to stop.
 
-
 ## Other resources
 
-* [TraceConfig Reference](/docs/reference/trace-config-proto.autogen)
-* [Buffers and dataflow](/docs/concepts/buffers.md)
+- [TraceConfig Reference](/docs/reference/trace-config-proto.autogen)
+- [Buffers and dataflow](/docs/concepts/buffers.md)
diff --git a/docs/contributing/common-tasks.md b/docs/contributing/common-tasks.md
index 43c5421..4a76189 100644
--- a/docs/contributing/common-tasks.md
+++ b/docs/contributing/common-tasks.md
@@ -24,7 +24,7 @@
 
 - Running the file cannot generate any data. There can be only `CREATE PERFETTO {FUNCTION|TABLE|VIEW|MACRO}` statements inside.
 - The name of each standard library object needs to start with `{module_name}_` or be prefixed with an underscore(`_`) for internal objects.
-  The names must only contain lower and upper case letters and underscores. When a module is included (using the `INCLUDE PERFETTO MODULE`) the internal objects  should not be treated as an API. 
+  The names must only contain lower and upper case letters and underscores. When a module is included (using the `INCLUDE PERFETTO MODULE`) the internal objects  should not be treated as an API.
 - Every table or view should have [a schema](/docs/analysis/perfetto-sql-syntax.md#tableview-schema).
 
 ### Documentation
@@ -98,11 +98,11 @@
   arg_set_id INT
 )
 AS
-SELECT 
-  slice_name, 
-  slice_ts, 
-  slice_dur, 
-  thread_name, 
+SELECT
+  slice_name,
+  slice_ts,
+  slice_dur,
+  thread_name,
   arg_set_id
 FROM thread_slices_for_all_launches
 WHERE launch_id = $launch_id AND slice_name GLOB $slice_name;
@@ -156,3 +156,17 @@
 1. Go to `protos/perfetto/trace_processor/trace_processor.proto`
 2. Increment `TRACE_PROCESSOR_CURRENT_API_VERSION`
 3. Add a comment explaining what has changed.
+
+## Update statsd descriptor
+
+Perfetto has limited support for statsd atoms it does not know about.
+
+* Must be referred to using `raw_atom_id` in the config.
+* Show up as `atom_xxx.field_yyy` in trace processor.
+* Only top level messages are parsed.
+
+To update Perfetto's descriptor and handle new atoms from AOSP without these
+limitations:
+
+1. Run `tools/update-statsd-descriptor`.
+2. Upload and land your change as normal.
diff --git a/docs/data-sources/syscalls.md b/docs/data-sources/syscalls.md
index 3e6156d..8e769e8 100644
--- a/docs/data-sources/syscalls.md
+++ b/docs/data-sources/syscalls.md
@@ -15,13 +15,13 @@
 
 At the UI level system calls are shown inlined with the per-thread slice tracks:
 
-![](/docs/images/syscalls.png "System calls in the thread tracks")
+![](/docs/images/syscalls.png 'System calls in the thread tracks')
 
 ## SQL
 
 At the SQL level, syscalls are no different than any other userspace slice
 event. They get interleaved in the per-thread slice stack and can be easily
-filtered by looking for the 'sys_' prefix:
+filtered by looking for the 'sys\_' prefix:
 
 ```sql
 select ts, dur, t.name as thread, s.name, depth from slices as s
@@ -30,14 +30,14 @@
 where s.name like 'sys_%'
 ```
 
-ts | dur | thread | name 
----|-----|--------|------
-856325324372751 | 439867648 | s.nexuslauncher | sys_epoll_pwait
-856325324376970 | 990 | FpsThrottlerThr | sys_recvfrom
-856325324378376 | 2657 | surfaceflinger | sys_ioctl
-856325324419574 | 1250 | android.anim.lf | sys_recvfrom
-856325324428168 | 27344 | android.anim.lf | sys_ioctl
-856325324451345 | 573 | FpsThrottlerThr | sys_getuid
+| ts              | dur       | thread          | name            |
+| --------------- | --------- | --------------- | --------------- |
+| 856325324372751 | 439867648 | s.nexuslauncher | sys_epoll_pwait |
+| 856325324376970 | 990       | FpsThrottlerThr | sys_recvfrom    |
+| 856325324378376 | 2657      | surfaceflinger  | sys_ioctl       |
+| 856325324419574 | 1250      | android.anim.lf | sys_recvfrom    |
+| 856325324428168 | 27344     | android.anim.lf | sys_ioctl       |
+| 856325324451345 | 573       | FpsThrottlerThr | sys_getuid      |
 
 ## TraceConfig
 
diff --git a/docs/instrumentation/tracing-sdk.md b/docs/instrumentation/tracing-sdk.md
index a77d363..a3b7c85a 100644
--- a/docs/instrumentation/tracing-sdk.md
+++ b/docs/instrumentation/tracing-sdk.md
@@ -5,27 +5,26 @@
 
 When using the Tracing SDK there are two main aspects to consider:
 
-1. Whether you are interested only in tracing events coming from your own app
-   or want to collect full-stack traces that overlay app trace events with
-   system trace events like scheduler traces, syscalls or any other Perfetto
-   data source.
+1. Whether you are interested only in tracing events coming from your own app or
+   want to collect full-stack traces that overlay app trace events with system
+   trace events like scheduler traces, syscalls or any other Perfetto data
+   source.
 
 2. For app-specific tracing, whether you need to trace simple types of timeline
-  events (e.g., slices, counters) or need to define complex data sources with a
-  custom strongly-typed schema (e.g., for dumping the state of a subsystem of
-  your app into the trace).
+   events (e.g., slices, counters) or need to define complex data sources with a
+   custom strongly-typed schema (e.g., for dumping the state of a subsystem of
+   your app into the trace).
 
 For Android-only instrumentation, the advice is to keep using the existing
-[android.os.Trace (SDK)][atrace-sdk] / [ATrace_* (NDK)][atrace-ndk] if they
+[android.os.Trace (SDK)][atrace-sdk] / [ATrace\_\* (NDK)][atrace-ndk] if they
 are sufficient for your use cases. Atrace-based instrumentation is fully
-supported in Perfetto.
-See the [Data Sources -> Android System -> Atrace Instrumentation][atrace-ds]
-for details.
+supported in Perfetto. See the [Data Sources -> Android System -> Atrace
+Instrumentation][atrace-ds] for details.
 
 ## Getting started
 
-TIP: The code from these examples is also available [in the
-repository](/examples/sdk/README.md).
+TIP: The code from these examples is also available
+[in the repository](/examples/sdk/README.md).
 
 To start using the Client API, first check out the latest SDK release:
 
@@ -105,9 +104,8 @@
 
 Track events are the suggested option when dealing with app-specific tracing as
 they take care of a number of subtleties (e.g., thread safety, flushing, string
-interning).
-Track events are time bounded events (e.g., slices, counter) based on simple
-`TRACE_EVENT` annotation tags in the codebase, like this:
+interning). Track events are time bounded events (e.g., slices, counter) based
+on simple `TRACE_EVENT` annotation tags in the codebase, like this:
 
 ```c++
 #include <perfetto.h>
@@ -167,15 +165,14 @@
 ### Custom data sources
 
 For most uses, track events are the most straightforward way of instrumenting
-apps for tracing. However, in some rare circumstances they are not
-flexible enough, e.g., when the data doesn't fit the notion of a track or is
-high volume enough that it needs a strongly typed schema to minimize the size of
-each event. In this case, you can implement a *custom data source* for
-Perfetto.
+apps for tracing. However, in some rare circumstances they are not flexible
+enough, e.g., when the data doesn't fit the notion of a track or is high volume
+enough that it needs a strongly typed schema to minimize the size of each event.
+In this case, you can implement a _custom data source_ for Perfetto.
 
 Unlike track events, when working with custom data sources, you will also need
-corresponding changes in [trace processor](/docs/analysis/trace-processor.md)
-to enable importing your data format.
+corresponding changes in [trace processor](/docs/analysis/trace-processor.md) to
+enable importing your data format.
 
 A custom data source is a subclass of `perfetto::DataSource`. Perfetto will
 automatically create one instance of the class for each tracing session it is
@@ -248,9 +245,9 @@
 ```
 
 If necessary the `Trace()` method can access the custom data source state
-(`my_custom_state` in the example above). Doing so, will take a mutex to
-ensure data source isn't destroyed (e.g., because of stopping tracing) while
-the `Trace()` method is called on another thread. For example:
+(`my_custom_state` in the example above). Doing so, will take a mutex to ensure
+data source isn't destroyed (e.g., because of stopping tracing) while the
+`Trace()` method is called on another thread. For example:
 
 ```C++
 CustomDataSource::Trace([](CustomDataSource::TraceContext ctx) {
@@ -261,9 +258,9 @@
 
 ## In-process vs System mode
 
-The two modes are not mutually exclusive. An app can be configured to work
-in both modes and respond both to in-process tracing requests and system
-tracing requests. Both modes generate the same trace file format.
+The two modes are not mutually exclusive. An app can be configured to work in
+both modes and respond both to in-process tracing requests and system tracing
+requests. Both modes generate the same trace file format.
 
 ### In-process mode
 
@@ -275,8 +272,8 @@
 `TracingInitArgs.backends = perfetto::kInProcessBackend` when initializing the
 SDK, see examples below.
 
-This mode is used to generate traces that contain only events emitted by
-the app, but not other types of events (e.g. scheduler traces).
+This mode is used to generate traces that contain only events emitted by the
+app, but not other types of events (e.g. scheduler traces).
 
 The main advantage is that by running fully in-process, it doesn't require any
 special OS privileges and the profiled process can control the lifecycle of
@@ -293,32 +290,32 @@
 `TracingInitArgs.backends = perfetto::kSystemBackend` when initializing the SDK,
 see examples below.
 
-The main advantage of this mode is that it is possible to create fused traces where
-app events are overlaid on the same timeline of OS events. This enables
+The main advantage of this mode is that it is possible to create fused traces
+where app events are overlaid on the same timeline of OS events. This enables
 full-stack performance investigations, looking all the way through syscalls and
 kernel scheduling events.
 
-The main limitation of this mode is that it requires the external `traced` daemon
-to be up and running and reachable through the UNIX socket connection.
+The main limitation of this mode is that it requires the external `traced`
+daemon to be up and running and reachable through the UNIX socket connection.
 
 This is suggested for local debugging or lab testing scenarios where the user
 (or the test harness) can control the OS deployment (e.g., sideload binaries on
 Android).
 
 When using system mode, the tracing session must be controlled from the outside,
-using the `perfetto` command-line client
-(See [reference](/docs/reference/perfetto-cli)). This is because when collecting
+using the `perfetto` command-line client (See
+[reference](/docs/reference/perfetto-cli)). This is because when collecting
 system traces, tracing data producers are not allowed to read back the trace
 data as it might disclose information about other processes and allow
 side-channel attacks.
 
-* On Android 9 (Pie) and beyond, traced is shipped as part of the platform.
-* On older versions of Android, traced can be built from sources using the
-  the [standalone NDK-based workflow](/docs/contributing/build-instructions.md)
-  and sideloaded via adb shell.
-* On Linux and MacOS and Windows `traced` must be built and run separately. See
+- On Android 9 (Pie) and beyond, traced is shipped as part of the platform.
+- On older versions of Android, traced can be built from sources using the the
+  [standalone NDK-based workflow](/docs/contributing/build-instructions.md) and
+  sideloaded via adb shell.
+- On Linux and MacOS and Windows `traced` must be built and run separately. See
   the [Linux quickstart](/docs/quickstart/linux-tracing.md) for instructions.
-* On Windows the tracing protocol works over TCP/IP (
+- On Windows the tracing protocol works over TCP/IP (
   [127.0.0.1:32278](https://cs.android.com/android/platform/superproject/main/+/main:external/perfetto/src/tracing/ipc/default_socket.cc;l=75;drc=4f88a2fdfd3801c109d5e927b8206f9756288b12)
   ) + named shmem.
 
@@ -370,12 +367,12 @@
 ```
 
 TIP: API methods with `Blocking` in their name will suspend the calling thread
-     until the respective operation is complete. There are also asynchronous
-     variants that don't have this limitation.
+until the respective operation is complete. There are also asynchronous variants
+that don't have this limitation.
 
-Now that tracing is active, instruct your app to perform the operation you
-want to record. After that, stop tracing and collect the
-protobuf-formatted trace data:
+Now that tracing is active, instruct your app to perform the operation you want
+to record. After that, stop tracing and collect the protobuf-formatted trace
+data:
 
 ```C++
 tracing_session->StopBlocking();
@@ -388,9 +385,9 @@
 output.close();
 ```
 
-To save memory with longer traces, you can also tell Perfetto to write
-directly into a file by passing a file descriptor into Setup(), remembering
-to close the file after tracing is done:
+To save memory with longer traces, you can also tell Perfetto to write directly
+into a file by passing a file descriptor into Setup(), remembering to close the
+file after tracing is done:
 
 ```C++
 int fd = open("example.perfetto-trace", O_RDWR | O_CREAT | O_TRUNC, 0600);
@@ -401,8 +398,9 @@
 close(fd);
 ```
 
-The resulting trace file can be directly opened in the [Perfetto
-UI](https://ui.perfetto.dev) or the [Trace Processor](/docs/analysis/trace-processor.md).
+The resulting trace file can be directly opened in the
+[Perfetto UI](https://ui.perfetto.dev) or the
+[Trace Processor](/docs/analysis/trace-processor.md).
 
 [ipc]: /docs/design-docs/api-and-abi.md#socket-protocol
 [atrace-ds]: /docs/data-sources/atrace.md
diff --git a/docs/quickstart/callstack-sampling.md b/docs/quickstart/callstack-sampling.md
index 30fd74a..f597e5c 100644
--- a/docs/quickstart/callstack-sampling.md
+++ b/docs/quickstart/callstack-sampling.md
@@ -2,12 +2,12 @@
 
 ## Prerequisites
 
-*   [ADB](https://developer.android.com/studio/command-line/adb) installed.
-*   A device running Android T+.
-*   Either a debuggable (`userdebug`/`eng`) Android image, or the apps to be
-    profiled need to be
-    [marked as profileable or debuggable](https://developer.android.com/guide/topics/manifest/profileable-element)
-    in their manifests.
+- [ADB](https://developer.android.com/studio/command-line/adb) installed.
+- A device running Android T+.
+- Either a debuggable (`userdebug`/`eng`) Android image, or the apps to be
+  profiled need to be
+  [marked as profileable or debuggable](https://developer.android.com/guide/topics/manifest/profileable-element)
+  in their manifests.
 
 ## Capture a CPU profile
 
@@ -130,6 +130,6 @@
 
 `cpu_profile` will also write separate profiles for each process that it
 profiled in the output directory, and those can be visualized using
-[`pprof`](https://github.com/google/pprof). You can merge them into one
-by passing all of them to pprof, e.g.
+[`pprof`](https://github.com/google/pprof). You can merge them into one by
+passing all of them to pprof, e.g.
 `pprof /tmp/perf_profile-240105114948clvad/*`.
diff --git a/docs/toc.md b/docs/toc.md
index 01be2af..deaeb5d 100644
--- a/docs/toc.md
+++ b/docs/toc.md
@@ -48,7 +48,6 @@
     * [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)
diff --git a/gn/proto_library.gni b/gn/proto_library.gni
index 6bc1760..af540c2 100644
--- a/gn/proto_library.gni
+++ b/gn/proto_library.gni
@@ -107,6 +107,7 @@
                              "generator_plugin_options",
                              "include_dirs",
                              "proto_data_sources",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -153,6 +154,7 @@
                              "defines",
                              "generator_plugin_options",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "sources",
@@ -205,6 +207,7 @@
                              "defines",
                              "extra_configs",
                              "include_dirs",
+                             "proto_deps",
                              "proto_in_dir",
                              "proto_out_dir",
                              "generator_plugin_options",
@@ -286,40 +289,6 @@
   # build generators and for generating descriptors.
   source_set_target_name =
       string_replace(target_name, expansion_token, "source_set")
-  group(source_set_target_name) {
-    public_deps_ = []
-    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 ]
-      }
-    }
-
-    deps = []
-    if (defined(invoker.deps)) {
-      foreach(dep, invoker.deps) {
-        mapped_dep = string_replace(dep, expansion_token, "source_set")
-        deps += [ mapped_dep ]
-      }
-    }
-    deps += public_deps_
-
-    sources = []
-    foreach(source, invoker.sources) {
-      sources += [ get_path_info(source, "abspath") ]
-    }
-
-    metadata = {
-      proto_library_sources = sources
-      proto_import_dirs = import_dirs_
-      exports = []
-      foreach(dep, public_deps_) {
-        exports +=
-            [ get_label_info(dep, "dir") + ":" + get_label_info(dep, "name") ]
-      }
-    }
-  }
 
   # This config is necessary for Chrome proto_library build rule to work
   # correctly.
@@ -328,6 +297,42 @@
     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")
@@ -385,6 +390,7 @@
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=pbzero"
         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)
@@ -395,6 +401,7 @@
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
         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)
@@ -405,6 +412,7 @@
         proto_in_dir = proto_path
         proto_out_dir = proto_path
         generator_plugin_options = "wrapper_namespace=gen"
+        proto_deps = [ ":$source_set_target_name" ]
         deps = all_deps_ + [ ":$cpp_target_name_" ]
         propagate_imports_configs = propagate_imports_configs_
         import_dirs = import_dirs_
@@ -419,6 +427,7 @@
         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 {
diff --git a/gn/standalone/proto_library.gni b/gn/standalone/proto_library.gni
index 93ed9c9..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.
diff --git a/infra/perfetto.dev/BUILD.gn b/infra/perfetto.dev/BUILD.gn
index 25ff547..b81fa3d 100644
--- a/infra/perfetto.dev/BUILD.gn
+++ b/infra/perfetto.dev/BUILD.gn
@@ -358,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/src/assets/script.js b/infra/perfetto.dev/src/assets/script.js
index aa368bc..6c4d04d 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();
@@ -89,7 +69,7 @@
     toc.appendChild(li);
     doAfterLoadEvent(() => {
       tocAnchors.push(
-          {top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link});
+        { top: anchor.offsetTop + anchor.offsetHeight / 2, obj: link });
     });
   }
   tocContainer.innerHTML = '';
@@ -101,7 +81,7 @@
     return;
   tocEventHandlersInstalled = true;
   const doc = document.querySelector('.doc');
-  const passive = {passive: true};
+  const passive = { passive: true };
   if (doc) {
     const offY = doc.offsetTop;
     doc.addEventListener('mousemove', (e) => onMouseMove(offY, e), passive);
@@ -111,9 +91,9 @@
   }
   window.addEventListener('scroll', () => onScroll(), passive);
   resizeObserver = new ResizeObserver(() => requestAnimationFrame(() => {
-                                        updateNav();
-                                        updateTOC();
-                                      }));
+    updateNav();
+    updateTOC();
+  }));
   resizeObserver.observe(doc);
 }
 
@@ -255,7 +235,7 @@
 
 function setupSearch() {
   const URL =
-      'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q='
+    'https://www.googleapis.com/customsearch/v1?key=AIzaSyBTD2XJkQkkuvDn76LSftsgWOkdBz9Gfwo&cx=007128963598137843411:8suis14kcmy&q='
   const searchContainer = document.getElementById('search');
   const searchBox = document.getElementById('search-box');
   const searchRes = document.getElementById('search-res')
@@ -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/assets/style.scss b/infra/perfetto.dev/src/assets/style.scss
index 63d6fc1..0a0ff2e 100644
--- a/infra/perfetto.dev/src/assets/style.scss
+++ b/infra/perfetto.dev/src/assets/style.scss
@@ -15,234 +15,235 @@
 // Common + CSS reset
 // -----------------------------------------------------------------------------
 :root {
-    --site-header-height: 50px;
-    --home-highlights-height: 128px;
-    --content-max-width: 1100px;
-    --anim-ease: cubic-bezier(0.4, 0.0, 0.2, 1);
+  --site-header-height: 50px;
+  --home-highlights-height: 128px;
+  --content-max-width: 1100px;
+  --anim-ease: cubic-bezier(0.4, 0, 0.2, 1);
 
-    // This is set to 1 by JS after onload. This is to prevent flickering on
-    // page load on the nav bar and other entries while transitioning in their
-    // initial state.
-    --anim-enabled: 0;
+  // This is set to 1 by JS after onload. This is to prevent flickering on
+  // page load on the nav bar and other entries while transitioning in their
+  // initial state.
+  --anim-enabled: 0;
 
-    --anim-time: calc(0.15s * var(--anim-enabled));
+  --anim-time: calc(0.15s * var(--anim-enabled));
 }
 
 $wide: "(max-width: 1100px)";
 $mobile: "(max-width: 768px)";
 
 @mixin minimal-scrollbar {
-    &::-webkit-scrollbar {
-        width: 8px;
-        background-color: transparent;
-    }
-    &::-webkit-scrollbar-thumb {
-        background-color: #ccc;
-        border-radius: 8px;
-    }
+  &::-webkit-scrollbar {
+    width: 8px;
+    background-color: transparent;
+  }
+  &::-webkit-scrollbar-thumb {
+    background-color: #ccc;
+    border-radius: 8px;
+  }
 }
 
 @media (max-aspect-ratio: 1/1) {
-     :root {
-        --home-highlights-height: 256px;
-    }
+  :root {
+    --home-highlights-height: 256px;
+  }
 }
 
 * {
-    box-sizing: border-box;
-    -webkit-tap-highlight-color: none;
+  box-sizing: border-box;
+  -webkit-tap-highlight-color: none;
 }
 
 html {
-    font-family: Roboto, sans-serif;
-    -webkit-font-smoothing: antialiased;
+  font-family: Roboto, sans-serif;
+  -webkit-font-smoothing: antialiased;
 }
 
 html,
 body {
-    padding: 0;
-    margin: 0;
+  padding: 0;
+  margin: 0;
 }
 
 h1,
 h2,
 h3 {
-    font-family: inherit;
-    font-size: inherit;
-    font-weight: inherit;
-    padding: 0;
-    margin: 0;
+  font-family: inherit;
+  font-size: inherit;
+  font-weight: inherit;
+  padding: 0;
+  margin: 0;
 }
 
 // -----------------------------------------------------------------------------
 // Site header
 // -----------------------------------------------------------------------------
 .site-header {
-    background-color: hsl(210, 30%, 16%);
-    color: hsl(210, 17%, 98%);
-    position: sticky; // Sticky so the .docs element below doesn't start @ 0.
+  background-color: hsl(210, 30%, 16%);
+  color: hsl(210, 17%, 98%);
+  position: sticky; // Sticky so the .docs element below doesn't start @ 0.
+  top: 0;
+  width: 100%;
+  --sh-padding-y: 5px;
+  max-height: var(--site-header-height);
+  padding: var(--sh-padding-y) 30px;
+  box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
+  overflow: hidden;
+  display: flex;
+  z-index: 10;
+  transition: max-height var(--anim-ease) var(--anim-time);
+  &.expanded {
+    max-height: 100vh;
+  }
+  .brand {
+    img {
+      height: 40px;
+      vertical-align: bottom;
+    }
+    font-weight: 200;
+    font-size: 28px;
+    flex-grow: 1;
+    .brand-docs {
+      text-transform: uppercase;
+      font-size: 14px;
+      color: #ecba2a;
+      vertical-align: bottom;
+      line-height: 30px;
+      font-weight: 400;
+    }
+  }
+  > *:not(:first-child) {
+    line-height: calc(var(--site-header-height) - var(--sh-padding-y) * 2);
+    font-family: "Source Sans Pro", sans-serif;
+    font-weight: 400;
+    font-size: 1.1rem;
+    margin: 0 20px;
+    color: hsl(210, 17%, 85%);
+  }
+  a {
+    text-decoration: none;
+    &:hover {
+      color: hsl(210, 17%, 100%);
+    }
+  }
+  .menu {
+    visibility: hidden;
+    font-family: "Material Icons Round";
+    font-size: 24px;
+    text-align: center;
+    position: absolute;
+    right: 0;
     top: 0;
-    width: 100%;
-    --sh-padding-y: 5px;
-    max-height: var(--site-header-height);
-    padding: var(--sh-padding-y) 30px;
-    box-shadow: rgba(0, 0, 0, 0.3) 0 3px 3px 0;
-    overflow: hidden;
-    display: flex;
-    z-index: 10;
-    transition: max-height var(--anim-ease) var(--anim-time);
-    &.expanded {
-        max-height: 100vh;
-    }
-    .brand {
-        img {
-            height: 40px;
-            vertical-align: bottom;
-        }
-        font-weight: 200;
-        font-size: 28px;
-        flex-grow: 1;
-        .brand-docs {
-            text-transform: uppercase;
-            font-size: 14px;
-            color: #ecba2a;
-            vertical-align: bottom;
-            line-height: 30px;
-            font-weight: 400;
-        }
-    }
-    >*:not(:first-child) {
-        line-height: calc(var(--site-header-height) - var(--sh-padding-y) * 2);
-        font-family: 'Source Sans Pro', sans-serif;
-        font-weight: 400;
-        font-size: 1.1rem;
-        margin: 0 20px;
-        color: hsl(210, 17%, 85%);
-    }
-    a {
-        text-decoration: none;
-        &:hover {
-            color: hsl(210, 17%, 100%);
-        }
+    line-height: var(--site-header-height);
+  }
+
+  @media #{$mobile} {
+    flex-direction: column;
+    > *:not(:first-child) {
+      margin-left: 40px;
     }
     .menu {
-        visibility: hidden;
-        font-family: 'Material Icons Round';
-        font-size: 24px;
-        text-align: center;
-        position: absolute;
-        right: 0;
-        top: 0;
-        line-height: var(--site-header-height);
+      visibility: visible;
     }
-
-    @media #{$mobile} {
-        flex-direction: column;
-        >*:not(:first-child) {
-            margin-left: 40px;
-        }
-        .menu {
-            visibility: visible;
-        }
-    }
+  }
 }
 
-
 #search {
-    position: relative;
-    flex-grow: 0;
-    transition: flex-grow cubic-bezier(1, 0.01, 1, 1) var(--anim-time), background-color ease var(--anim-time);
-    padding: 0;
+  position: relative;
+  flex-grow: 0;
+  transition: flex-grow cubic-bezier(1, 0.01, 1, 1) var(--anim-time),
+    background-color ease var(--anim-time);
+  padding: 0;
+  &::before {
+    visibility: hidden;
+    user-select: none;
+    content: "";
+    position: fixed;
+    left: 0;
+    right: 0;
+    top: var(--site-header-height);
+    bottom: 0;
+    z-index: -100;
+    background-color: rgba(255, 255, 255, 0.8);
+    backdrop-filter: blur(3px);
+    opacity: 0;
+    transition: opacity ease var(--anim-time), visibility 0s;
+  }
+  &:focus-within {
+    flex-grow: 1000;
     &::before {
-        visibility: hidden;
-        user-select: none;
-        content: '';
-        position: fixed;
-        left: 0;
-        right: 0;
-        top: var(--site-header-height);
-        bottom: 0;
-        z-index: -100;
-        background-color: rgba(255, 255, 255, 0.8);
-        backdrop-filter: blur(3px);
-        opacity: 0;
-        transition: opacity ease var(--anim-time), visibility 0s;
-
+      display: block;
+      opacity: 1;
+      visibility: visible;
     }
-    &:focus-within {
-        flex-grow: 1000;
-        &::before {
-            display: block;
-            opacity: 1;
-            visibility: visible;
-        }
-        #search-res {
-            display: block;
-        }
-
-    }
-
-    @media #{$mobile} {
-        display: none;
-    }
-
-    #search-box {
-        width: 100%;
-        height: 32px;
-        font-size: 1rem;;
-        color: #333;
-        background-color: rgba(255, 255, 255, 0.9);
-        border: 1px solid #eee;
-        border-radius: 2px;
-        background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39.8 41.95 26.65 28.8q-1.5 1.3-3.5 2.025-2 .725-4.25.725-5.4 0-9.15-3.75T6 18.75q0-5.3 3.75-9.05 3.75-3.75 9.1-3.75 5.3 0 9.025 3.75 3.725 3.75 3.725 9.05 0 2.15-.7 4.15-.7 2-2.1 3.75L42 39.75Zm-20.95-13.4q4.05 0 6.9-2.875Q28.6 22.8 28.6 18.75t-2.85-6.925Q22.9 8.95 18.85 8.95q-4.1 0-6.975 2.875T9 18.75q0 4.05 2.875 6.925t6.975 2.875Z"/></svg>');
-        background-repeat: no-repeat;
-        background-size: contain;
-        padding-left: 40px;
-        outline: none;
-        &:hover, &:focus {
-            background-color: rgba(255, 255, 255, 0.95);
-        }
-    }
-
     #search-res {
-        display: none;
-        background-color: rgba(255, 255, 255, 1.0);
-        border: 1px solid #eee;
-        box-shadow: #aaa 0px 1px 5px;
-        color: #333;
-        line-height: initial;
-        margin-top: -4px;
-        overflow-x: auto;
-        position: fixed;
-        top: var(--site-header-height);
-        max-height: calc(100vh - var(--site-header-height));
-        z-index: 10;
-        >div {
-            padding: 10px;
-            margin: 0;
-            &:hover {
-                background-color: #f0f0f0;
-            }
-        }
-        .sr-title {
-            color: #333;
-            font-weight: bold;
-        }
-        .sr-snippet {
-            color: #444;
-            font-size: 0.9rem;
-         }
+      display: block;
+    }
+  }
 
-        a { text-decoration: none; }
-        a:hover { color: initial };
+  @media #{$mobile} {
+    display: none;
+  }
 
-        &:empty {
-            visibility: hidden;
-        }
+  #search-box {
+    width: 100%;
+    height: 32px;
+    font-size: 1rem;
+    color: #333;
+    background-color: rgba(255, 255, 255, 0.9);
+    border: 1px solid #eee;
+    border-radius: 2px;
+    background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" height="48" width="48"><path d="M39.8 41.95 26.65 28.8q-1.5 1.3-3.5 2.025-2 .725-4.25.725-5.4 0-9.15-3.75T6 18.75q0-5.3 3.75-9.05 3.75-3.75 9.1-3.75 5.3 0 9.025 3.75 3.725 3.75 3.725 9.05 0 2.15-.7 4.15-.7 2-2.1 3.75L42 39.75Zm-20.95-13.4q4.05 0 6.9-2.875Q28.6 22.8 28.6 18.75t-2.85-6.925Q22.9 8.95 18.85 8.95q-4.1 0-6.975 2.875T9 18.75q0 4.05 2.875 6.925t6.975 2.875Z"/></svg>');
+    background-repeat: no-repeat;
+    background-size: contain;
+    padding-left: 40px;
+    outline: none;
+    &:hover,
+    &:focus {
+      background-color: rgba(255, 255, 255, 0.95);
+    }
+  }
+
+  #search-res {
+    display: none;
+    background-color: rgba(255, 255, 255, 1);
+    border: 1px solid #eee;
+    box-shadow: #aaa 0px 1px 5px;
+    color: #333;
+    line-height: initial;
+    margin-top: -4px;
+    overflow-x: auto;
+    position: fixed;
+    top: var(--site-header-height);
+    max-height: calc(100vh - var(--site-header-height));
+    z-index: 10;
+    > div {
+      padding: 10px;
+      margin: 0;
+      &:hover {
+        background-color: #f0f0f0;
+      }
+    }
+    .sr-title {
+      color: #333;
+      font-weight: bold;
+    }
+    .sr-snippet {
+      color: #444;
+      font-size: 0.9rem;
     }
 
-}
+    a {
+      text-decoration: none;
+    }
+    a:hover {
+      color: initial;
+    }
 
+    &:empty {
+      visibility: hidden;
+    }
+  }
+}
 
 // -----------------------------------------------------------------------------
 // Site footer
@@ -250,722 +251,759 @@
 
 // Footer in the index page.
 .site-footer {
-    background-color: hsl(210, 30%, 16%);
-    padding: 1em 0;
-    font-size: 14px;
-    color: #fff;
-    text-align: center;
-    ul {
-        list-style: none;
-        margin: 0;
-        padding: 0;
-        li {
-            display: inline;
-            padding: 0 10px;
-            &:not(:last-child) {
-                border-right: solid 1px #fff;
-            }
-        }
+  background-color: hsl(210, 30%, 16%);
+  padding: 1em 0;
+  font-size: 14px;
+  color: #fff;
+  text-align: center;
+  ul {
+    list-style: none;
+    margin: 0;
+    padding: 0;
+    li {
+      display: inline;
+      padding: 0 10px;
+      &:not(:last-child) {
+        border-right: solid 1px #fff;
+      }
     }
-    a,
-    a:visited {
-        text-decoration: none;
-        color: inherit;
-    }
-    .docs-footer-notice { display: none; }
+  }
+  a,
+  a:visited {
+    text-decoration: none;
+    color: inherit;
+  }
+  .docs-footer-notice {
+    display: none;
+  }
 }
 
 // Footer overrides for the /docs/ page.
 .docs .site-footer {
-    grid-area: footer;
-    background: transparent;
-    color: #666;
-    text-align: left;
-    margin: 0 20px;
-    padding: 12px 0;
+  grid-area: footer;
+  background: transparent;
+  color: #666;
+  text-align: left;
+  margin: 0 20px;
+  padding: 12px 0;
 
-    .docs-footer-notice {
-        padding: 0;
-        margin: 0;
-        display: block;
-    }
+  .docs-footer-notice {
+    padding: 0;
+    margin: 0;
+    display: block;
+  }
 
-    ul { display: none; }
+  ul {
+    display: none;
+  }
 }
 
 // -----------------------------------------------------------------------------
 // Site content
 // -----------------------------------------------------------------------------
 .site-content {
-    .section-wrapper {
-        border-bottom: solid 1px #eee;
-        &:nth-child(2n+1) {
-          background-color: hsl(210, 17%, 98%);
-        }
+  .section-wrapper {
+    border-bottom: solid 1px #eee;
+    &:nth-child(2n + 1) {
+      background-color: hsl(210, 17%, 98%);
     }
-    section {
+  }
+  section {
+    display: block;
+    position: relative;
+    overflow: hidden;
+    padding: 0 20px;
+    margin: 0 auto;
+    max-width: calc(var(--content-max-width) + 2 * 20px);
+  }
+
+  .banner {
+    height: calc(
+      100vh - var(--home-highlights-height) - var(--site-header-height)
+    );
+    @media (max-height: 639px) {
+      // If the screen is too short (e.g. smartphone in landscape mode)
+      // move the highlights sections (the four tiles) out of the visible
+      // viewport.
+      height: calc(100vh - var(--site-header-height));
+    }
+    min-height: 25vw;
+    display: grid;
+    grid-template-columns: 1fr;
+    grid-template-rows: 1fr 1fr 5fr;
+    h1,
+    h2 {
+      margin: auto;
+      font-family: "Source Sans Pro", sans-serif;
+      text-align: center;
+      color: hsl(0, 0, 35%);
+      span {
+        white-space: nowrap;
+      }
+    }
+    h1 {
+      font-size: 2.5rem;
+      font-size: calc(min(4rem, 8vw, 6vh));
+      font-weight: 400;
+      padding-top: calc(max(1rem, 2vh));
+    }
+    h2 {
+      font-size: 1.25rem;
+      font-size: calc(min(2rem, 6vw, 4vh));
+      font-weight: 200;
+      padding-top: 10px;
+    }
+    .home-img {
+      padding: 1rem 0;
+      overflow: hidden;
+      position: relative;
+      display: flex;
+      img {
+        max-height: 100%;
+        max-width: 100%;
+        margin: auto;
         display: block;
-        position: relative;
-        overflow: hidden;
-        padding: 0 20px;
-        margin: 0 auto;
-        max-width: calc(var(--content-max-width) + 2 * 20px);
+      }
     }
+  }
 
-    .banner {
-        height: calc(100vh - var(--home-highlights-height) - var(--site-header-height));
-        @media (max-height: 639px) {
-            // If the screen is too short (e.g. smartphone in landscape mode)
-            // move the highlights sections (the four tiles) out of the visible
-            // viewport.
-            height: calc(100vh - var(--site-header-height));
+  .home-highlights {
+    &:before {
+      border-top: 1px solid hsl(210, 17%, 90%);
+    }
+    height: var(--home-highlights-height);
+    display: grid;
+    grid-template-columns: repeat(4, 1fr);
+    grid-template-rows: 1fr;
+    background-color: #fff;
+    z-index: 2;
+    @media (max-aspect-ratio: 1/1) {
+      grid-template-columns: repeat(2, 1fr);
+    }
+    > a {
+      color: hsl(0, 0, 20%);
+      font-size: 22px;
+      font-weight: 400;
+      text-align: center;
+      padding: 20px 0;
+      font-family: "Source Sans Pro", sans-serif;
+      text-decoration: none;
+      .icon {
+        background-image: url("/assets/sprite.png");
+        background-repeat: no-repeat;
+        width: 64px;
+        height: 64px;
+        margin: auto;
+        background-size: 256px 128px;
+        filter: grayscale(1);
+        transition: filter ease var(--anim-time);
+      }
+      &:nth-child(1) .icon {
+        background-position: 0 -64px;
+      }
+      &:nth-child(2) .icon {
+        background-position: -64px -64px;
+      }
+      &:nth-child(3) .icon {
+        background-position: -128px -64px;
+      }
+      &:nth-child(4) .icon {
+        background-position: -192px -64px;
+      }
+      &:hover {
+        background-color: hsl(210, 17%, 90%);
+        .icon {
+          filter: grayscale(0);
         }
-        min-height: 25vw;
-        display: grid;
+      }
+    }
+  }
+  .home-section {
+    min-height: calc(min(100vh - var(--site-header-height), 800px));
+    padding: 5% 20px;
+    display: grid;
+    grid-template-rows: 1fr;
+    grid-column-gap: 4vw;
+    > img {
+      grid-area: img;
+      max-width: 100%;
+      max-height: 55vh;
+      margin: auto;
+      margin-top: 40px;
+    }
+    h2,
+    > div {
+      grid-area: content;
+    }
+    h2 {
+      font-family: "Source Sans Pro", sans-serif;
+      font-weight: 600;
+      font-size: 2.5rem;
+      color: #333;
+      text-align: center;
+    }
+    &:nth-child(2n) {
+      grid-template-columns: 5fr 4fr;
+      grid-template-areas: "content img";
+      h2 {
+        padding: 0 0 0 50px;
+        text-align: left;
+      }
+    }
+    &:nth-child(2n + 1) {
+      grid-template-columns: 4fr 5fr;
+      grid-template-areas: "img content";
+      h2 {
+        padding: 0 50px 0 0;
+        text-align: left;
+      }
+    }
+    @media (max-aspect-ratio: 1/1) {
+      padding: 5vh 20px;
+      &:nth-child(n) {
+        grid-template-rows: auto auto;
         grid-template-columns: 1fr;
-        grid-template-rows: 1fr 1fr 5fr;
-        h1,
+        grid-template-areas: "img" "content";
+        grid-row-gap: 30px;
         h2 {
-            margin: auto;
-            font-family: 'Source Sans Pro', sans-serif;
-            text-align: center;
-            color: hsl(0, 0, 35%);
-            span {
-                white-space: nowrap;
-            }
+          padding: 0;
+          text-align: center;
         }
-        h1 {
-            font-size: 2.5rem;
-            font-size: calc(min(4rem, 8vw, 6vh));
-            font-weight: 400;
-            padding-top: calc(max(1rem, 2vh));
-        }
-        h2 {
-            font-size: 1.25rem;
-            font-size: calc(min(2rem, 6vw, 4vh));
-            font-weight: 200;
-            padding-top: 10px;
-        }
-        .home-img {
-            padding: 1rem 0;
-            overflow: hidden;
-            position: relative;
-            display: flex;
-            img {
-                max-height: 100%;
-                max-width: 100%;
-                margin: auto;
-                display: block;
-            }
-        }
+      }
+      > img {
+        padding: 0 10vw;
+      }
     }
-
-
-    .home-highlights {
-        &:before {
-            border-top: 1px solid hsl(210, 17%, 90%);
+    div {
+      grid-area: content;
+      .button {
+        display: inline-block;
+        background: #337ab7;
+        font-weight: 500;
+        color: #fff;
+        border-radius: 6px;
+        font-size: 18px;
+        padding: 10px 16px;
+        transition: background-color ease var(--anim-time);
+        text-decoration: none;
+        &:hover {
+          background: #286090;
         }
-        height: var(--home-highlights-height);
-        display: grid;
-        grid-template-columns: repeat(4, 1fr);
-        grid-template-rows: 1fr;
-        background-color: #fff;
-        z-index: 2;
-        @media (max-aspect-ratio: 1/1) {
-            grid-template-columns: repeat(2, 1fr);
-        }
-        >a {
-            color: hsl(0, 0, 20%);
-            font-size: 22px;
-            font-weight: 400;
-            text-align: center;
-            padding: 20px 0;
-            font-family: 'Source Sans Pro', sans-serif;
-            text-decoration: none;
-            .icon {
-                background-image: url('/assets/sprite.png');
-                background-repeat: no-repeat;
-                width: 64px;
-                height: 64px;
-                margin: auto;
-                background-size: 256px 128px;
-                filter: grayscale(1);
-                transition: filter ease var(--anim-time);
-            }
-            &:nth-child(1) .icon {
-                background-position: 0 -64px;
-            }
-            &:nth-child(2) .icon {
-                background-position: -64px -64px;
-            }
-            &:nth-child(3) .icon {
-                background-position: -128px -64px;
-            }
-            &:nth-child(4) .icon {
-                background-position: -192px -64px;
-            }
-            &:hover {
-                background-color: hsl(210, 17%, 90%);
-                .icon {
-                    filter: grayscale(0);
-                }
-            }
-        }
+      }
     }
-    .home-section {
-        min-height: calc(min(100vh - var(--site-header-height), 800px));
-        padding: 5% 20px;
-        display: grid;
-        grid-template-rows: 1fr;
-        grid-column-gap: 4vw;
-        >img {
-            grid-area: img;
-            max-width: 100%;
-            max-height: 55vh;
-            margin: auto;
-            margin-top: 40px;
-        }
-        h2,
-        >div {
-            grid-area: content;
-        }
-        h2 {
-            font-family: 'Source Sans Pro', sans-serif;
-            font-weight: 600;
-            font-size: 2.5rem;
-            color: #333;
-            text-align: center;
-        }
-        &:nth-child(2n) {
-            grid-template-columns: 5fr 4fr;
-            grid-template-areas: "content img";
-            h2 {
-                padding: 0 0 0 50px;
-                text-align: left;
-            }
-        }
-        &:nth-child(2n+1) {
-            grid-template-columns: 4fr 5fr;
-            grid-template-areas: "img content";
-            h2 {
-                padding: 0 50px 0 0;
-                text-align: left;
-            }
-        }
-        @media (max-aspect-ratio: 1/1) {
-            padding: 5vh 20px;
-            &:nth-child(n) {
-                grid-template-rows: auto auto;
-                grid-template-columns: 1fr;
-                grid-template-areas: "img" "content";
-                grid-row-gap: 30px;
-                h2 {
-                    padding: 0;
-                    text-align: center;
-                }
-            }
-            >img {
-                padding: 0 10vw;
-            }
-        }
-        div {
-            grid-area: content;
-            .button {
-                display: inline-block;
-                background: #337ab7;
-                font-weight: 500;
-                color: #fff;
-                border-radius: 6px;
-                font-size: 18px;
-                padding: 10px 16px;
-                transition: background-color ease var(--anim-time);
-                text-decoration: none;
-                &:hover {
-                    background: #286090;
-                }
-            }
-        }
-        .home-item {
-            display: grid;
-            grid-template-rows: auto auto;
-            grid-template-columns: 50px auto;
-            grid-template-areas: "img title" "img label";
-            grid-column-gap: 20px;
-            padding: 20px 30px;
-            margin: 10px 0;
-            // border: 1px solid #eee;
-            font-family: 'Source Sans Pro', sans-serif;
-            color: #111111;
-            transition: filter var(--anim-ease) var(--anim-time), background-color var(--anim-ease) var(--anim-time), transform var(--anim-ease) var(--anim-time), box-shadow linear var(--anim-time);
-            border-radius: 6px;
-            filter: opacity(0.6);
-            &:hover {
-                background-color: hsla(0, 0, 0, 0.02);
-                filter: opacity(1);
-                transform: scale(1.01);
-            }
-            >img,
-            >i {
-                grid-area: img;
-                margin: auto;
-                font-size: 50px;
-            }
-            >h3 {
-                grid-area: title;
-                font-size: 1.25rem;
-                line-height: 20px;
-                font-weight: 600;
-            }
-            >p {
-                grid-area: label;
-                font-size: 1rem;
-                font-weight: 400;
-                margin: 1em 0;
-            }
-        }
+    .home-item {
+      display: grid;
+      grid-template-rows: auto auto;
+      grid-template-columns: 50px auto;
+      grid-template-areas: "img title" "img label";
+      grid-column-gap: 20px;
+      padding: 20px 30px;
+      margin: 10px 0;
+      // border: 1px solid #eee;
+      font-family: "Source Sans Pro", sans-serif;
+      color: #111111;
+      transition: filter var(--anim-ease) var(--anim-time),
+        background-color var(--anim-ease) var(--anim-time),
+        transform var(--anim-ease) var(--anim-time),
+        box-shadow linear var(--anim-time);
+      border-radius: 6px;
+      filter: opacity(0.6);
+      &:hover {
+        background-color: hsla(0, 0, 0, 0.02);
+        filter: opacity(1);
+        transform: scale(1.01);
+      }
+      > img,
+      > i {
+        grid-area: img;
+        margin: auto;
+        font-size: 50px;
+      }
+      > h3 {
+        grid-area: title;
+        font-size: 1.25rem;
+        line-height: 20px;
+        font-weight: 600;
+      }
+      > p {
+        grid-area: label;
+        font-size: 1rem;
+        font-weight: 400;
+        margin: 1em 0;
+      }
     }
+  }
 }
 
 // -----------------------------------------------------------------------------
 // Docs
 // -----------------------------------------------------------------------------
 .docs {
-    min-height: 100vh;
-    display: grid;
-    --nav-width: 240px;
-    --toc-width: 180px;
+  min-height: 100vh;
+  display: grid;
+  --nav-width: 240px;
+  --toc-width: 180px;
 
-    // 1665px is the clientWidth on a macbook pro. Adjust the layout so that
-    // the max-width of the central .doc fits precisely when the browser is
-    // full-screen on a macbook.
-    --max-doc-width: calc(1665px - var(--toc-width) - var(--nav-width));
+  // 1665px is the clientWidth on a macbook pro. Adjust the layout so that
+  // the max-width of the central .doc fits precisely when the browser is
+  // full-screen on a macbook.
+  --max-doc-width: calc(1665px - var(--toc-width) - var(--nav-width));
 
-    grid-template-columns: var(--nav-width) minmax(auto, var(--max-doc-width)) var(--toc-width);
-    grid-template-rows: 1fr max-content;
-    grid-template-areas: "nav doc toc" "nav footer toc";
+  grid-template-columns: var(--nav-width) minmax(auto, var(--max-doc-width)) var(
+      --toc-width
+    );
+  grid-template-rows: 1fr max-content;
+  grid-template-areas: "nav doc toc" "nav footer toc";
 
-    background-color: hsl(210, 10%, 97%);
-    .nav {
-        grid-area: nav;
-        border-right: 1px solid hsl(210, 30%, 90%);
-        background-color: #fefefe;
-        padding: 20px 0;
-        padding-right: 16px;
+  background-color: hsl(210, 10%, 97%);
+  .nav {
+    grid-area: nav;
+    border-right: 1px solid hsl(210, 30%, 90%);
+    background-color: #fefefe;
+    padding: 20px 0;
+    padding-right: 16px;
 
-        position: sticky;
-        top: var(--site-header-height);
-        height: calc(100vh -  var(--site-header-height));
-        overflow-y: auto;
-        @include minimal-scrollbar;
+    position: sticky;
+    top: var(--site-header-height);
+    height: calc(100vh - var(--site-header-height));
+    overflow-y: auto;
+    @include minimal-scrollbar;
 
-        a {
-            color: inherit;
-            text-decoration: none;
-            line-height: 24px;
-            display: flex;
-            transition: background-color var(--anim-ease) var(--anim-time),
-                        visibility linear var(--anim-time);
-            border-radius: 0 10px 10px 0;
-            -webkit-tap-highlight-color: transparent;
-            &[href] {
-                &:hover {
-                    color: #000;
-                    background-color: #f1f3f4;
-                }
-                &.selected {
-                    background-color: #ecba2a;
-                }
-            }
+    a {
+      color: inherit;
+      text-decoration: none;
+      line-height: 24px;
+      display: flex;
+      transition: background-color var(--anim-ease) var(--anim-time),
+        visibility linear var(--anim-time);
+      border-radius: 0 10px 10px 0;
+      -webkit-tap-highlight-color: transparent;
+      &[href] {
+        &:hover {
+          color: #000;
+          background-color: #f1f3f4;
         }
-
-        ul {
-            list-style: none;
-            margin: 0;
-            padding: 0;
-            overflow: hidden;
-            li {
-                font-size: 1rem;
-                font-weight: 400;
-                font-family: 'Source Sans Pro', sans-serif;
-                color: #4a4a4a;
-                max-width: 100%;
-                margin: 3px 0;
-            }
-            p { margin: 0; }
+        &.selected {
+          background-color: #ecba2a;
         }
-
-        // Applies only to outer-level submenus.
-        >ul {
-            position: static; // Otherwise gets v-centered in the middle.
-            > li {
-                padding-bottom: 10px;
-                margin-bottom: 10px;
-                font-weight: 600;
-                color: #111;
-
-                &:not(:last-child) {
-                    border-bottom: 1px solid #eee;
-                }
-
-                &.compressible {
-                    > p > a::after {
-                        content: 'keyboard_arrow_up';
-                        font-family: 'Material Icons Round';
-                        font-size: 24px;
-                        width: 24px;
-                        transition: transform var(--anim-ease) var(--anim-time);
-                        margin: 0 0 0 auto;
-                        font-weight: 200;
-                        color: #666;
-                    }
-                    > ul {
-                        transition: max-height var(--anim-ease) var(--anim-time),
-                                    opacity var(--anim-ease) var(--anim-time);
-                        opacity: 1;
-                    }
-                    &.compressed {
-                        // The JS will compute and set the maxHeight on each
-                        // element depending on the size of their children.
-                        // !important is needed to override the element-inline
-                        // max-height property set by JS, which is prioritary.
-                        > ul {
-                            max-height: 0 !important;
-                            visibility: hidden;
-                            opacity: 0;
-                        }
-                        > p > a::after {
-                            transform: scaleY(-1);
-                        }
-                    }
-                }  // .compressible
-
-            }
-        }
-
-        li a {
-            padding-left: 16px;
-        }
-        li li a {
-            padding-left: 30px;
-        }
-        li li li a {
-            padding-left: 44px;
-        }
-        .expanded a::after {
-            transform: rotate(180deg);
-        }
+      }
     }
-    .doc {
-        grid-area: doc;
-        background-color: #fff;
-        margin: 20px;
-        padding: 30px 40px;
-        font-family: Roboto, sans-serif;
+
+    ul {
+      list-style: none;
+      margin: 0;
+      padding: 0;
+      overflow: hidden;
+      li {
         font-size: 1rem;
         font-weight: 400;
-        line-height: 24px;
-        -webkit-font-smoothing: antialiased;
+        font-family: "Source Sans Pro", sans-serif;
         color: #4a4a4a;
-        position: relative;
-        box-shadow: 0 1px 2px 0 rgba(60, 64, 67, .1), 0 1px 3px 1px rgba(60, 64, 67, .15);
-        overflow: hidden;
+        max-width: 100%;
+        margin: 3px 0;
+      }
+      p {
+        margin: 0;
+      }
+    }
 
-        a {
-            text-decoration: none;
-            &:link { color: #007b83; }
-            &:visited { color: #8e3317; }
-            &:hover { color: #009da8; }
-            &[href^="http"] {
-                // External link.
-                &:after {
-                    content: 'open_in_new';
-                    font-family: 'Material Icons Round';
-                    color: #666;
-                    text-decoration: none;
-                    margin-left: 2px;
-                    margin-right: 4px;
-                    vertical-align: bottom;
-                }
-            }
+    // Applies only to outer-level submenus.
+    > ul {
+      position: static; // Otherwise gets v-centered in the middle.
+      > li {
+        padding-bottom: 10px;
+        margin-bottom: 10px;
+        font-weight: 600;
+        color: #111;
+
+        &:not(:last-child) {
+          border-bottom: 1px solid #eee;
         }
 
-        h1,
-        h2,
-        h3 {
-            margin: 10px 0;
-            padding: 0;
-            padding-top: 30px;
-        }
-        h1 {
-            font-size: 2.25rem;
-            line-height: 2.25rem;
-            margin: 0;
-            padding: 0;
-            margin-bottom: 1.5rem;
-            font-family: 'Source Sans Pro', sans-serif;
-        }
-        h2 {
-            font-size: 1.5rem;
-            border-bottom: 1px solid #e8eaed;
-            padding-bottom: 6px;
-        }
-        h3 {
-            font-size: 1.25rem;
-        }
-        * {
-            max-width: 100%;
-        }
-
-        img[alt$="screenshot"] {
-            box-shadow: 0 0 10px 2px #eee;
-        }
-
-        code:not(.code-block) {
-            background: hsla(210, 17%, 90%, 0.2);
-            border: 1px solid #E8EAED;
-            border-radius: 6px;
-            padding: 1px 4px;
-        }
-        .code-block {
-            overflow-x: auto;
-            white-space: pre;
-            border-radius: 6px;
-            box-shadow: 1px 1px 6px #999;
-            border-top: 5px solid #8BC34A;
-        }
-        // Hide mermaid graphs until they are rendered, this is to avoid showing
-        // the mermaid source while the renderer generates the SVG.
-        .mermaid {
-            transition: opacity var(--anim-ease) var(--anim-time);
-            &:not(.rendered) {
-                opacity: 0;
-            }
-        }
-        .anchor {
-            margin-left: -29px;
-            padding-right: 5px;
-            text-decoration: none;
-            position: absolute;
-            padding-top: var(--site-header-height);
-            margin-top: calc(-1 * var(--site-header-height));
-            outline: none;
-            opacity: 0;
-            transition: opacity var(--anim-ease) var(--anim-time);
-            &::before {
-                content: 'insert_link';
-                font-family: 'Material Icons Round';
-                color: #333;
-                font-size: 24px;
-            }
-        }
-        *:hover .anchor {
+        &.compressible {
+          > p > a::after {
+            content: "keyboard_arrow_up";
+            font-family: "Material Icons Round";
+            font-size: 24px;
+            width: 24px;
+            transition: transform var(--anim-ease) var(--anim-time);
+            margin: 0 0 0 auto;
+            font-weight: 200;
+            color: #666;
+          }
+          > ul {
+            transition: max-height var(--anim-ease) var(--anim-time),
+              opacity var(--anim-ease) var(--anim-time);
             opacity: 1;
+          }
+          &.compressed {
+            // The JS will compute and set the maxHeight on each
+            // element depending on the size of their children.
+            // !important is needed to override the element-inline
+            // max-height property set by JS, which is prioritary.
+            > ul {
+              max-height: 0 !important;
+              visibility: hidden;
+              opacity: 0;
+            }
+            > p > a::after {
+              transform: scaleY(-1);
+            }
+          }
+        } // .compressible
+      }
+    }
+
+    li a {
+      padding-left: 16px;
+    }
+    li li a {
+      padding-left: 30px;
+    }
+    li li li a {
+      padding-left: 44px;
+    }
+    .expanded a::after {
+      transform: rotate(180deg);
+    }
+  }
+  .doc {
+    grid-area: doc;
+    background-color: #fff;
+    margin: 20px;
+    padding: 30px 40px;
+    font-family: Roboto, sans-serif;
+    font-size: 1rem;
+    font-weight: 400;
+    line-height: 24px;
+    -webkit-font-smoothing: antialiased;
+    color: #4a4a4a;
+    position: relative;
+    box-shadow: 0 1px 2px 0 rgba(60, 64, 67, 0.1),
+      0 1px 3px 1px rgba(60, 64, 67, 0.15);
+    overflow: hidden;
+
+    a {
+      text-decoration: none;
+      &:link {
+        color: #007b83;
+      }
+      &:visited {
+        color: #8e3317;
+      }
+      &:hover {
+        color: #009da8;
+      }
+      &[href^="http"] {
+        // External link.
+        &:after {
+          content: "open_in_new";
+          font-family: "Material Icons Round";
+          color: #666;
+          text-decoration: none;
+          margin-left: 2px;
+          margin-right: 4px;
+          vertical-align: bottom;
         }
+      }
+    }
+
+    h1,
+    h2,
+    h3 {
+      margin: 10px 0;
+      padding: 0;
+      padding-top: 30px;
+    }
+    h1 {
+      font-size: 2.25rem;
+      line-height: 2.25rem;
+      margin: 0;
+      padding: 0;
+      margin-bottom: 1.5rem;
+      font-family: "Source Sans Pro", sans-serif;
+    }
+    h2 {
+      font-size: 1.5rem;
+      border-bottom: 1px solid #e8eaed;
+      padding-bottom: 6px;
+    }
+    h3 {
+      font-size: 1.25rem;
+    }
+    * {
+      max-width: 100%;
+    }
+
+    img[alt$="screenshot"] {
+      box-shadow: 0 0 10px 2px #eee;
+    }
+
+    code:not(.code-block) {
+      background: hsla(210, 17%, 90%, 0.2);
+      border: 1px solid #e8eaed;
+      border-radius: 6px;
+      padding: 1px 4px;
+    }
+    .code-block {
+      overflow-x: auto;
+      white-space: pre;
+      border-radius: 6px;
+      box-shadow: 1px 1px 6px #999;
+      border-top: 5px solid #8bc34a;
+    }
+    // Hide mermaid graphs until they are rendered, this is to avoid showing
+    // the mermaid source while the renderer generates the SVG.
+    .mermaid {
+      transition: opacity var(--anim-ease) var(--anim-time);
+      &:not(.rendered) {
+        opacity: 0;
+      }
+    }
+    .anchor {
+      margin-left: -29px;
+      padding-right: 5px;
+      text-decoration: none;
+      position: absolute;
+      padding-top: var(--site-header-height);
+      margin-top: calc(-1 * var(--site-header-height));
+      outline: none;
+      opacity: 0;
+      transition: opacity var(--anim-ease) var(--anim-time);
+      &::before {
+        content: "insert_link";
+        font-family: "Material Icons Round";
+        color: #333;
+        font-size: 24px;
+      }
+    }
+    *:hover .anchor {
+      opacity: 1;
+    }
+    code {
+      font-family: "Roboto Mono", monospace;
+      font-size: 14px;
+    }
+    table {
+      width: 100%;
+      font-size: 14px;
+      border-spacing: 0;
+      border-collapse: collapse;
+      th,
+      td {
+        padding: 8px;
+        border: 0 solid #dadce0;
+        border-top-width: 1px;
+        border-bottom-width: 1px;
+      }
+      tr {
+        height: 20px;
+      }
+      tr:target {
+        background-color: #ecba2a;
+      }
+      thead {
+        text-align: left;
+        background-color: #e8eaed;
+        color: #202124;
+      }
+    }
+
+    &[data-md-file^="/docs/reference/"] {
+      h1,
+      h2,
+      h3 {
         code {
-            font-family: 'Roboto Mono', monospace;
-            font-size: 14px;
+          margin-left: 20px;
+          color: #666;
         }
-        table {
-            width: 100%;
-            font-size: 14px;
-            border-spacing: 0;
-            border-collapse: collapse;
-            th, td {
-                padding: 8px;
-                border: 0 solid #dadce0;
-                border-top-width: 1px;
-                border-bottom-width: 1px;
-
-            }
-            tr {
-                height: 20px;
-            }
-            tr:target {
-                background-color: #ecba2a;
-            }
-            thead {
-                text-align: left;
-                background-color: #e8eaed;
-                color: #202124;
-            }
+      }
+      table {
+        width: 100%;
+        font-size: 14px;
+        border-spacing: 0;
+        border-collapse: collapse;
+        th,
+        td {
+          padding: 8px;
+          border: 0 solid #dadce0;
+          border-top-width: 1px;
+          border-bottom-width: 1px;
         }
+        tr {
+          height: 20px;
+        }
+        thead {
+          text-align: left;
+          background-color: #e8eaed;
+          color: #202124;
+        }
+        td {
+          &:first-child {
+            background: #f7f7f7;
+          }
 
-        &[data-md-file^="/docs/reference/"] {
-            h1, h2, h3 {
-                code {
-                    margin-left: 20px;
-                    color: #666;
-                }
-            }
-            table {
-                width: 100%;
-                font-size: 14px;
-                border-spacing: 0;
-                border-collapse: collapse;
-                th, td {
-                    padding: 8px;
-                    border: 0 solid #dadce0;
-                    border-top-width: 1px;
-                    border-bottom-width: 1px;
-
-                }
-                tr {
-                    height: 20px;
-                }
-                thead {
-                    text-align: left;
-                    background-color: #e8eaed;
-                    color: #202124;
-                }
-                td {
-                    &:first-child { background: #f7f7f7; }
-
-                    /* Not really 100% but makes sure that the description
+          /* Not really 100% but makes sure that the description
                      * column takes most of the width */
-                    &:last-child { width: 80%; }
-                }
-            }
+          &:last-child {
+            width: 80%;
+          }
         }
-
-        .callout {
-            padding: .5rem .5rem .5rem 2rem;
-            border: none;
-            border-radius: 2px;
-            margin-left: auto;
-            margin-right: auto;
-            width: 90%;
-            border-left: 3px solid transparent;
-            box-shadow: 0 0.2rem 0.5rem rgba(0,0,0,.05), 0 0 0.05rem rgba(0,0,0,.1);
-
-            &:before {
-                font-family: 'Material Icons Round';
-                position: absolute;
-                font-size: 1.5rem;
-                margin-left: -1.75rem;
-                margin-top: -2px;
-            }
-
-            &.note {
-                background-color: #E8F0FE;
-                border-color: #1967D2;
-                color: #1967D2;
-                &:before { content: 'bookmark'; }
-            }
-
-            &.summary {
-                background-color: #E4F7FB;
-                border-color:  #129EAF;
-                color: #129EAF;
-                &:before { content: 'sms'; }
-            }
-
-            &.tip {
-                background-color: #E6F4EA;
-                border-color: #188038;
-                color: #188038;
-                &:before { content: 'star'; }
-            }
-
-            &.todo {
-                background-color: #F1F3F4;
-                border-color: #5F6368;
-                color: #5F6368;
-                &:before { content: 'error'; }
-            }
-
-            &.warning {
-                background-color: #FCE8E6;
-                border-color: #C5221F;
-                color: #C5221F;
-                &:before { content: 'warning'; }
-            }
-        }
+      }
     }
-    .toc {
-        grid-area: toc;
-        padding: 20px 16px 20px 0;
 
-        position: sticky;
-        top: var(--site-header-height);
-        height: calc(100vh - var(--site-header-height));
-        overflow-y: auto;
-        @include minimal-scrollbar;
+    .callout {
+      padding: 0.5rem 0.5rem 0.5rem 2rem;
+      border: none;
+      border-radius: 2px;
+      margin-left: auto;
+      margin-right: auto;
+      width: 90%;
+      border-left: 3px solid transparent;
+      box-shadow: 0 0.2rem 0.5rem rgba(0, 0, 0, 0.05),
+        0 0 0.05rem rgba(0, 0, 0, 0.1);
 
-        font-family: 'Source Sans Pro', sans-serif;
-        word-break: break-word;
-        a {
-            text-decoration: none;
+      &:before {
+        font-family: "Material Icons Round";
+        position: absolute;
+        font-size: 1.5rem;
+        margin-left: -1.75rem;
+        margin-top: -2px;
+      }
+
+      &.note {
+        background-color: #e8f0fe;
+        border-color: #1967d2;
+        color: #1967d2;
+        &:before {
+          content: "bookmark";
         }
-        a,
-        a:visited {
-            color: #333;
+      }
+
+      &.summary {
+        background-color: #e4f7fb;
+        border-color: #129eaf;
+        color: #129eaf;
+        &:before {
+          content: "sms";
         }
-        a.highlighted {
-            font-weight: 500;
-            color: hsl(45, 100%, 40%);
+      }
+
+      &.tip {
+        background-color: #e6f4ea;
+        border-color: #188038;
+        color: #188038;
+        &:before {
+          content: "star";
         }
-        font-size: 0.875rem;
-        ul {
-            list-style: none;
-            margin: 0;
-            padding: 0;
-            li {
-                margin: 5px 0;
-                /* This make it so that a single word gets elided but if there
+      }
+
+      &.todo {
+        background-color: #f1f3f4;
+        border-color: #5f6368;
+        color: #5f6368;
+        &:before {
+          content: "error";
+        }
+      }
+
+      &.warning {
+        background-color: #fce8e6;
+        border-color: #c5221f;
+        color: #c5221f;
+        &:before {
+          content: "warning";
+        }
+      }
+    }
+  }
+  .toc {
+    grid-area: toc;
+    padding: 20px 16px 20px 0;
+
+    position: sticky;
+    top: var(--site-header-height);
+    height: calc(100vh - var(--site-header-height));
+    overflow-y: auto;
+    @include minimal-scrollbar;
+
+    font-family: "Source Sans Pro", sans-serif;
+    word-break: break-word;
+    a {
+      text-decoration: none;
+    }
+    a,
+    a:visited {
+      color: #333;
+    }
+    a.highlighted {
+      font-weight: 500;
+      color: hsl(45, 100%, 40%);
+    }
+    font-size: 0.875rem;
+    ul {
+      list-style: none;
+      margin: 0;
+      padding: 0;
+      li {
+        margin: 5px 0;
+        /* This make it so that a single word gets elided but if there
                  * are multiple words they span across lines.  */
-                overflow: hidden;
-                text-overflow: ellipsis;
-                white-space: break-spaces;
-                word-break: normal;
-            }
-        }
-        >ul {
-            border-left: 4px solid #ecba2a;
-            padding-left: 10px;
-            position: static;  // Otherwise gets v-centered in the middle.
-            top: calc(var(--site-header-height) + 25px);
-        }
+        overflow: hidden;
+        text-overflow: ellipsis;
+        white-space: break-spaces;
+        word-break: normal;
+      }
     }
+    > ul {
+      border-left: 4px solid #ecba2a;
+      padding-left: 10px;
+      position: static; // Otherwise gets v-centered in the middle.
+      top: calc(var(--site-header-height) + 25px);
+    }
+  }
 
-    @media #{$wide} {
-        grid-template-columns: var(--nav-width) auto 0;
-        .toc { display: none; }
+  @media #{$wide} {
+    grid-template-columns: var(--nav-width) auto 0;
+    .toc {
+      display: none;
     }
-    @media #{$mobile} {
+  }
+  @media #{$mobile} {
+    display: block;
+    .doc {
+      margin: 0;
+      padding: 20px;
+    }
+    .nav {
+      // JS will persistently toggle to .after_first_click. This is to
+      // avoid spurious transitions on page load.
+      display: none;
+
+      --nav-width-mobile: calc(min(90vw, 360px));
+      width: var(--nav-width-mobile);
+      position: fixed;
+      z-index: 2;
+      height: 100vh;
+      overflow-y: auto;
+      top: var(--site-header-height);
+      transition: transform var(--anim-ease) var(--anim-time),
+        box-shadow var(--anim-ease) var(--anim-time),
+        visibility ease var(--anim-time);
+      transform: translateX(calc(-1 * var(--nav-width-mobile)));
+      visibility: hidden;
+      > ul {
+        position: static;
+        top: 0;
+      }
+      &.after_first_click {
         display: block;
-        .doc {
-            margin: 0;
-            padding: 20px;
-        }
-        .nav {
-            // JS will persistently toggle to .after_first_click. This is to
-            // avoid spurious transitions on page load.
-            display: none;
-
-            --nav-width-mobile: calc(min(90vw, 360px));
-            width: var(--nav-width-mobile);
-            position: fixed;
-            z-index: 2;
-            height: 100vh;
-            overflow-y: auto;
-            top: var(--site-header-height);
-            transition: transform var(--anim-ease) var(--anim-time),
-                        box-shadow var(--anim-ease) var(--anim-time),
-                        visibility ease var(--anim-time);
-            transform: translateX(calc(-1 * var(--nav-width-mobile)));
-            visibility: hidden;
-            >ul {
-                position: static;
-                top: 0;
-            }
-            &.after_first_click {
-                display: block;
-            }
-            &.expanded {
-                visibility: visible;
-                transform: translateX(0);
-                box-shadow: 0 1px 0 100vw rgba(0,0,0,0.4);
-            }
-        }
+      }
+      &.expanded {
+        visibility: visible;
+        transform: translateX(0);
+        box-shadow: 0 1px 0 100vw rgba(0, 0, 0, 0.4);
+      }
     }
+  }
 }
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/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index 40259f2..ba88077 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -2726,276 +2726,13 @@
   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;
-  ATOM_SOFT_AP_STARTED = 680;
-  ATOM_SOFT_AP_STOPPED = 681;
-  ATOM_WIFI_LOCK_RELEASED = 687;
-  ATOM_WIFI_LOCK_DEACTIVATED = 688;
-  ATOM_WIFI_CONFIG_SAVED = 689;
-  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
-  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
-  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
-  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_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_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;
@@ -3063,27 +2800,68 @@
   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_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_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_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_APEX_INSTALLATION_REQUESTED = 732;
   ATOM_APEX_INSTALLATION_STAGED = 733;
   ATOM_APEX_INSTALLATION_ENDED = 734;
+  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_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_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  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_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  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_BLUETOOTH_RFCOMM_CONNECTION_REPORTED_AT_CLOSE = 982;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED = 979;
+  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_CPU_POLICY = 10199;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -3093,8 +2871,232 @@
   ATOM_CREDENTIAL_MANAGER_GET_REPORTED = 669;
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
-  ATOM_UWB_ACTIVITY_INFO = 10188;
+  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_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  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_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
   ATOM_DND_STATE_CHANGED = 657;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  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_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  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_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  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_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  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_MTE_STATE = 10181;
+  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_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  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_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_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_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  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_SELINUX_AUDIT_LOG = 799;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
+  ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
+  ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
+  ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
+  ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  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_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_UWB_ACTIVITY_INFO = 10188;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  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_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_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_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_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_WIFI_AWARE_NDP_REPORTED = 638;
+  ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
+  ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
+  ATOM_SOFT_AP_STARTED = 680;
+  ATOM_SOFT_AP_STOPPED = 681;
+  ATOM_WIFI_LOCK_RELEASED = 687;
+  ATOM_WIFI_LOCK_DEACTIVATED = 688;
+  ATOM_WIFI_CONFIG_SAVED = 689;
+  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
+  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
+  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
+  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_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
 }
 // End of protos/perfetto/config/statsd/atom_ids.proto
 
diff --git a/protos/perfetto/config/statsd/atom_ids.proto b/protos/perfetto/config/statsd/atom_ids.proto
index 0d0af24..c4da538 100644
--- a/protos/perfetto/config/statsd/atom_ids.proto
+++ b/protos/perfetto/config/statsd/atom_ids.proto
@@ -752,276 +752,13 @@
   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;
-  ATOM_SOFT_AP_STARTED = 680;
-  ATOM_SOFT_AP_STOPPED = 681;
-  ATOM_WIFI_LOCK_RELEASED = 687;
-  ATOM_WIFI_LOCK_DEACTIVATED = 688;
-  ATOM_WIFI_CONFIG_SAVED = 689;
-  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
-  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
-  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
-  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_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_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;
@@ -1089,27 +826,68 @@
   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_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_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_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_APEX_INSTALLATION_REQUESTED = 732;
   ATOM_APEX_INSTALLATION_STAGED = 733;
   ATOM_APEX_INSTALLATION_ENDED = 734;
+  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_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_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  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_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  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_BLUETOOTH_RFCOMM_CONNECTION_REPORTED_AT_CLOSE = 982;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED = 979;
+  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_CPU_POLICY = 10199;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -1119,6 +897,230 @@
   ATOM_CREDENTIAL_MANAGER_GET_REPORTED = 669;
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
-  ATOM_UWB_ACTIVITY_INFO = 10188;
+  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_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  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_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
   ATOM_DND_STATE_CHANGED = 657;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  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_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  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_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  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_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  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_MTE_STATE = 10181;
+  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_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  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_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_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_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  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_SELINUX_AUDIT_LOG = 799;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
+  ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
+  ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
+  ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
+  ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  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_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_UWB_ACTIVITY_INFO = 10188;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  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_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_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_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_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_WIFI_AWARE_NDP_REPORTED = 638;
+  ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
+  ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
+  ATOM_SOFT_AP_STARTED = 680;
+  ATOM_SOFT_AP_STOPPED = 681;
+  ATOM_WIFI_LOCK_RELEASED = 687;
+  ATOM_WIFI_LOCK_DEACTIVATED = 688;
+  ATOM_WIFI_CONFIG_SAVED = 689;
+  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
+  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
+  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
+  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_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
 }
\ No newline at end of file
diff --git a/protos/perfetto/metrics/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index 1de0b47..86c206c 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -303,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;
@@ -355,6 +355,13 @@
     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.
   message TraceThreadSection {
     optional int64 start_timestamp = 1;
@@ -371,6 +378,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // 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.
diff --git a/protos/perfetto/metrics/chrome/BUILD.gn b/protos/perfetto/metrics/chrome/BUILD.gn
index bf47885..c0b0c36 100644
--- a/protos/perfetto/metrics/chrome/BUILD.gn
+++ b/protos/perfetto/metrics/chrome/BUILD.gn
@@ -27,6 +27,7 @@
     "dropped_frames.proto",
     "frame_times.proto",
     "histogram_hashes.proto",
+    "histogram_summaries.proto",
     "long_latency.proto",
     "media_metric.proto",
     "performance_mark_hashes.proto",
diff --git a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
index 596bc7f..ebc0ac5 100644
--- a/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
+++ b/protos/perfetto/metrics/chrome/all_chrome_metrics.proto
@@ -23,6 +23,7 @@
 import "protos/perfetto/metrics/chrome/dropped_frames.proto";
 import "protos/perfetto/metrics/chrome/frame_times.proto";
 import "protos/perfetto/metrics/chrome/histogram_hashes.proto";
+import "protos/perfetto/metrics/chrome/histogram_summaries.proto";
 import "protos/perfetto/metrics/chrome/long_latency.proto";
 import "protos/perfetto/metrics/chrome/media_metric.proto";
 import "protos/perfetto/metrics/chrome/performance_mark_hashes.proto";
@@ -53,4 +54,5 @@
   optional ChromeUnsymbolizedArgs chrome_unsymbolized_args = 1014;
   optional ChromeArgsClassNames chrome_args_class_names = 1015;
   optional ChromeScrollJankV3 chrome_scroll_jank_v3 = 1017;
+  optional ChromeHistogramSummaries chrome_histogram_summaries = 1018;
 }
diff --git a/protos/perfetto/metrics/chrome/histogram_summaries.proto b/protos/perfetto/metrics/chrome/histogram_summaries.proto
new file mode 100644
index 0000000..57dad94
--- /dev/null
+++ b/protos/perfetto/metrics/chrome/histogram_summaries.proto
@@ -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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+message HistogramSummary {
+  // The name of the histogram event
+  optional string name = 1;
+  // The avarage value of the histogram event
+  optional int64 mean = 2;
+  // The number of the histogram event in the trace track
+  optional uint32 count = 3;
+  // The sum of value of the histogram event
+  optional int64 sum = 4;
+  // The maximum value of the histogram event
+  optional int64 max = 5;
+  // The 90 percentile value of the histogram event
+  optional int64 p90 = 6;
+  // The 50 percentile (median) value of the histogram event
+  optional int64 p50 = 7;
+}
+
+// The list of the summary of Chrome Histograms in trace track events.
+// This includes the statistic information of each histograms from Chrome.
+message ChromeHistogramSummaries {
+  repeated HistogramSummary histogram_summary = 1;
+}
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 47da583..9ab7eda 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -78,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;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index a2763d9..e2fc3a3 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -2541,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;
@@ -2593,6 +2593,13 @@
     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.
   message TraceThreadSection {
     optional int64 start_timestamp = 1;
@@ -2609,6 +2616,13 @@
     optional uint32 thread_tid = 6;
   }
 
+  // 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.
@@ -3009,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;
diff --git a/protos/perfetto/trace/android/server/windowmanagerservice.proto b/protos/perfetto/trace/android/server/windowmanagerservice.proto
index dcb4583..2c22522 100644
--- a/protos/perfetto/trace/android/server/windowmanagerservice.proto
+++ b/protos/perfetto/trace/android/server/windowmanagerservice.proto
@@ -454,6 +454,7 @@
   repeated RectProto unrestricted_keep_clear_areas = 46;
   repeated InsetsSourceProto mergedLocalInsetsSources = 47;
   optional int32 requested_visible_types = 48;
+  optional RectProto dim_bounds = 49;
 }
 
 message IdentifierProto {
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 59148eb..8565125 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -28,6 +28,7 @@
   "clk.proto",
   "cma.proto",
   "compaction.proto",
+  "cpm_trace.proto",
   "cpuhp.proto",
   "cros_ec.proto",
   "dcvsh.proto",
@@ -41,6 +42,7 @@
   "fastrpc.proto",
   "fence.proto",
   "filemap.proto",
+  "fs.proto",
   "ftrace.proto",
   "g2d.proto",
   "google_icc_trace.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/fs.proto b/protos/perfetto/trace/ftrace/fs.proto
new file mode 100644
index 0000000..4cc3531
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/fs.proto
@@ -0,0 +1,15 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message DoSysOpenFtraceEvent {
+  optional string filename = 1;
+  optional int32 flags = 2;
+  optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+  optional string filename = 1;
+}
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 7858418..40e7113 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -28,6 +28,7 @@
 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";
@@ -41,6 +42,7 @@
 import "protos/perfetto/trace/ftrace/fastrpc.proto";
 import "protos/perfetto/trace/ftrace/fence.proto";
 import "protos/perfetto/trace/ftrace/filemap.proto";
+import "protos/perfetto/trace/ftrace/fs.proto";
 import "protos/perfetto/trace/ftrace/ftrace.proto";
 import "protos/perfetto/trace/ftrace/g2d.proto";
 import "protos/perfetto/trace/ftrace/google_icc_trace.proto";
@@ -681,5 +683,8 @@
     SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+    DoSysOpenFtraceEvent do_sys_open = 544;
+    OpenExecFtraceEvent open_exec = 545;
   }
 }
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index f77915c..131332d 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -2726,276 +2726,13 @@
   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;
-  ATOM_SOFT_AP_STARTED = 680;
-  ATOM_SOFT_AP_STOPPED = 681;
-  ATOM_WIFI_LOCK_RELEASED = 687;
-  ATOM_WIFI_LOCK_DEACTIVATED = 688;
-  ATOM_WIFI_CONFIG_SAVED = 689;
-  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
-  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
-  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
-  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_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_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;
@@ -3063,27 +2800,68 @@
   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_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_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_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_APEX_INSTALLATION_REQUESTED = 732;
   ATOM_APEX_INSTALLATION_STAGED = 733;
   ATOM_APEX_INSTALLATION_ENDED = 734;
+  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_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_BACKGROUND_DEXOPT_JOB_ENDED = 467;
+  ATOM_PREREBOOT_DEXOPT_JOB_ENDED = 883;
+  ATOM_ODREFRESH_REPORTED = 366;
+  ATOM_ODSIGN_REPORTED = 548;
+  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_CAR_RECENTS_EVENT_REPORTED = 770;
+  ATOM_CAR_CALM_MODE_EVENT_REPORTED = 797;
+  ATOM_CAR_WAKEUP_FROM_SUSPEND_REPORTED = 852;
+  ATOM_PLUGIN_INITIALIZED = 655;
+  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_BLUETOOTH_RFCOMM_CONNECTION_REPORTED_AT_CLOSE = 982;
+  ATOM_CAMERA_FEATURE_COMBINATION_QUERY_EVENT = 900;
+  ATOM_DAILY_KEEPALIVE_INFO_REPORTED = 650;
+  ATOM_NETWORK_REQUEST_STATE_CHANGED = 779;
+  ATOM_TETHERING_ACTIVE_SESSIONS_REPORTED = 925;
+  ATOM_NETWORK_STATS_RECORDER_FILE_OPERATED = 783;
+  ATOM_CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED = 979;
+  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_CPU_POLICY = 10199;
   ATOM_CREDENTIAL_MANAGER_API_CALLED = 585;
   ATOM_CREDENTIAL_MANAGER_INIT_PHASE_REPORTED = 651;
   ATOM_CREDENTIAL_MANAGER_CANDIDATE_PHASE_REPORTED = 652;
@@ -3093,8 +2871,232 @@
   ATOM_CREDENTIAL_MANAGER_GET_REPORTED = 669;
   ATOM_CREDENTIAL_MANAGER_AUTH_CLICK_REPORTED = 670;
   ATOM_CREDENTIAL_MANAGER_APIV2_CALLED = 671;
-  ATOM_UWB_ACTIVITY_INFO = 10188;
+  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_DESKTOP_MODE_UI_CHANGED = 818;
+  ATOM_DESKTOP_MODE_SESSION_TASK_UPDATE = 819;
+  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_DEVICE_POLICY_MANAGEMENT_MODE = 10216;
+  ATOM_DEVICE_POLICY_STATE = 10217;
+  ATOM_DISPLAY_MODE_DIRECTOR_VOTE_CHANGED = 792;
+  ATOM_EXTERNAL_DISPLAY_STATE_CHANGED = 806;
   ATOM_DND_STATE_CHANGED = 657;
+  ATOM_DREAM_SETTING_CHANGED = 705;
+  ATOM_DREAM_SETTING_SNAPSHOT = 10192;
+  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_FEDERATED_COMPUTE_API_CALLED = 712;
+  ATOM_FEDERATED_COMPUTE_TRAINING_EVENT_REPORTED = 771;
+  ATOM_EXAMPLE_ITERATOR_NEXT_LATENCY_REPORTED = 838;
+  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_APPLICATION_GRAMMATICAL_INFLECTION_CHANGED = 584;
+  ATOM_SYSTEM_GRAMMATICAL_INFLECTION_CHANGED = 816;
+  ATOM_HDMI_EARC_STATUS_REPORTED = 701;
+  ATOM_HDMI_SOUNDBAR_MODE_STATUS_REPORTED = 724;
+  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_HEALTH_CONNECT_UI_IMPRESSION = 623;
+  ATOM_HEALTH_CONNECT_UI_INTERACTION = 624;
+  ATOM_HEALTH_CONNECT_APP_OPENED_REPORTED = 625;
+  ATOM_HOTWORD_EGRESS_SIZE_ATOM_REPORTED = 761;
+  ATOM_IKE_SESSION_TERMINATED = 678;
+  ATOM_IKE_LIVENESS_CHECK_SESSION_VALIDATED = 760;
+  ATOM_NEGOTIATED_SECURITY_ASSOCIATION = 821;
+  ATOM_KEYBOARD_CONFIGURED = 682;
+  ATOM_KEYBOARD_SYSTEMS_EVENT_REPORTED = 683;
+  ATOM_INPUTDEVICE_USAGE_REPORTED = 686;
+  ATOM_TOUCHPAD_USAGE = 10191;
+  ATOM_KERNEL_OOM_KILL_OCCURRED = 754;
+  ATOM_EMERGENCY_STATE_CHANGED = 633;
+  ATOM_CHRE_SIGNIFICANT_MOTION_STATE_CHANGED = 868;
+  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_MTE_STATE = 10181;
+  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_ONDEVICEPERSONALIZATION_API_CALLED = 711;
+  ATOM_COMPONENT_STATE_CHANGED_REPORTED = 863;
+  ATOM_PDF_LOAD_REPORTED = 859;
+  ATOM_PDF_API_USAGE_REPORTED = 860;
+  ATOM_PDF_SEARCH_REPORTED = 861;
+  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_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_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_MEDIA_PROVIDER_DATABASE_ROLLBACK_REPORTED = 784;
+  ATOM_BACKUP_SETUP_STATUS_REPORTED = 785;
+  ATOM_RKPD_POOL_STATS = 664;
+  ATOM_RKPD_CLIENT_OPERATION = 665;
+  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_SELINUX_AUDIT_LOG = 799;
+  ATOM_SETTINGS_SPA_REPORTED = 622;
+  ATOM_TEST_EXTENSION_ATOM_REPORTED = 660;
+  ATOM_TEST_RESTRICTED_ATOM_REPORTED = 672;
+  ATOM_STATS_SOCKET_LOSS_REPORTED = 752;
+  ATOM_LOCKSCREEN_SHORTCUT_SELECTED = 611;
+  ATOM_LOCKSCREEN_SHORTCUT_TRIGGERED = 612;
+  ATOM_LAUNCHER_IMPRESSION_EVENT_V2 = 716;
+  ATOM_DISPLAY_SWITCH_LATENCY_TRACKED = 753;
+  ATOM_NOTIFICATION_LISTENER_SERVICE = 829;
+  ATOM_NAV_HANDLE_TOUCH_POINTS = 869;
+  ATOM_EMERGENCY_NUMBER_DIALED = 637;
+  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_THREADNETWORK_TELEMETRY_DATA_REPORTED = 738;
+  ATOM_THREADNETWORK_TOPO_ENTRY_REPEATED = 739;
+  ATOM_THREADNETWORK_DEVICE_INFO_REPORTED = 740;
+  ATOM_BOOT_INTEGRITY_INFO_REPORTED = 775;
+  ATOM_TV_LOW_POWER_STANDBY_POLICY = 679;
+  ATOM_EXTERNAL_TV_INPUT_EVENT = 717;
+  ATOM_UWB_ACTIVITY_INFO = 10188;
+  ATOM_MEDIATOR_UPDATED = 721;
+  ATOM_SYSPROXY_BLUETOOTH_BYTES_TRANSFER = 10196;
+  ATOM_SYSPROXY_CONNECTION_UPDATED = 786;
+  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_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_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_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_WEAR_POWER_MENU_OPENED = 731;
+  ATOM_WEAR_ASSISTANT_OPENED = 755;
+  ATOM_WIFI_AWARE_NDP_REPORTED = 638;
+  ATOM_WIFI_AWARE_ATTACH_REPORTED = 639;
+  ATOM_WIFI_SELF_RECOVERY_TRIGGERED = 661;
+  ATOM_SOFT_AP_STARTED = 680;
+  ATOM_SOFT_AP_STOPPED = 681;
+  ATOM_WIFI_LOCK_RELEASED = 687;
+  ATOM_WIFI_LOCK_DEACTIVATED = 688;
+  ATOM_WIFI_CONFIG_SAVED = 689;
+  ATOM_WIFI_AWARE_RESOURCE_USING_CHANGED = 690;
+  ATOM_WIFI_AWARE_HAL_API_CALLED = 691;
+  ATOM_WIFI_LOCAL_ONLY_REQUEST_RECEIVED = 692;
+  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_WIFI_SETTING_INFO = 10194;
+  ATOM_WIFI_COMPLEX_SETTING_INFO = 10195;
+  ATOM_WIFI_CONFIGURED_NETWORK_INFO = 10198;
 }
 // End of protos/perfetto/config/statsd/atom_ids.proto
 
@@ -7538,6 +7540,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 {
@@ -8795,6 +8807,19 @@
 
 // End of protos/perfetto/trace/ftrace/filemap.proto
 
+// Begin of protos/perfetto/trace/ftrace/fs.proto
+
+message DoSysOpenFtraceEvent {
+  optional string filename = 1;
+  optional int32 flags = 2;
+  optional int32 mode = 3;
+}
+message OpenExecFtraceEvent {
+  optional string filename = 1;
+}
+
+// End of protos/perfetto/trace/ftrace/fs.proto
+
 // Begin of protos/perfetto/trace/ftrace/ftrace.proto
 
 message PrintFtraceEvent {
@@ -11413,6 +11438,9 @@
     SchedWakeupTaskAttrFtraceEvent sched_wakeup_task_attr = 540;
     DevfreqFrequencyFtraceEvent devfreq_frequency = 541;
     KprobeEvent kprobe_event = 542;
+    ParamSetValueCpmFtraceEvent param_set_value_cpm = 543;
+    DoSysOpenFtraceEvent do_sys_open = 544;
+    OpenExecFtraceEvent open_exec = 545;
   }
 }
 
@@ -15294,6 +15322,8 @@
 
     THREAD_MEMORY_INFRA = 50;
     THREAD_SAMPLING_PROFILER = 51;
+
+    THREAD_COMPOSITOR_GPU = 52;
   };
 
   optional ThreadType thread_type = 1;
diff --git a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
index bd82b5a..5e2f119 100644
--- a/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
+++ b/protos/perfetto/trace/track_event/chrome_thread_descriptor.proto
@@ -77,6 +77,8 @@
 
     THREAD_MEMORY_INFRA = 50;
     THREAD_SAMPLING_PROFILER = 51;
+
+    THREAD_COMPOSITOR_GPU = 52;
   };
 
   optional ThreadType thread_type = 1;
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index bfb2a1f..86b4918 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -155,6 +155,8 @@
     StatusResult status = 210;
     // For TPM_REGISTER_SQL_PACKAGE.
     RegisterSqlPackageResult register_sql_package_result = 211;
+    // For TPM_FINALIZE_TRACE_DATA.
+    FinalizeDataResult finalize_data_result = 212;
   }
 
   // Previously: RawQueryArgs for TPM_QUERY_RAW_DEPRECATED
@@ -356,4 +358,8 @@
 
 message RegisterSqlPackageResult {
   optional string error = 1;
-}
\ No newline at end of file
+}
+
+message FinalizeDataResult {
+  optional string error = 1;
+}
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index 7cf29f6..6d92cb6 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -2020,6 +2020,15 @@
 
   // Timestamp in microseconds of the start of the task containing this slice.
   optional uint64 task_start_time_us = 2;
+
+  // t1 - t0, where t1 is the start timestamp of this slice and t0 is the
+  // timestamp of the time when the task containing this slice
+  // was queued.
+  optional uint64 task_queueing_time_us = 3;
+
+  // Timestamp in microseconds of the time when the task containing
+  // this slice was queued.
+  optional uint64 task_queued_time_us = 4;
 }
 
 message ChromeLatencyInfo2 {
diff --git a/python/generators/sql_processing/utils.py b/python/generators/sql_processing/utils.py
index 6d58286..edb6b95 100644
--- a/python/generators/sql_processing/utils.py
+++ b/python/generators/sql_processing/utils.py
@@ -113,7 +113,7 @@
     'chrome/util': ['cr'],
     'intervals': ['interval'],
     'graphs': ['graph'],
-    'slices': ['slice'],
+    'slices': ['slice', 'thread_slice', 'process_slice'],
     'linux': ['cpu', 'memory'],
     'stacks': ['cpu_profiling'],
 }
@@ -121,8 +121,6 @@
 # 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', 'thread_slice_cpu_cycles']
 }
 
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index a2da25e..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/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 976fb9b..40cec35 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 2695450..07b6843 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -115,15 +115,9 @@
     (['/public/lib/colorizer'], '/core/feature_flags'),
 
     # TODO(primiano): Record page-related technical debt.
-    ('/frontend/record*', '/controller/*'),
-    ('/frontend/permalink', '/controller/*'),
-    ('/common/*', '/controller/record_config_types'),
-    ('/controller/index', '/common/recordingV2/target_factories/index'),
-    ('/common/recordingV2/*', '/controller/*'),
-    ('/controller/record_controller*', '*'),
-    ('/controller/adb_*', '*'),
-    ('/chrome_extension/chrome_tracing_controller', '/controller/*'),
-    ('/chrome_extension/chrome_tracing_controller', '/core/trace_config_utils'),
+    ('/plugins/dev.perfetto.RecordTrace/*', '/frontend/globals'),
+    ('/chrome_extension/chrome_tracing_controller',
+     '/plugins/dev.perfetto.RecordTrace/*'),
 
     # TODO(primiano): query-table tech debt.
     (
@@ -139,6 +133,12 @@
         '/frontend/tracks/generic_slice_details_tab',
     ]),
 
+    # TODO(stevegolton): It's too much effort to change all the callsites of
+    # Timestamp and Duration widgets in order to inject trace into them.
+    ('/public/lib/widgets/*', [
+        '/core/app_impl',
+    ]),
+
     # TODO(primiano): controller-related tech debt.
     ('/frontend/index', '/controller/*'),
     ('/controller/*', ['/base/*', '/core/*', '/common/*']),
@@ -150,9 +150,6 @@
     # Bigtrace deps.
     ('/bigtrace/*', ['/base/*', '/widgets/*', '/trace_processor/*']),
 
-    # TODO(primiano): rationalize recordingv2. RecordingV2 is a mess of subdirs.
-    ('/common/recordingV2/*', '/common/recordingV2/*'),
-
     # TODO(primiano): misc tech debt.
     ('/public/lib/extensions', '/frontend/*'),
     ('/bigtrace/index', ['/core/live_reload', '/core/raf_scheduler']),
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index d53b0c3..a34ff08 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -36,7 +36,7 @@
 
 from dataclasses import dataclass
 
-EXPECTED_ANY_COUNT = 52
+EXPECTED_ANY_COUNT = 51
 EXPECTED_RUN_METRIC_COUNT = 4
 
 ROOT_DIR = os.path.dirname(
diff --git a/src/base/time.cc b/src/base/time.cc
index e799542..ad971af 100644
--- a/src/base/time.cc
+++ b/src/base/time.cc
@@ -188,10 +188,15 @@
 std::string GetTimeFmt(const std::string& fmt) {
   time_t raw_time;
   time(&raw_time);
-  struct tm* local_tm;
-  local_tm = localtime(&raw_time);
+  struct tm local_tm;
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
+  PERFETTO_CHECK(localtime_s(&local_tm, &raw_time) == 0);
+#else
+  tzset();
+  PERFETTO_CHECK(localtime_r(&raw_time, &local_tm) != nullptr);
+#endif
   char buf[128];
-  PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), local_tm) > 0);
+  PERFETTO_CHECK(strftime(buf, 80, fmt.c_str(), &local_tm) > 0);
   return buf;
 }
 
diff --git a/src/profiling/memory/client_api_factory_android.cc b/src/profiling/memory/client_api_factory_android.cc
index 9c7b63c..fe5318f 100644
--- a/src/profiling/memory/client_api_factory_android.cc
+++ b/src/profiling/memory/client_api_factory_android.cc
@@ -46,7 +46,7 @@
   std::optional<perfetto::base::UnixSocketRaw> sock =
       Client::ConnectToHeapprofd(perfetto::profiling::kHeapprofdSocketFile);
   if (!sock) {
-    PERFETTO_ELOG("Failed to connect to %s. This is benign on user builds.",
+    PERFETTO_ELOG("Failed to connect to %s.",
                   perfetto::profiling::kHeapprofdSocketFile);
     return nullptr;
   }
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index dc6d6b9..7eeadfb 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -536,3 +536,6 @@
 pixel_mm/pixel_mm_kswapd_done
 sched/sched_wakeup_task_attr
 devfreq/devfreq_frequency
+cpm_trace/param_set_value_cpm
+fs/do_sys_open
+fs/open_exec
diff --git a/src/trace_processor/importers/common/tracks.h b/src/trace_processor/importers/common/tracks.h
index 3245a6b..ce4fbcd 100644
--- a/src/trace_processor/importers/common/tracks.h
+++ b/src/trace_processor/importers/common/tracks.h
@@ -56,6 +56,7 @@
   F(legacy_chrome_global_instants)               \
   F(linux_device_frequency)                      \
   F(linux_rpm)                                   \
+  F(pixel_cpm_trace)                             \
   F(pkvm_hypervisor)                             \
   F(softirq_counter)                             \
   F(thread)                                      \
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 1d31d7d..c9d3970 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, 543> descriptors{{
+std::array<FtraceMessageDescriptor, 546> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -6006,6 +6006,34 @@
             {"type", ProtoSchemaType::kInt32},
         },
     },
+    {
+        "param_set_value_cpm",
+        3,
+        {
+            {},
+            {"body", ProtoSchemaType::kString},
+            {"value", ProtoSchemaType::kUint32},
+            {"timestamp", ProtoSchemaType::kInt64},
+        },
+    },
+    {
+        "do_sys_open",
+        3,
+        {
+            {},
+            {"filename", ProtoSchemaType::kString},
+            {"flags", ProtoSchemaType::kInt32},
+            {"mode", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "open_exec",
+        1,
+        {
+            {},
+            {"filename", ProtoSchemaType::kString},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 51ab54d..59b2446 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -71,6 +71,7 @@
 #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"
@@ -1372,6 +1373,10 @@
         ParseKprobe(ts, pid, fld_bytes);
         break;
       }
+      case FtraceEvent::kParamSetValueCpmFieldNumber: {
+        ParseParamSetValueCpm(fld_bytes);
+        break;
+      }
       default:
         break;
     }
@@ -2263,6 +2268,22 @@
   // family) and thread creation (clone(CLONE_THREAD, ...)).
   static const uint32_t kCloneThread = 0x00010000;  // From kernel's sched.h.
 
+  if (PERFETTO_UNLIKELY(new_tid == 0)) {
+    // In the case of boot-time tracing (kernel is started with tracing
+    // enabled), the ftrace buffer will see /bin/init creating swapper/0 tasks:
+    // event {
+    //  pid: 1
+    //  task_newtask {
+    //    pid: 0
+    //    comm: "swapper/0"
+    //  }
+    // }
+    // Skip these task_newtask events since they are kernel idle tasks.
+    PERFETTO_DCHECK(source_tid == 1);
+    PERFETTO_DCHECK(base::StartsWith(evt.comm().ToStdString(), "swapper"));
+    return;
+  }
+
   // If the process is a fork, start a new process.
   if ((clone_flags & kCloneThread) == 0) {
     // This is a plain-old fork() or equivalent.
@@ -3836,4 +3857,16 @@
                                        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 335649d..6a08f65 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -317,6 +317,7 @@
   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_;
diff --git a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
index 5c6c2e3..50a85da 100644
--- a/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_tokenizer.cc
@@ -43,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"
@@ -279,6 +280,11 @@
     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;
   }
 
   auto timestamp = context_->clock_tracker->ToTraceTime(
@@ -448,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;
@@ -490,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;
@@ -513,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/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index 2da16c9..7c3e9f9 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -147,8 +147,10 @@
         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);
+    // Current is negative when discharging, but we want the power counter to
+    // always be positive, so take the absolute value.
+    auto power = std::abs(static_cast<double>(current * voltage / 1000000000));
+    context_->event_tracker->PushCounter(ts, power, track);
   }
 }
 
diff --git a/src/trace_processor/importers/proto/atoms.descriptor b/src/trace_processor/importers/proto/atoms.descriptor
index b318814..85c6ca8 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_string_lookup.cc b/src/trace_processor/importers/proto/chrome_string_lookup.cc
index f3e1125..f688510 100644
--- a/src/trace_processor/importers/proto/chrome_string_lookup.cc
+++ b/src/trace_processor/importers/proto/chrome_string_lookup.cc
@@ -170,6 +170,7 @@
      "NetworkConfigWatcher"},
     {ChromeThreadDescriptor::THREAD_WASAPI_RENDER, "wasapi_render_thread"},
     {ChromeThreadDescriptor::THREAD_LOADER_LOCK_SAMPLER, "LoaderLockSampler"},
+    {ChromeThreadDescriptor::THREAD_COMPOSITOR_GPU, "CompositorGpuThread"},
 };
 
 }  // namespace
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 2d5b646..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
@@ -46,10 +46,13 @@
 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_tid', tid, 'process_pid', pid,
-    '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 p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -62,10 +65,13 @@
 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_tid', tid, 'process_pid', pid,
-    '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 p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -78,10 +84,13 @@
 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_tid', tid, 'process_pid', pid,
-    '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 p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -95,10 +104,13 @@
 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_tid', tid, 'process_pid', pid,
-    '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 p.pid, ts, dur, thread.tid, thread_name
     FROM launch_threads_by_thread_state l, android_startup_processes p
@@ -111,13 +123,16 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_missing_baseline_profile_for_launch(
   startup_id LONG, pkg_name STRING)
 RETURNS PROTO AS
-  SELECT 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))
+  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 p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM ANDROID_SLICES_FOR_STARTUP_AND_SLICE_NAME($startup_id,
@@ -135,13 +150,16 @@
 
 CREATE OR REPLACE PERFETTO FUNCTION get_run_from_apk(startup_id LONG)
 RETURNS PROTO AS
-  SELECT 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))
+  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 p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups l, android_startup_processes p
@@ -157,13 +175,16 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_unlock_running_during_launch_slice(startup_id LONG,
   pid INT)
 RETURNS PROTO AS
-  SELECT 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))
+  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 tid, slice.ts as slice_ts, slice.dur as slice_dur,
       slice.id as slice_id, slice.name as slice_name
@@ -180,13 +201,16 @@
 
 CREATE OR REPLACE PERFETTO FUNCTION get_gc_activity(startup_id LONG, num_slices INT)
 RETURNS PROTO  AS
-  SELECT 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))
+  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 p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups slice, android_startup_processes p
@@ -204,13 +228,16 @@
 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(
-    'thread_tid', tid,
-    'process_pid', pid,
-    '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 p.pid, tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups l,
@@ -223,11 +250,14 @@
 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(
-    'thread_tid', tid,
-    'process_pid', pid,
-    '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 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
@@ -253,11 +283,14 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_concurrent_to_launch(
   startup_id INT, slice_glob STRING, num_slices INT, pid INT)
 RETURNS PROTO AS
-  SELECT RepeatedField(AndroidStartupMetric_TraceSliceSection(
-    'thread_tid', tid,
-    'process_pid', $pid,
-    '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 thread.tid, s.ts as ts, dur, s.id, s.name FROM slice s
     JOIN thread_track t ON s.track_id = t.id
@@ -275,11 +308,14 @@
 CREATE OR REPLACE PERFETTO FUNCTION get_slices_for_startup_and_slice_name(
   startup_id INT, slice_name STRING, num_slices INT, pid int)
 RETURNS PROTO AS
-  SELECT 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))
+  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 tid, slice_ts, slice_dur, slice_id, slice_name
     FROM android_thread_slices_for_all_startups
diff --git a/src/trace_processor/metrics/sql/chrome/BUILD.gn b/src/trace_processor/metrics/sql/chrome/BUILD.gn
index 30962b8..7d10aeb 100644
--- a/src/trace_processor/metrics/sql/chrome/BUILD.gn
+++ b/src/trace_processor/metrics/sql/chrome/BUILD.gn
@@ -26,6 +26,7 @@
     "chrome_args_class_names.sql",
     "chrome_event_metadata.sql",
     "chrome_histogram_hashes.sql",
+    "chrome_histogram_summaries.sql",
     "chrome_input_to_browser_intervals.sql",
     "chrome_input_to_browser_intervals_base.sql",
     "chrome_input_to_browser_intervals_template.sql",
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
new file mode 100644
index 0000000..7946491
--- /dev/null
+++ b/src/trace_processor/metrics/sql/chrome/chrome_histogram_summaries.sql
@@ -0,0 +1,49 @@
+--
+-- 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 chrome.histograms;
+
+DROP VIEW IF EXISTS HistogramSummaryTable;
+CREATE PERFETTO VIEW HistogramSummaryTable AS
+SELECT
+    hist.name AS histname,
+    CAST(AVG(hist.value) AS INTEGER) AS mean_histval,
+    COUNT(*) AS hist_count,
+    CAST(SUM(hist.value) AS INTEGER) AS sum_histval,
+    CAST(MAX(hist.value) AS INTEGER) AS max_histval,
+    CAST(PERCENTILE(hist.value, 90) AS INTEGER) AS p90_histval,
+    CAST(PERCENTILE(hist.value, 50) AS INTEGER) AS p50_histval
+FROM chrome_histograms hist
+GROUP BY hist.name;
+
+DROP VIEW IF EXISTS chrome_histogram_summaries_output;
+CREATE PERFETTO VIEW chrome_histogram_summaries_output AS
+SELECT ChromeHistogramSummaries(
+    'histogram_summary', (
+        SELECT RepeatedField(
+            HistogramSummary(
+                'name', histname,
+                'mean', mean_histval,
+                'count', hist_count,
+                'sum', sum_histval,
+                'max', max_histval,
+                'p90', p90_histval,
+                'p50', p50_histval
+            )
+        )
+        FROM HistogramSummaryTable
+    )
+);
diff --git a/src/trace_processor/metrics/sql/trace_metadata.sql b/src/trace_processor/metrics/sql/trace_metadata.sql
index 90c112d..1e45573 100644
--- a/src/trace_processor/metrics/sql/trace_metadata.sql
+++ b/src/trace_processor/metrics/sql/trace_metadata.sql
@@ -23,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'
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
index 90697fa..66d2d01 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/critical_blocking_calls.sql
@@ -39,6 +39,8 @@
   OR $name GLOB 'NotificationStackScrollLayout#onMeasure'
   OR $name GLOB 'ExpNotRow#*'
   OR $name GLOB 'GC: Wait For*'
+  OR $name GLOB 'Recomposer:*'
+  OR $name GLOB 'Compose:*'
   OR (
     -- Some top level handler slices
     $depth = 0
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql b/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql
index 496e4ee..29c0b84 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/desktop_mode.sql
@@ -51,12 +51,21 @@
   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_statsd_reset_event AS (
+    SELECT ts FROM atoms
+    WHERE type = 'TASK_INIT_STATSD'
+    UNION
+    SELECT trace_end()),
   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(r.ts,
+        (
+          SELECT MIN(ts) FROM dw_statsd_reset_event
+          WHERE ts > ifnull(a.ts, trace_start())
+        )) - ifnull(a.ts, trace_start()) AS dur,  -- Assume next reset event or 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
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 38c287c..8323366 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/garbage_collection.sql
@@ -106,7 +106,10 @@
 CREATE PERFETTO TABLE _gc_slice_heap
 AS
 SELECT
-  *,
+  gc_ts as ts,
+  gc_dur as dur,
+  upid, gc_id, gc_name, gc_ts, gc_dur, utid, tid, pid,
+  thread_name, process_name, last_value, value,
   CASE
     WHEN gc_name GLOB '*young*' THEN 'young'
     WHEN gc_name GLOB '*NativeAlloc*' THEN 'native_alloc'
diff --git a/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
index b882627..ebc8f8b 100644
--- a/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/chrome/input.sql
@@ -19,7 +19,9 @@
   -- Step name (ChromeLatencyInfo.step).
   step STRING,
   -- Input type.
-  input_type STRING
+  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,
@@ -28,7 +30,8 @@
   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
+  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
@@ -69,7 +72,9 @@
   -- Step name (ChromeLatencyInfo.step).
   step STRING,
   -- Input type.
-  input_type STRING
+  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,
@@ -78,7 +83,8 @@
   dur,
   utid,
   step,
-  chrome_inputs.input_type AS input_type
+  chrome_inputs.input_type AS input_type,
+  task_start_time_ts
 FROM
   chrome_inputs
 LEFT JOIN
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
index d7e9608..3a2ccbe 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/BUILD.gn
@@ -16,6 +16,7 @@
 
 perfetto_sql_source_set("sched") {
   sources = [
+    "latency.sql",
     "runnable.sql",
     "states.sql",
     "thread_executing_span.sql",
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/latency.sql b/src/trace_processor/perfetto_sql/stdlib/sched/latency.sql
new file mode 100644
index 0000000..50dd7e7
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/latency.sql
@@ -0,0 +1,51 @@
+--
+-- 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 sched.runnable;
+
+CREATE PERFETTO VIEW _sched_with_thread_state_join AS
+SELECT
+    thread_state.id AS thread_state_id,
+    sched.id AS sched_id
+FROM sched
+JOIN thread_state USING (utid, ts, dur);
+
+-- Scheduling latency of running thread states.
+-- For each time the thread was running, returns the duration of the runnable
+-- state directly before.
+CREATE PERFETTO TABLE sched_latency_for_running_interval(
+    -- Running state of the thread. Alias of `thread_state.id`.
+    thread_state_id INT,
+    -- Id of a corresponding slice in a `sched` table. Alias of `sched.id`.
+    sched_id INT,
+    -- Thread with running state. Alias of `thread.id`.
+    utid INT,
+    -- Runnable state before thread is "running". Duration of this thread state
+    -- is `latency_dur`. One of `thread_state.id`.
+    runnable_latency_id INT,
+    -- Scheduling latency of thread state. Duration of thread state with
+    -- `runnable_latency_id`.
+    latency_dur INT
+) AS
+SELECT
+    r.id AS thread_state_id,
+    sched_id,
+    utid,
+    prev_runnable_id AS runnable_latency_id,
+    dur AS latency_dur
+FROM sched_previous_runnable_on_thread r
+JOIN thread_state prev_ts ON prev_runnable_id = prev_ts.id
+JOIN _sched_with_thread_state_join ON thread_state_id = r.id
+
diff --git a/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql b/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql
index 38956b1..ffe8b55 100644
--- a/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/sched/runnable.sql
@@ -19,12 +19,12 @@
 -- - previous "Runnable" (or runnable preempted) state.
 -- - previous uninterrupted "Runnable" state with a valid waker thread.
 CREATE PERFETTO TABLE sched_previous_runnable_on_thread(
-    -- `thread_state.id` id.
+    -- Alias of `thread_state.id`.
     id INT,
-    -- Previous runnable `thread_state.id`.
+    -- Previous runnable thread state. Alias of `thread_state.id`.
     prev_runnable_id INT,
-    -- Previous runnable `thread_state.id` with valid waker
-    -- thread.
+    -- Previous runnable thread state with valid waker thread. Alias of
+    -- `thread_state.id`.
     prev_wakeup_runnable_id INT
 ) AS
 WITH running_and_runnable AS (
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/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/wattson/cpu_freq.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
index 94f8fd7..1256f3b 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_freq.sql
@@ -51,4 +51,16 @@
   freq,
   cpu,
   policy
-FROM _cpu_freq;
+FROM _cpu_freq
+UNION ALL
+-- Add empty cpu freq counters for CPUs that are physically present, but did not
+-- have a single freq event register. The time region needs to be defined so
+-- that interval_intersect doesn't remove the undefined time region.
+SELECT
+  trace_start() as ts,
+  trace_dur() as dur,
+  NULL as freq,
+  cpu,
+  NULL as policy
+FROM _dev_cpu_policy_map
+WHERE cpu NOT IN (SELECT cpu FROM first_cpu_freq_slices);
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 5c8c6b5..c2170b7 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/cpu_idle.sql
@@ -91,4 +91,15 @@
   idle
 FROM _cpu_idle
 -- Some durations are 0 post-adjustment and won't work with interval intersect
-WHERE dur > 0;
+WHERE dur > 0
+UNION ALL
+-- Add empty cpu idle counters for CPUs that are physically present, but did not
+-- have a single idle event register. The time region needs to be defined so
+-- that interval_intersect doesn't remove the undefined time region.
+SELECT
+  trace_start() as ts,
+  trace_dur() as dur,
+  cpu,
+  NULL as idle
+FROM _dev_cpu_policy_map
+WHERE cpu NOT IN (SELECT cpu FROM first_cpu_idle_slices);
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 49feef3..d26cdc1 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -212,7 +212,11 @@
     }
     case RpcProto::TPM_FINALIZE_TRACE_DATA: {
       Response resp(tx_seq_id_++, req_type);
-      NotifyEndOfFile();
+      auto* result = resp->set_finalize_data_result();
+      base::Status res = NotifyEndOfFile();
+      if (!res.ok()) {
+        result->set_error(res.message());
+      }
       resp.Send(rpc_response_fn_);
       break;
     }
diff --git a/src/trace_processor/util/proto_to_args_parser.cc b/src/trace_processor/util/proto_to_args_parser.cc
index 42639ea..da2f15c 100644
--- a/src/trace_processor/util/proto_to_args_parser.cc
+++ b/src/trace_processor/util/proto_to_args_parser.cc
@@ -482,7 +482,11 @@
       pool_.descriptors()[*opt_enum_descriptor_idx].FindEnumString(value);
   if (!opt_enum_string) {
     // Fall back to the integer representation of the field.
-    delegate.AddInteger(key_prefix_, value);
+    // 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(
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index b152d02..f93e6e4 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",
        {
@@ -5029,6 +5045,32 @@
        kUnsetFtraceId,
        98,
        kUnsetSize},
+      {"do_sys_open",
+       "fs",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "filename", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "flags", 2, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "mode", 3, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       544,
+       kUnsetSize},
+      {"open_exec",
+       "fs",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "filename", 1, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       545,
+       kUnsetSize},
       {"print",
        "ftrace",
        {
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/fs/do_sys_open/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
new file mode 100644
index 0000000..d6bac92
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/do_sys_open/format
@@ -0,0 +1,13 @@
+name: do_sys_open
+ID: 685
+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[] filename;	offset:8;	size:4;	signed:1;
+	field:int flags;	offset:12;	size:4;	signed:1;
+	field:int mode;	offset:16;	size:4;	signed:1;
+
+print fmt: ""%s" %x %o", __get_str(filename), REC->flags, REC->mode
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
new file mode 100644
index 0000000..9f6fe3d
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/fs/open_exec/format
@@ -0,0 +1,11 @@
+name: open_exec
+ID: 686
+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[] filename;	offset:8;	size:4;	signed:1;
+
+print fmt: ""%s"", __get_str(filename)
diff --git a/test/cmdline_integrationtest.cc b/test/cmdline_integrationtest.cc
index 2febd90..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,56 +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);
+  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);
@@ -601,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);
@@ -666,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");
@@ -676,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);
@@ -772,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);
@@ -831,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");
@@ -841,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,
@@ -853,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);
@@ -910,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");
@@ -931,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,14 +919,9 @@
 }
 
 TEST_F(PerfettoCmdlineTest, CloneByName) {
-  constexpr size_t kMessageCount = 2;
-  protos::gen::TraceConfig trace_config;
-  trace_config.add_buffers()->set_size_kb(1024);
+  protos::gen::TraceConfig trace_config =
+      CreateTraceConfigForTest(kTestMessageCount, kTestMessageSize);
   trace_config.set_unique_session_name("my_unique_session_name");
-  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(2);
 
   // We have to construct all the processes we want to fork before we start the
   // service with |StartServiceIfRequired()|. this is because it is unsafe
@@ -1026,26 +979,18 @@
   EXPECT_EQ(0, perfetto_proc_clone_2.Run(&stderr_)) << "stderr: " << stderr_;
   EXPECT_FALSE(base::FileExists(path_cloned_2));
 
-  std::string cloned_trace_str;
-  base::ReadFile(path_cloned, &cloned_trace_str);
   protos::gen::Trace cloned_trace;
-  ASSERT_TRUE(cloned_trace.ParseFromString(cloned_trace_str));
-  ssize_t cloned_num_test_packets = std::count_if(
-      cloned_trace.packet().begin(), cloned_trace.packet().end(),
-      [](const protos::gen::TracePacket& tp) { return tp.has_for_testing(); });
-  EXPECT_EQ(cloned_num_test_packets, static_cast<ssize_t>(kMessageCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path_cloned, cloned_trace));
+  ExpectTraceContainsTestMessages(cloned_trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(cloned_trace, kTestMessageSize);
 
   perfetto_proc.SendSigterm();
   background_trace.join();
 
-  std::string trace_str;
-  base::ReadFile(path, &trace_str);
   protos::gen::Trace trace;
-  ASSERT_TRUE(trace.ParseFromString(trace_str));
-  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>(kMessageCount));
+  ASSERT_TRUE(ParseNotEmptyTraceFromFile(path, trace));
+  ExpectTraceContainsTestMessages(trace, kTestMessageCount);
+  ExpectTraceContainsTestMessagesWithSize(trace, kTestMessageSize);
 }
 
 // Regression test for b/279753347: --save-for-bugreport would create an empty
@@ -1185,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,
@@ -1211,8 +1154,9 @@
   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);
@@ -1263,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/data/android_desktop_mode/task_update_reset_events.pb.sha256 b/test/data/android_desktop_mode/task_update_reset_events.pb.sha256
new file mode 100644
index 0000000..f047a6a
--- /dev/null
+++ b/test/data/android_desktop_mode/task_update_reset_events.pb.sha256
@@ -0,0 +1 @@
+1aca7da3154a8893d214444d10559d88e689d090e6d39206e30959aa2a2cd306
\ No newline at end of file
diff --git a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256 b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
index b97e598..66f89fe 100644
--- a/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
+++ b/test/data/ui-screenshots/ftrace_tracks_and_tab.test.ts/ftrace-tracks/ftrace-events.png.sha256
@@ -1 +1 @@
-ef239fe1e10e3b830f7adf9b5b8f96a52c69a50e6d879cfb5a873cd26caab769
\ No newline at end of file
+ae6623f3d00536a815ad76c40c5933845a436e6e8acee6349d0e925c06d0b6b0
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/metrics/android/android_boot.out b/test/trace_processor/diff_tests/metrics/android/android_boot.out
index 3d69fdc..50805cf 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_boot.out
+++ b/test/trace_processor/diff_tests/metrics/android/android_boot.out
@@ -33,28 +33,28 @@
 num_of_processes_with_gc: 4
 num_of_threads_with_gc: 4
 avg_gc_duration: 260516077.75
-avg_running_gc_duration: 3902628.5
+avg_running_gc_duration: 20215729.0
 full_gc_count: 4
 collector_transition_gc_count: 0
 young_gc_count: 0
 native_alloc_gc_count: 0
 explicit_gc_count: 0
 alloc_gc_count: 0
-mb_per_ms_of_gc: 0.8829305684617432
+mb_per_ms_of_gc: 0.17044896080670652
 }
 post_boot_gc_aggregation {
 total_gc_count: 4
 num_of_processes_with_gc: 4
 num_of_threads_with_gc: 4
 avg_gc_duration: 260516077.75
-avg_running_gc_duration: 3902628.5
+avg_running_gc_duration: 20215729.0
 full_gc_count: 4
 collector_transition_gc_count: 0
 young_gc_count: 0
 native_alloc_gc_count: 0
 explicit_gc_count: 0
 alloc_gc_count: 0
-mb_per_ms_of_gc: 0.8829305684617432
+mb_per_ms_of_gc: 0.17044896080670652
 }
 post_boot_oom_adjuster_transition_counts_global {
 src_bucket: "cached_app"
diff --git a/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
index cb0d712..dcaed19 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
+++ b/test/trace_processor/diff_tests/metrics/android/android_boot_unagg.out
@@ -223,11 +223,11 @@
       reclaimed_mb: 3.1430000000000002
       min_heap_mb: 2.754000
       max_heap_mb: 5.897000
-      mb_per_ms_of_running_gc: 0.3262420579054222
+      mb_per_ms_of_running_gc: 0.07341516044319213
       mb_per_ms_of_wall_gc: 0.004718018639325977
       gc_dur: 666169475
-      gc_running_dur: 9633951
-      gc_runnable_dur: 44371461
+      gc_running_dur: 42811321
+      gc_runnable_dur: 623358154
       gc_unint_io_dur: 0
       gc_unint_non_io_dur: 0
       gc_int_dur: 0
@@ -244,14 +244,14 @@
       reclaimed_mb: 10.640000
       min_heap_mb: 6.861000
       max_heap_mb: 17.501000
-      mb_per_ms_of_running_gc: 93.68918788028213
+      mb_per_ms_of_running_gc: 0.426417596041947
       mb_per_ms_of_wall_gc: 0.18073523433053545
       gc_dur: 58870646
-      gc_running_dur: 113567
-      gc_runnable_dur: 0
+      gc_running_dur: 24952066
+      gc_runnable_dur: 31236573
       gc_unint_io_dur: 0
       gc_unint_non_io_dur: 0
-      gc_int_dur: 0
+      gc_int_dur: 2682007
       gc_ts: 39849615741
       tid: 2534
       pid: 2523
@@ -263,8 +263,8 @@
       gc_type: "full"
       is_mark_compact: 1
       gc_dur: 168393585
-      gc_running_dur: 1714233
-      gc_runnable_dur: 28482219
+      gc_running_dur: 8950766
+      gc_runnable_dur: 69270710
       gc_unint_io_dur: 0
       gc_unint_non_io_dur: 0
       gc_int_dur: -1
diff --git a/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out b/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out
index 1341536..b28579b 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out
+++ b/test/trace_processor/diff_tests/metrics/android/android_garbage_collection_unagg.out
@@ -7,11 +7,11 @@
     reclaimed_mb: 3.1430000000000002
     min_heap_mb: 2.754000
     max_heap_mb: 5.897000
-    mb_per_ms_of_running_gc: 0.3262420579054222
+    mb_per_ms_of_running_gc: 0.07341516044319213
     mb_per_ms_of_wall_gc: 0.004718018639325977
     gc_dur: 666169475
-    gc_running_dur: 9633951
-    gc_runnable_dur: 44371461
+    gc_running_dur: 42811321
+    gc_runnable_dur: 623358154
     gc_unint_io_dur: 0
     gc_unint_non_io_dur: 0
     gc_int_dur: 0
@@ -28,14 +28,14 @@
     reclaimed_mb: 10.640000
     min_heap_mb: 6.861000
     max_heap_mb: 17.501000
-    mb_per_ms_of_running_gc: 93.68918788028213
+    mb_per_ms_of_running_gc: 0.426417596041947
     mb_per_ms_of_wall_gc: 0.18073523433053545
     gc_dur: 58870646
-    gc_running_dur: 113567
-    gc_runnable_dur: 0
+    gc_running_dur: 24952066
+    gc_runnable_dur: 31236573
     gc_unint_io_dur: 0
     gc_unint_non_io_dur: 0
-    gc_int_dur: 0
+    gc_int_dur: 2682007
     gc_ts: 39849615741
     tid: 2534
     pid: 2523
@@ -47,8 +47,8 @@
     gc_type: "full"
     is_mark_compact: 1
     gc_dur: 168393585
-    gc_running_dur: 1714233
-    gc_runnable_dur: 28482219
+    gc_running_dur: 8950766
+    gc_runnable_dur: 69270710
     gc_unint_io_dur: 0
     gc_unint_non_io_dur: 0
     gc_int_dur: -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 42368b3..ec2b075 100644
--- a/test/trace_processor/diff_tests/metrics/startup/android_startup.out
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
@@ -78,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     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 2d6070c..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
@@ -148,12 +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: 20
-        slice_name: "CollectorTransition mark sweep GC"
-        process_pid: 3
-        thread_tid: 5
       }
     }
     slow_start_reason_with_details {
@@ -171,20 +175,24 @@
       }
       launch_dur: 999999900
       trace_slice_sections {
-        start_timestamp: 170
-        end_timestamp: 500000000
-        slice_id: 9
-        slice_name: "OpenDexFilesFromOat(something else)"
-        process_pid: 3
-        thread_tid: 3
-      }
-      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)"
-        process_pid: 3
-        thread_tid: 3
+        end_timestamp: 500000000
       }
     }
     slow_start_reason_with_details {
@@ -200,12 +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: 21
-        slice_name: "binder transaction"
-        process_pid: 3
-        thread_tid: 3
       }
     }
   }
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 a98505f..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
@@ -107,12 +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"
-        process_pid: 3
-        thread_tid: 5
       }
     }
     slow_start_reason_with_details {
@@ -129,25 +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_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
-      }
-      trace_thread_sections {
-        start_timestamp: 170000000000
-        end_timestamp: 175000000000
-        thread_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
-      }
-      trace_thread_sections {
-        start_timestamp: 185000000000
         end_timestamp: 190000000000
-        thread_name: "Jit thread pool"
-        thread_tid: 4
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -164,28 +172,32 @@
       }
       launch_dur: 999999900000000000
       trace_slice_sections {
-        start_timestamp: 200000000000
-        end_timestamp: 210000000000
-        slice_id: 84
-        slice_name: "JIT compiling nothing"
-        process_pid: 3
-        thread_tid: 3
-      }
-      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"
-        process_pid: 3
-        thread_tid: 4
-      }
-      trace_slice_sections {
-        start_timestamp: 101000000000
-        end_timestamp: 102000000000
-        slice_id: 10
-        slice_name: "JIT compiling something"
-        process_pid: 3
-        thread_tid: 4
+        end_timestamp: 210000000000
       }
     }
   }
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 2405d7e..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
@@ -128,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -149,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -171,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -193,20 +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"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
-        start_timestamp: 191000000000
         end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -223,12 +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"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -245,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     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 3dbb2aa..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
@@ -127,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -149,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -171,20 +179,24 @@
       }
       launch_dur: 108000000000
       trace_slice_sections {
-        start_timestamp: 190000000000
-        end_timestamp: 192000000000
-        slice_id: 8
-        slice_name: "inflate"
-        process_pid: 3
-        thread_tid: 3
-      }
-      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"
-        process_pid: 3
-        thread_tid: 3
+        end_timestamp: 192000000000
       }
     }
     slow_start_reason_with_details {
@@ -201,12 +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"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -223,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     startup_type: "cold"
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 4913852..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
@@ -46,25 +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"
-        thread_tid: 1
-      }
-      trace_slice_sections {
-        start_timestamp: 106
-        end_timestamp: 107
-        slice_id: 8
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-        thread_tid: 1
-      }
-      trace_slice_sections {
-        start_timestamp: 107
         end_timestamp: 108
-        slice_id: 10
-        slice_name: "Broadcast dispatched from android (2005:system/1000) x"
-        thread_tid: 1
       }
     }
     slow_start_reason_with_details {
@@ -81,25 +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"
-        thread_tid: 2
-      }
-      trace_slice_sections {
-        start_timestamp: 101
-        end_timestamp: 102
-        slice_id: 2
-        slice_name: "broadcastReceiveReg: x"
-        thread_tid: 2
-      }
-      trace_slice_sections {
-        start_timestamp: 102
         end_timestamp: 103
-        slice_id: 3
-        slice_name: "broadcastReceiveReg: x"
-        thread_tid: 2
       }
     }
   }
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 dabbb5f..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
@@ -82,12 +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"
-        process_pid: 3
-        thread_tid: 3
       }
     }
     slow_start_reason_with_details {
@@ -105,28 +109,32 @@
       }
       launch_dur: 100000000000
       trace_slice_sections {
-        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
-      }
-      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: 130000000000
-        slice_id: 4
-        slice_name: "Lock contention on thread list lock (owner tid: 2)"
-        process_pid: 3
-        thread_tid: 3
-      }
-      trace_slice_sections {
-        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
       }
     }
     slow_start_reason_with_details {
@@ -144,20 +152,24 @@
      }
      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: 157000000000
-       slice_id: 5
-       slice_name: "Lock contention on a monitor lock (owner tid: 2)"
-       process_pid: 3
-       thread_tid: 3
-     }
-     trace_slice_sections {
-       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
      }
     }
     startup_type: "cold"
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 ded275e..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
@@ -77,11 +77,15 @@
       }
       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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
   }
@@ -164,11 +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_name: "com.google.android.calendar"
-        thread_tid: 4
-        process_pid: 4
       }
     }
   }
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 942037e..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
@@ -81,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -102,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -123,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
     slow_start_reason_with_details {
@@ -144,11 +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_name: "com.google.android.calendar"
-        thread_tid: 3
-        process_pid: 3
       }
     }
   }
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 3f52432..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
@@ -44,11 +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"
-        thread_tid: 2
       }
     }
   }
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
index 54707c1..8af739d 100644
--- a/test/trace_processor/diff_tests/parser/parsing/tests.py
+++ b/test/trace_processor/diff_tests/parser/parsing/tests.py
@@ -1570,3 +1570,76 @@
         5230422153284,0,1306,"[NULL]"
         5230425693562,0,10,1
         """))
+
+  # Kernel idle tasks created by /sbin/init should be filtered.
+  def test_task_newtask_swapper_by_init(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          first_packet_on_sequence: true
+          ftrace_events {
+            cpu: 1
+            event {
+              timestamp: 1000000
+              pid: 0
+              task_newtask {
+                pid: 1
+                comm: "swapper/0"
+                clone_flags: 8389376
+                oom_score_adj: 0
+              }
+            }
+            event {
+              timestamp: 1000000
+              pid: 0
+              task_newtask {
+                pid: 2
+                comm: "swapper/0"
+                clone_flags: 8390400
+                oom_score_adj: 0
+              }
+            }
+            event {
+              timestamp: 17000000
+              pid: 1
+              task_newtask {
+                pid: 0
+                comm: "swapper/0"
+                clone_flags: 256
+                oom_score_adj: 0
+              }
+            }
+            event {
+              timestamp: 17000000
+              pid: 1
+              task_newtask {
+                pid: 0
+                comm: "swapper/0"
+                clone_flags: 256
+                oom_score_adj: 0
+              }
+            }
+            event {
+              timestamp: 17000000
+              pid: 1
+              task_newtask {
+                pid: 0
+                comm: "swapper/0"
+                clone_flags: 256
+                oom_score_adj: 0
+              }
+            }
+          }
+          trusted_uid: 9999
+          trusted_packet_sequence_id: 2
+          trusted_pid: 521
+          previous_packet_dropped: true
+        }
+        """),
+        query="""
+        SELECT utid, tid, name from thread where tid = 0
+        """,
+        out=Csv("""
+        "utid","tid","name"
+        0,0,"swapper"
+        """))
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 046d1c4..ccdae4f 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
@@ -147,7 +147,7 @@
         packet {
           timestamp: 4000000
           battery {
-            current_ua: 510000
+            current_ua: -510000
             voltage_uv: 12000000
           }
         }
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
index 6c16720..0570da9 100644
--- a/test/trace_processor/diff_tests/stdlib/android/desktop_mode_tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/desktop_mode_tests.py
@@ -108,3 +108,37 @@
         8966481546961,"[NULL]",8966481546961,3595287192,1000028,1110329
         """))
 
+  def test_android_desktop_mode_windows_statsd_events_reset_events(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_desktop_mode/task_update_reset_events.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"
+        84164708379314,84188981730278,84164708379314,24273350964,1000054,1010210
+        84182861720998,84184961957530,84182861720998,2100236532,1000056,1010197
+        84190656246474,"[NULL]",84190656246474,663853800,1000054,1010210
+        84190656724094,"[NULL]",84190656724094,663376180,1000058,1010222
+        84199757126076,84223637163807,84199757126076,23880037731,1000054,1010210
+        84199757350156,84223637751006,84199757350156,23880400850,1000058,1010222
+        84204112441752,84208226333681,84204112441752,4113891929,1000062,1010226
+        84226052369131,84241418229490,84226052369131,15365860359,1000054,1010210
+        84226054551300,84241419846189,84226054551300,15365294889,1000058,1010222
+        84248341935751,"[NULL]",84248341935751,407771770,1000054,1010210
+        84248342227662,"[NULL]",84248342227662,407479859,1000058,1010222
+        84253290279911,84292320102806,84253290279911,39029822895,1000054,1010210
+        84253290608646,84292320892072,84253290608646,39030283426,1000058,1010222
+        84294338295719,84856791435520,84294338295719,562453139801,1000054,1010210
+        84294339318912,84859506938084,84294339318912,565167619172,1000058,1010222
+        84304813959094,84858368194309,84304813959094,553554235215,1000069,1010211
+        84335762434858,84852636055309,84335762434858,516873620451,1000070,1010297
+        84357340686822,84378879321354,84357340686822,21538634532,1000071,1010225
+        84361592237320,84363638663184,84361592237320,2046425864,1000072,1010317
+        84370549790954,84853627568534,84370549790954,483077777580,1000074,1010317
+        84575862070921,84643498791291,84575862070921,67636720370,1000075,1001000
+        84619864755554,84638100287708,84619864755554,18235532154,1000076,1010235
+        84782344478655,84804145589506,84782344478655,21801110851,1000077,1010238
+        """))
+
diff --git a/test/trace_processor/diff_tests/stdlib/android/tests.py b/test/trace_processor/diff_tests/stdlib/android/tests.py
index 1b204fc..0baf4b0 100644
--- a/test/trace_processor/diff_tests/stdlib/android/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/android/tests.py
@@ -1115,10 +1115,10 @@
       """,
         out=Csv("""
         "tid","pid","thread_name","process_name","gc_type","is_mark_compact","reclaimed_mb","min_heap_mb","gc_ts","gc_dur","gc_running_dur","gc_runnable_dur","gc_unint_io_dur","gc_unint_non_io_dur","gc_int_dur"
-        2013,2003,"HeapTaskDaemon","android.process.media","collector_transition",0,0.670000,2.153000,2.823000,326468170,80326441,11087787,0,0,10056086
-        3494,3487,"HeapTaskDaemon","com.android.providers.media.module","young",0,"[NULL]","[NULL]","[NULL]",213263593,55205035,10429437,0,0,1208604
-        3494,3487,"HeapTaskDaemon","com.android.providers.media.module","collector_transition",0,1.248000,2.201000,3.449000,169735717,65828710,20965673,0,0,0
-        3556,3549,"HeapTaskDaemon","com.android.externalstorage","collector_transition",0,0.450000,2.038000,2.488000,166379142,52906367,7881722,0,0,0
+        2013,2003,"HeapTaskDaemon","android.process.media","collector_transition",0,0.670000,2.153000,2.823000,326468170,138781185,177630899,0,0,10056086
+        3494,3487,"HeapTaskDaemon","com.android.providers.media.module","young",0,"[NULL]","[NULL]","[NULL]",213263593,164870500,29632860,0,0,12881595
+        3494,3487,"HeapTaskDaemon","com.android.providers.media.module","collector_transition",0,1.248000,2.201000,3.449000,169735717,135385084,24036227,0,1301,10313105
+        3556,3549,"HeapTaskDaemon","com.android.externalstorage","collector_transition",0,0.450000,2.038000,2.488000,166379142,123781761,32547509,0,0,10049872
         """))
 
   def test_input_events(self):
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 bb42920..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
@@ -393,6 +393,36 @@
         -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'),
diff --git a/test/trace_processor/diff_tests/stdlib/sched/tests.py b/test/trace_processor/diff_tests/stdlib/sched/tests.py
index 1de62b5..a327125 100644
--- a/test/trace_processor/diff_tests/stdlib/sched/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/sched/tests.py
@@ -187,3 +187,33 @@
         538177,537492,537492
         538175,538174,524613
         """))
+
+  def test_sched_latency(self):
+    return DiffTestBlueprint(
+        trace=DataPath('android_boot.pftrace'),
+        query="""
+        INCLUDE PERFETTO MODULE sched.latency;
+
+        SELECT 
+          thread_state_id, 
+          sched_id, 
+          utid, 
+          runnable_latency_id, 
+          latency_dur
+        FROM sched_latency_for_running_interval
+        ORDER BY thread_state_id DESC
+        LIMIT 10;
+        """,
+        out=Csv("""
+        "thread_state_id","sched_id","utid","runnable_latency_id","latency_dur"
+        538199,269427,2,538191,91919
+        538197,269425,2,538191,91919
+        538195,269423,2,538191,91919
+        538190,269422,1330,538136,1437215
+        538188,269420,2,538088,826823
+        538184,269419,91,538176,131388
+        538181,269418,319,538178,4883
+        538179,269417,1022,524619,469849
+        538177,269416,319,537492,670736
+        538175,269415,91,538174,12532
+        """))
diff --git a/test/trace_processor/diff_tests/stdlib/slices/tests.py b/test/trace_processor/diff_tests/stdlib/slices/tests.py
index 625659e..88b430f 100644
--- a/test/trace_processor/diff_tests/stdlib/slices/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/slices/tests.py
@@ -177,3 +177,27 @@
         8,46926
         9,17865
         """))
+
+  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/tools/gen_android_bp b/tools/gen_android_bp
index f5386dc..71340c9 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -302,6 +302,13 @@
     ],
     'libperfetto': [('export_include_dirs', {'include', buildflags_dir}),],
     'perfetto': [('required', {'perfetto_persistent_cfg.pbtxt'}),],
+    'trace_redactor': [
+        ('min_sdk_version', '35'),
+        ('apex_available', {
+            '//apex_available:platform',
+            'com.android.profiling'
+        }),
+    ],
 }
 
 
diff --git a/tools/gen_bazel b/tools/gen_bazel
index a30f5ca..a5776ba 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -83,12 +83,12 @@
     '//src/trace_processor:trace_processor_shell',
     '//src/trace_processor:trace_processor',
     '//src/traceconv:traceconv',
-    '//src/traceconv:libpprofbuilder',
 ]
 
 # These targets will be exported with visibility only to our allowlist.
 allowlist_public_targets = [
     '//src/shared_lib:libperfetto_c',
+    '//src/traceconv:libpprofbuilder',
 ]
 
 # These targets are required by internal build rules but don't need to be
diff --git a/ui/release/channels.json b/ui/release/channels.json
index c2c76c2..5f8615b 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
     },
     {
       "name": "canary",
-      "rev": "4817ff8af4289f905c36a8a1ba6a583afc569af4"
+      "rev": "2db61efa59d1e2eecb6975854c14b2a122fbfa8a"
     },
     {
       "name": "autopush",
diff --git a/ui/src/common/gcs_uploader.ts b/ui/src/base/gcs_uploader.ts
similarity index 93%
rename from ui/src/common/gcs_uploader.ts
rename to ui/src/base/gcs_uploader.ts
index d2012ee..b2f2bd5 100644
--- a/ui/src/common/gcs_uploader.ts
+++ b/ui/src/base/gcs_uploader.ts
@@ -12,9 +12,8 @@
 // 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';
+import {defer} from './deferred';
+import {Time} from './time';
 
 export const BUCKET_NAME = 'perfetto-ui-data';
 export const MIME_JSON = 'application/json; charset=utf-8';
@@ -184,13 +183,16 @@
  * @returns A hex-encoded string containing the hash of the file.
  */
 async function hashFileStreaming(file: Blob): Promise<string> {
-  const fileStream = new TraceFileStream(file);
+  const CHUNK_SIZE = 32 * 1024 * 1024; // 32MB
+  const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
   let chunkDigests = '';
-  for (;;) {
-    const chunk = await fileStream.readChunk();
-    const digest = await crypto.subtle.digest('SHA-1', chunk.data);
+
+  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);
-    if (chunk.eof) break;
   }
   return sha1(chunkDigests);
 }
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index 17e3a00..85c194f 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -88,7 +88,9 @@
   | 'ArrowLeft'
   | 'ArrowRight'
   | '['
-  | ']';
+  | ']'
+  | ','
+  | '.';
 export type Key = Alphabet | Number | Special;
 export type Modifier =
   | ''
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/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/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/recordingV2/recording_error_handling.ts b/ui/src/common/recordingV2/recording_error_handling.ts
deleted file mode 100644
index ffec467..0000000
--- a/ui/src/common/recordingV2/recording_error_handling.ts
+++ /dev/null
@@ -1,141 +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/track_helper.ts b/ui/src/common/track_helper.ts
index 3087228..e9ef6fb 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -95,6 +95,6 @@
     const {start, end} = this.latestTimespan;
     const resolution = this.latestResolution;
     this.data_ = await this.doFetch(start, end, resolution);
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 }
diff --git a/ui/src/core/app_impl.ts b/ui/src/core/app_impl.ts
index 24eceaf..c1045e2 100644
--- a/ui/src/core/app_impl.ts
+++ b/ui/src/core/app_impl.ts
@@ -32,7 +32,7 @@
 import {createProxy, getOrCreate} from '../base/utils';
 import {PageManagerImpl} from './page_manager';
 import {PageHandler} from '../public/page';
-import {setPerfHooks} from './perf';
+import {PerfManager} from './perf_manager';
 import {ServiceWorkerController} from '../frontend/service_worker_controller';
 import {FeatureFlagManager, FlagSettings} from '../public/feature_flag';
 import {featureFlags} from './feature_flags';
@@ -59,6 +59,7 @@
   readonly pageMgr = new PageManagerImpl();
   readonly sidebarMgr: SidebarManagerImpl;
   readonly pluginMgr: PluginManagerImpl;
+  readonly perfMgr = new PerfManager();
   readonly analytics: AnalyticsInternal;
   readonly serviceWorkerController: ServiceWorkerController;
   httpRpc = {
@@ -67,7 +68,6 @@
   };
   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;
@@ -79,19 +79,31 @@
   // 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().
-  constructor(initArgs: AppInitArgs) {
+  private constructor(initArgs: AppInitArgs) {
     this.initArgs = initArgs;
     this.initialRouteArgs = initArgs.initialRouteArgs;
-    this.sidebarMgr = new SidebarManagerImpl({
-      sidebarEnabled: !this.initialRouteArgs.hideSidebar,
-    });
     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),
@@ -143,19 +155,16 @@
   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.
-  private static _instance: AppImpl;
-
-  // Invoked by frontend/index.ts.
-  static initialize(args: AppInitArgs) {
-    assertTrue(AppImpl._instance === undefined);
-    AppImpl._instance = new AppContext(args).forPlugin(CORE_PLUGIN_ID);
-  }
-
   static get instance(): AppImpl {
-    return assertExists(AppImpl._instance);
+    return AppContext.instance.forPlugin(CORE_PLUGIN_ID);
   }
 
   // Only called by AppContext.forPlugin().
@@ -173,6 +182,10 @@
     });
   }
 
+  forPlugin(pluginId: string): AppImpl {
+    return this.appCtx.forPlugin(pluginId);
+  }
+
   get commands(): CommandManagerImpl {
     return this.appCtx.commandMgr;
   }
@@ -201,8 +214,8 @@
     return this.appCtx.currentTrace?.forPlugin(this.pluginId);
   }
 
-  scheduleFullRedraw(): void {
-    raf.scheduleFullRedraw();
+  scheduleFullRedraw(force?: 'force'): void {
+    raf.scheduleFullRedraw(force);
   }
 
   get httpRpc() {
@@ -236,7 +249,6 @@
   }
 
   private async openTrace(src: TraceSource) {
-    assertTrue(this.pluginId === CORE_PLUGIN_ID);
     this.appCtx.closeCurrentTrace();
     this.appCtx.isLoadingTrace = true;
     try {
@@ -284,17 +296,8 @@
     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 perfDebugging(): PerfManager {
+    return this.appCtx.perfMgr;
   }
 
   get serviceWorkerController(): ServiceWorkerController {
diff --git a/ui/src/core/command_manager.ts b/ui/src/core/command_manager.ts
index fdf6ee6..ad0f482 100644
--- a/ui/src/core/command_manager.ts
+++ b/ui/src/core/command_manager.ts
@@ -15,6 +15,7 @@
 import {FuzzyFinder, FuzzySegment} from '../base/fuzzy';
 import {Registry} from '../base/registry';
 import {Command, CommandManager} from '../public/command';
+import {raf} from './raf_scheduler';
 
 export interface CommandWithMatchInfo extends Command {
   segments: FuzzySegment[];
@@ -39,10 +40,11 @@
     return this.registry.register(cmd);
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
+  runCommand(id: string, ...args: unknown[]): unknown {
     const cmd = this.registry.get(id);
-    return cmd.callback(...args);
+    const res = cmd.callback(...args);
+    Promise.resolve(res).finally(() => raf.scheduleFullRedraw('force'));
+    return res;
   }
 
   // Returns a list of commands that match the search term, along with a list
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index c30256a..caba9f0 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -22,6 +22,7 @@
 // - 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',
@@ -55,9 +56,11 @@
   'dev.perfetto.ProcessSummary',
   'dev.perfetto.ProcessThreadGroups',
   'dev.perfetto.QueryPage',
+  'dev.perfetto.RecordTrace',
   'dev.perfetto.RestorePinnedTrack',
   'dev.perfetto.Sched',
   'dev.perfetto.Screenshots',
+  'dev.perfetto.SqlModules',
   'dev.perfetto.Thread',
   'dev.perfetto.ThreadState',
   'dev.perfetto.TimelineSync',
diff --git a/ui/src/core/feature_flags.ts b/ui/src/core/feature_flags.ts
index 68ce511..7fa974d 100644
--- a/ui/src/core/feature_flags.ts
+++ b/ui/src/core/feature_flags.ts
@@ -16,6 +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.
+import {z} from 'zod';
 import {Flag, FlagSettings, OverrideState} from '../public/feature_flag';
 
 export interface FlagStore {
@@ -28,22 +29,6 @@
   [id: string]: OverrideState;
 }
 
-// Check if the given object is a valid FlagOverrides.
-// This is necessary since someone could modify the persisted flags
-// behind our backs.
-function isFlagOverrides(o: object): o is FlagOverrides {
-  const states = [
-    OverrideState.TRUE.toString(),
-    OverrideState.FALSE.toString(),
-  ];
-  for (const v of Object.values(o)) {
-    if (typeof v !== 'string' || !states.includes(v)) {
-      return false;
-    }
-  }
-  return true;
-}
-
 class Flags {
   private store: FlagStore;
   private flags: Map<string, FlagImpl>;
@@ -89,8 +74,17 @@
 
   load(): void {
     const o = this.store.load();
-    if (isFlagOverrides(o)) {
-      this.overrides = o;
+
+    // Check if the given object is a valid FlagOverrides.
+    // This is necessary since someone could modify the persisted flags
+    // behind our backs.
+    const flagsSchema = z.record(
+      z.string(),
+      z.union([z.literal(OverrideState.TRUE), z.literal(OverrideState.FALSE)]),
+    );
+    const {success, data} = flagsSchema.safeParse(o);
+    if (success) {
+      this.overrides = data;
     }
   }
 
@@ -105,21 +99,6 @@
 
     this.store.save(this.overrides);
   }
-
-  /**
-   * 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 {
@@ -202,10 +181,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/load_trace.ts b/ui/src/core/load_trace.ts
index c39245f..41196c7 100644
--- a/ui/src/core/load_trace.ts
+++ b/ui/src/core/load_trace.ts
@@ -147,7 +147,7 @@
       ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(),
     });
   }
-  engine.onResponseReceived = () => raf.scheduleFullRedraw();
+  engine.onResponseReceived = () => raf.scheduleFullRedraw('force');
 
   if (isMetatracingEnabled()) {
     engine.enableMetatrace(assertExists(getEnabledMetatracingCategories()));
diff --git a/ui/src/core/perf.ts b/ui/src/core/perf.ts
deleted file mode 100644
index 6e9afaf..0000000
--- a/ui/src/core/perf.ts
+++ /dev/null
@@ -1,135 +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';
-
-const hooks = {
-  isDebug: () => false,
-  toggleDebug: () => {},
-};
-
-export function setPerfHooks(isDebug: () => boolean, toggleDebug: () => void) {
-  hooks.isDebug = isDebug;
-  hooks.toggleDebug = toggleDebug;
-}
-
-// Shorthand for if globals perf debug mode is on.
-export const perfDebug = () => hooks.isDebug();
-
-// Returns performance.now() if perfDebug is enabled, otherwise 0.
-// This is needed because calling performance.now is generally expensive
-// and should not be done for every frame.
-export const debugNow = () => (perfDebug() ? performance.now() : 0);
-
-// Returns execution time of |fn| if perf debug mode is on. Returns 0 otherwise.
-export function measure(fn: () => void): number {
-  const start = debugNow();
-  fn();
-  return debugNow() - start;
-}
-
-// Stores statistics about samples, and keeps a fixed size buffer of most recent
-// samples.
-export class RunningStatistics {
-  private _count = 0;
-  private _mean = 0;
-  private _lastValue = 0;
-  private _ptr = 0;
-
-  private buffer: number[] = [];
-
-  constructor(private _maxBufferSize = 10) {}
-
-  addValue(value: number) {
-    this._lastValue = value;
-    if (this.buffer.length >= this._maxBufferSize) {
-      this.buffer[this._ptr++] = value;
-      if (this._ptr >= this.buffer.length) {
-        this._ptr -= this.buffer.length;
-      }
-    } else {
-      this.buffer.push(value);
-    }
-
-    this._mean = (this._mean * this._count + value) / (this._count + 1);
-    this._count++;
-  }
-
-  get mean() {
-    return this._mean;
-  }
-  get count() {
-    return this._count;
-  }
-  get bufferMean() {
-    return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
-  }
-  get bufferSize() {
-    return this.buffer.length;
-  }
-  get maxBufferSize() {
-    return this._maxBufferSize;
-  }
-  get last() {
-    return this._lastValue;
-  }
-}
-
-// Returns a summary string representation of a RunningStatistics object.
-export function runningStatStr(stat: RunningStatistics) {
-  return (
-    `Last: ${stat.last.toFixed(2)}ms | ` +
-    `Avg: ${stat.mean.toFixed(2)}ms | ` +
-    `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
-  );
-}
-
-export interface PerfStatsSource {
-  renderPerfStats(): m.Children;
-}
-
-// Globals singleton class that renders performance stats for the whole app.
-class PerfDisplay {
-  private containers: PerfStatsSource[] = [];
-
-  addContainer(container: PerfStatsSource) {
-    this.containers.push(container);
-  }
-
-  removeContainer(container: PerfStatsSource) {
-    const i = this.containers.indexOf(container);
-    this.containers.splice(i, 1);
-  }
-
-  renderPerfStats(src: PerfStatsSource) {
-    if (!perfDebug()) return;
-    const perfDisplayEl = document.querySelector('.perf-stats');
-    if (!perfDisplayEl) return;
-    m.render(perfDisplayEl, [
-      m('section', src.renderPerfStats()),
-      m(
-        'button.close-button',
-        {
-          onclick: hooks.toggleDebug,
-        },
-        m('i.material-icons', 'close'),
-      ),
-      this.containers.map((c, i) =>
-        m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
-      ),
-    ]);
-  }
-}
-
-export const perfDisplay = new PerfDisplay();
diff --git a/ui/src/core/perf_manager.ts b/ui/src/core/perf_manager.ts
new file mode 100644
index 0000000..e63e7e8
--- /dev/null
+++ b/ui/src/core/perf_manager.ts
@@ -0,0 +1,145 @@
+// 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 {raf} from './raf_scheduler';
+import {PerfStats, PerfStatsContainer, runningStatStr} from './perf_stats';
+
+export class PerfManager {
+  private _enabled = false;
+  readonly containers: PerfStatsContainer[] = [];
+
+  get enabled(): boolean {
+    return this._enabled;
+  }
+
+  set enabled(enabled: boolean) {
+    this._enabled = enabled;
+    raf.setPerfStatsEnabled(true);
+    this.containers.forEach((c) => c.setPerfStatsEnabled(enabled));
+  }
+
+  addContainer(container: PerfStatsContainer): Disposable {
+    this.containers.push(container);
+    return {
+      [Symbol.dispose]: () => {
+        const i = this.containers.indexOf(container);
+        this.containers.splice(i, 1);
+      },
+    };
+  }
+
+  renderPerfStats(): m.Children {
+    if (!this._enabled) return;
+    // The rendering of the perf stats UI is atypical. The main issue is that we
+    // want to redraw the mithril component even if there is no full DOM redraw
+    // happening (and we don't want to force redraws as a side effect). So we
+    // return here just a container and handle its rendering ourselves.
+    const perfMgr = this;
+    let removed = false;
+    return m('.perf-stats', {
+      oncreate(vnode: m.VnodeDOM) {
+        const animationFrame = (dom: Element) => {
+          if (removed) return;
+          m.render(dom, m(PerfStatsUi, {perfMgr}));
+          requestAnimationFrame(() => animationFrame(dom));
+        };
+        animationFrame(vnode.dom);
+      },
+      onremove() {
+        removed = true;
+      },
+    });
+  }
+}
+
+// The mithril component that draws the contents of the perf stats box.
+
+interface PerfStatsUiAttrs {
+  perfMgr: PerfManager;
+}
+
+class PerfStatsUi implements m.ClassComponent<PerfStatsUiAttrs> {
+  view({attrs}: m.Vnode<PerfStatsUiAttrs>) {
+    return m(
+      '.perf-stats',
+      {},
+      m('section', this.renderRafSchedulerStats()),
+      m(
+        'button.close-button',
+        {
+          onclick: () => (attrs.perfMgr.enabled = false),
+        },
+        m('i.material-icons', 'close'),
+      ),
+      attrs.perfMgr.containers.map((c, i) =>
+        m('section', m('div', `Panel Container ${i + 1}`), c.renderPerfStats()),
+      ),
+    );
+  }
+
+  renderRafSchedulerStats() {
+    return m(
+      'div',
+      m('div', [
+        m(
+          'button',
+          {onclick: () => raf.scheduleCanvasRedraw()},
+          'Do Canvas Redraw',
+        ),
+        '   |   ',
+        m(
+          'button',
+          {onclick: () => raf.scheduleFullRedraw()},
+          'Do Full Redraw',
+        ),
+      ]),
+      m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
+      m(
+        'table',
+        this.statTableHeader(),
+        this.statTableRow('Actions', raf.perfStats.rafActions),
+        this.statTableRow('Dom', raf.perfStats.rafDom),
+        this.statTableRow('Canvas', raf.perfStats.rafCanvas),
+        this.statTableRow('Total', raf.perfStats.rafTotal),
+      ),
+      m(
+        'div',
+        'Dom redraw: ' +
+          `Count: ${raf.perfStats.domRedraw.count} | ` +
+          runningStatStr(raf.perfStats.domRedraw),
+      ),
+    );
+  }
+
+  statTableHeader() {
+    return m(
+      'tr',
+      m('th', ''),
+      m('th', 'Last (ms)'),
+      m('th', 'Avg (ms)'),
+      m('th', 'Avg-10 (ms)'),
+    );
+  }
+
+  statTableRow(title: string, stat: PerfStats) {
+    return m(
+      'tr',
+      m('td', title),
+      m('td', stat.last.toFixed(2)),
+      m('td', stat.mean.toFixed(2)),
+      m('td', stat.bufferMean.toFixed(2)),
+    );
+  }
+}
diff --git a/ui/src/core/perf_stats.ts b/ui/src/core/perf_stats.ts
new file mode 100644
index 0000000..3f1eda0
--- /dev/null
+++ b/ui/src/core/perf_stats.ts
@@ -0,0 +1,78 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+// The interface that every container (e.g. Track Panels) that exposes granular
+// per-container masurements implements to be perf-stats-aware.
+export interface PerfStatsContainer {
+  setPerfStatsEnabled(enable: boolean): void;
+  renderPerfStats(): m.Children;
+}
+
+// Stores statistics about samples, and keeps a fixed size buffer of most recent
+// samples.
+export class PerfStats {
+  private _count = 0;
+  private _mean = 0;
+  private _lastValue = 0;
+  private _ptr = 0;
+
+  private buffer: number[] = [];
+
+  constructor(private _maxBufferSize = 10) {}
+
+  addValue(value: number) {
+    this._lastValue = value;
+    if (this.buffer.length >= this._maxBufferSize) {
+      this.buffer[this._ptr++] = value;
+      if (this._ptr >= this.buffer.length) {
+        this._ptr -= this.buffer.length;
+      }
+    } else {
+      this.buffer.push(value);
+    }
+
+    this._mean = (this._mean * this._count + value) / (this._count + 1);
+    this._count++;
+  }
+
+  get mean() {
+    return this._mean;
+  }
+  get count() {
+    return this._count;
+  }
+  get bufferMean() {
+    return this.buffer.reduce((sum, v) => sum + v, 0) / this.buffer.length;
+  }
+  get bufferSize() {
+    return this.buffer.length;
+  }
+  get maxBufferSize() {
+    return this._maxBufferSize;
+  }
+  get last() {
+    return this._lastValue;
+  }
+}
+
+// Returns a summary string representation of a RunningStatistics object.
+export function runningStatStr(stat: PerfStats) {
+  return (
+    `Last: ${stat.last.toFixed(2)}ms | ` +
+    `Avg: ${stat.mean.toFixed(2)}ms | ` +
+    `Avg${stat.maxBufferSize}: ${stat.bufferMean.toFixed(2)}ms`
+  );
+}
diff --git a/ui/src/core/perf_unittest.ts b/ui/src/core/perf_stats_unittest.ts
similarity index 86%
rename from ui/src/core/perf_unittest.ts
rename to ui/src/core/perf_stats_unittest.ts
index 5ba357c..1b24bf5 100644
--- a/ui/src/core/perf_unittest.ts
+++ b/ui/src/core/perf_stats_unittest.ts
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {RunningStatistics} from './perf';
+import {PerfStats} from './perf_stats';
 
 test('buffer size is accurate before reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 10; i++) {
     buf.addValue(i);
@@ -24,7 +24,7 @@
 });
 
 test('buffer size is accurate after reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 10; i++) {
     buf.addValue(i);
@@ -37,7 +37,7 @@
 });
 
 test('buffer mean is accurate before reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   buf.addValue(1);
   buf.addValue(2);
@@ -47,7 +47,7 @@
 });
 
 test('buffer mean is accurate after reaching max capacity', () => {
-  const buf = new RunningStatistics(10);
+  const buf = new PerfStats(10);
 
   for (let i = 0; i < 20; i++) {
     buf.addValue(2);
diff --git a/ui/src/core/raf_scheduler.ts b/ui/src/core/raf_scheduler.ts
index c6ca0fc..2935963 100644
--- a/ui/src/core/raf_scheduler.ts
+++ b/ui/src/core/raf_scheduler.ts
@@ -12,39 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {PerfStats} from './perf_stats';
 import m from 'mithril';
-import {
-  debugNow,
-  measure,
-  perfDebug,
-  perfDisplay,
-  PerfStatsSource,
-  RunningStatistics,
-  runningStatStr,
-} from './perf';
+import {featureFlags} from './feature_flags';
 
-function statTableHeader() {
-  return m(
-    'tr',
-    m('th', ''),
-    m('th', 'Last (ms)'),
-    m('th', 'Avg (ms)'),
-    m('th', 'Avg-10 (ms)'),
-  );
-}
+export type AnimationCallback = (lastFrameMs: number) => void;
+export type RedrawCallback = () => void;
 
-function statTableRow(title: string, stat: RunningStatistics) {
-  return m(
-    'tr',
-    m('td', title),
-    m('td', stat.last.toFixed(2)),
-    m('td', stat.mean.toFixed(2)),
-    m('td', stat.bufferMean.toFixed(2)),
-  );
-}
-
-export type ActionCallback = (nowMs: number) => void;
-export type RedrawCallback = (nowMs: number) => void;
+export const AUTOREDRAW_FLAG = featureFlags.register({
+  id: 'mithrilAutoredraw',
+  name: 'Enable Mithril autoredraw',
+  description: 'Turns calls to schedulefullRedraw() a no-op',
+  defaultValue: false,
+});
 
 // This class orchestrates all RAFs in the UI. It ensures that there is only
 // one animation frame handler overall and that callbacks are called in
@@ -54,146 +34,176 @@
 // - redraw callbacks that will repaint canvases.
 // This class guarantees that, on each frame, redraw callbacks are called after
 // all action callbacks.
-export class RafScheduler implements PerfStatsSource {
-  private actionCallbacks = new Set<ActionCallback>();
+export class RafScheduler {
+  // These happen at the beginning of any animation frame. Used by Animation.
+  private animationCallbacks = new Set<AnimationCallback>();
+
+  // These happen during any animaton frame, after the (optional) DOM redraw.
   private canvasRedrawCallbacks = new Set<RedrawCallback>();
-  private _syncDomRedraw: RedrawCallback = (_) => {};
+
+  // These happen at the end of full (DOM) animation frames.
+  private postRedrawCallbacks = new Array<RedrawCallback>();
   private hasScheduledNextFrame = false;
   private requestedFullRedraw = false;
   private isRedrawing = false;
   private _shutdown = false;
-  private _beforeRedraw: () => void = () => {};
-  private _afterRedraw: () => void = () => {};
-  private _pendingCallbacks: RedrawCallback[] = [];
+  private recordPerfStats = false;
+  private mounts = new Map<Element, m.ComponentTypes>();
 
-  private perfStats = {
-    rafActions: new RunningStatistics(),
-    rafCanvas: new RunningStatistics(),
-    rafDom: new RunningStatistics(),
-    rafTotal: new RunningStatistics(),
-    domRedraw: new RunningStatistics(),
+  readonly perfStats = {
+    rafActions: new PerfStats(),
+    rafCanvas: new PerfStats(),
+    rafDom: new PerfStats(),
+    rafTotal: new PerfStats(),
+    domRedraw: new PerfStats(),
   };
 
-  start(cb: ActionCallback) {
-    this.actionCallbacks.add(cb);
-    this.maybeScheduleAnimationFrame();
+  constructor() {
+    // Patch m.redraw() to our RAF full redraw.
+    const origSync = m.redraw.sync;
+    const redrawFn = () => this.scheduleFullRedraw('force');
+    redrawFn.sync = origSync;
+    m.redraw = redrawFn;
+
+    m.mount = this.mount.bind(this);
   }
 
-  stop(cb: ActionCallback) {
-    this.actionCallbacks.delete(cb);
-  }
-
-  addRedrawCallback(cb: RedrawCallback) {
-    this.canvasRedrawCallbacks.add(cb);
-  }
-
-  removeRedrawCallback(cb: RedrawCallback) {
-    this.canvasRedrawCallbacks.delete(cb);
-  }
-
-  addPendingCallback(cb: RedrawCallback) {
-    this._pendingCallbacks.push(cb);
+  // Schedule re-rendering of virtual DOM and canvas.
+  // If a callback is passed it will be executed after the DOM redraw has
+  // completed.
+  scheduleFullRedraw(force?: 'force', cb?: RedrawCallback) {
+    // If we are using autoredraw mode, make this function a no-op unless
+    // 'force' is passed.
+    if (AUTOREDRAW_FLAG.get() && force !== 'force') return;
+    this.requestedFullRedraw = true;
+    cb && this.postRedrawCallbacks.push(cb);
+    this.maybeScheduleAnimationFrame(true);
   }
 
   // Schedule re-rendering of canvas only.
-  scheduleRedraw() {
+  scheduleCanvasRedraw() {
     this.maybeScheduleAnimationFrame(true);
   }
 
+  startAnimation(cb: AnimationCallback) {
+    this.animationCallbacks.add(cb);
+    this.maybeScheduleAnimationFrame();
+  }
+
+  stopAnimation(cb: AnimationCallback) {
+    this.animationCallbacks.delete(cb);
+  }
+
+  addCanvasRedrawCallback(cb: RedrawCallback): Disposable {
+    this.canvasRedrawCallbacks.add(cb);
+    const canvasRedrawCallbacks = this.canvasRedrawCallbacks;
+    return {
+      [Symbol.dispose]() {
+        canvasRedrawCallbacks.delete(cb);
+      },
+    };
+  }
+
+  mount(element: Element, component: m.ComponentTypes | null): void {
+    const mounts = this.mounts;
+    if (component === null) {
+      mounts.delete(element);
+    } else {
+      mounts.set(element, component);
+    }
+    this.syncDomRedrawMountEntry(element, component);
+  }
+
   shutdown() {
     this._shutdown = true;
   }
 
-  set domRedraw(cb: RedrawCallback) {
-    this._syncDomRedraw = cb;
-  }
-
-  set beforeRedraw(cb: () => void) {
-    this._beforeRedraw = cb;
-  }
-
-  set afterRedraw(cb: () => void) {
-    this._afterRedraw = cb;
-  }
-
-  // Schedule re-rendering of virtual DOM and canvas.
-  scheduleFullRedraw() {
-    this.requestedFullRedraw = true;
-    this.maybeScheduleAnimationFrame(true);
-  }
-
-  // Schedule a full redraw to happen after a short delay (50 ms).
-  // This is done to prevent flickering / visual noise and allow the UI to fetch
-  // the initial data from the Trace Processor.
-  // There is a chance that someone else schedules a full redraw in the
-  // meantime, forcing the flicker, but in practice it works quite well and
-  // avoids a lot of complexity for the callers.
-  scheduleDelayedFullRedraw() {
-    // 50ms is half of the responsiveness threshold (100ms):
-    // https://web.dev/rail/#response-process-events-in-under-50ms
-    const delayMs = 50;
-    setTimeout(() => this.scheduleFullRedraw(), delayMs);
-  }
-
-  syncDomRedraw(nowMs: number) {
-    const redrawStart = debugNow();
-    this._syncDomRedraw(nowMs);
-    if (perfDebug()) {
-      this.perfStats.domRedraw.addValue(debugNow() - redrawStart);
-    }
+  setPerfStatsEnabled(enabled: boolean) {
+    this.recordPerfStats = enabled;
+    this.scheduleFullRedraw();
   }
 
   get hasPendingRedraws(): boolean {
     return this.isRedrawing || this.hasScheduledNextFrame;
   }
 
-  private syncCanvasRedraw(nowMs: number) {
-    const redrawStart = debugNow();
-    if (this.isRedrawing) return;
-    this._beforeRedraw();
-    this.isRedrawing = true;
-    for (const redraw of this.canvasRedrawCallbacks) redraw(nowMs);
-    this.isRedrawing = false;
-    this._afterRedraw();
-    for (const cb of this._pendingCallbacks) {
-      cb(nowMs);
+  private syncDomRedraw() {
+    const redrawStart = performance.now();
+
+    for (const [element, component] of this.mounts.entries()) {
+      this.syncDomRedrawMountEntry(element, component);
     }
-    this._pendingCallbacks.splice(0, this._pendingCallbacks.length);
-    if (perfDebug()) {
-      this.perfStats.rafCanvas.addValue(debugNow() - redrawStart);
+
+    if (this.recordPerfStats) {
+      this.perfStats.domRedraw.addValue(performance.now() - redrawStart);
+    }
+  }
+
+  private syncDomRedrawMountEntry(
+    element: Element,
+    component: m.ComponentTypes | null,
+  ) {
+    // Mithril's render() function takes a third argument which tells us if a
+    // further redraw is needed (e.g. due to managed event handler). This allows
+    // us to implement auto-redraw. The redraw argument is documented in the
+    // official Mithril docs but is just not part of the @types/mithril package.
+    const mithrilRender = m.render as (
+      el: Element,
+      vnodes: m.Children,
+      redraw?: () => void,
+    ) => void;
+
+    mithrilRender(
+      element,
+      component !== null ? m(component) : null,
+      AUTOREDRAW_FLAG.get() ? () => raf.scheduleFullRedraw('force') : undefined,
+    );
+  }
+
+  private syncCanvasRedraw() {
+    const redrawStart = performance.now();
+    if (this.isRedrawing) return;
+    this.isRedrawing = true;
+    this.canvasRedrawCallbacks.forEach((cb) => cb());
+    this.isRedrawing = false;
+    if (this.recordPerfStats) {
+      this.perfStats.rafCanvas.addValue(performance.now() - redrawStart);
     }
   }
 
   private maybeScheduleAnimationFrame(force = false) {
     if (this.hasScheduledNextFrame) return;
-    if (this.actionCallbacks.size !== 0 || force) {
+    if (this.animationCallbacks.size !== 0 || force) {
       this.hasScheduledNextFrame = true;
       window.requestAnimationFrame(this.onAnimationFrame.bind(this));
     }
   }
 
-  private onAnimationFrame(nowMs: number) {
+  private onAnimationFrame(lastFrameMs: number) {
     if (this._shutdown) return;
-    const rafStart = debugNow();
     this.hasScheduledNextFrame = false;
-
     const doFullRedraw = this.requestedFullRedraw;
     this.requestedFullRedraw = false;
 
-    const actionTime = measure(() => {
-      for (const action of this.actionCallbacks) action(nowMs);
-    });
+    const tStart = performance.now();
+    this.animationCallbacks.forEach((cb) => cb(lastFrameMs));
+    const tAnim = performance.now();
+    doFullRedraw && this.syncDomRedraw();
+    const tDom = performance.now();
+    this.syncCanvasRedraw();
+    const tCanvas = performance.now();
 
-    const domTime = measure(() => {
-      if (doFullRedraw) this.syncDomRedraw(nowMs);
-    });
-    const canvasTime = measure(() => this.syncCanvasRedraw(nowMs));
-
-    const totalRafTime = debugNow() - rafStart;
-    this.updatePerfStats(actionTime, domTime, canvasTime, totalRafTime);
-    perfDisplay.renderPerfStats(this);
-
+    const animTime = tAnim - tStart;
+    const domTime = tDom - tAnim;
+    const canvasTime = tCanvas - tDom;
+    const totalTime = tCanvas - tStart;
+    this.updatePerfStats(animTime, domTime, canvasTime, totalTime);
     this.maybeScheduleAnimationFrame();
+
+    if (doFullRedraw && this.postRedrawCallbacks.length > 0) {
+      const pendingCbs = this.postRedrawCallbacks.splice(0); // splice = clear.
+      pendingCbs.forEach((cb) => cb());
+    }
   }
 
   private updatePerfStats(
@@ -202,42 +212,12 @@
     canvasTime: number,
     totalRafTime: number,
   ) {
-    if (!perfDebug()) return;
+    if (!this.recordPerfStats) return;
     this.perfStats.rafActions.addValue(actionsTime);
     this.perfStats.rafDom.addValue(domTime);
     this.perfStats.rafCanvas.addValue(canvasTime);
     this.perfStats.rafTotal.addValue(totalRafTime);
   }
-
-  renderPerfStats() {
-    return m(
-      'div',
-      m('div', [
-        m('button', {onclick: () => this.scheduleRedraw()}, 'Do Canvas Redraw'),
-        '   |   ',
-        m(
-          'button',
-          {onclick: () => this.scheduleFullRedraw()},
-          'Do Full Redraw',
-        ),
-      ]),
-      m('div', 'Raf Timing ' + '(Total may not add up due to imprecision)'),
-      m(
-        'table',
-        statTableHeader(),
-        statTableRow('Actions', this.perfStats.rafActions),
-        statTableRow('Dom', this.perfStats.rafDom),
-        statTableRow('Canvas', this.perfStats.rafCanvas),
-        statTableRow('Total', this.perfStats.rafTotal),
-      ),
-      m(
-        'div',
-        'Dom redraw: ' +
-          `Count: ${this.perfStats.domRedraw.count} | ` +
-          runningStatStr(this.perfStats.domRedraw),
-      ),
-    );
-  }
 }
 
 export const raf = new RafScheduler();
diff --git a/ui/src/core/scroll_helper.ts b/ui/src/core/scroll_helper.ts
index 59b7b11..c732b91 100644
--- a/ui/src/core/scroll_helper.ts
+++ b/ui/src/core/scroll_helper.ts
@@ -35,7 +35,7 @@
   // See comments in ScrollToArgs for the intended semantics.
   scrollTo(args: ScrollToArgs) {
     const {time, track} = args;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
 
     if (time !== undefined) {
       if (time.end === undefined) {
diff --git a/ui/src/core/sidebar_manager.ts b/ui/src/core/sidebar_manager.ts
index 11c12dd..9de9b90 100644
--- a/ui/src/core/sidebar_manager.ts
+++ b/ui/src/core/sidebar_manager.ts
@@ -27,9 +27,9 @@
 
   readonly menuItems = new Registry<SidebarMenuItemInternal>((m) => m.id);
 
-  constructor(args: {sidebarEnabled: boolean}) {
-    this.enabled = args.sidebarEnabled;
-    this._visible = args.sidebarEnabled;
+  constructor(args: {disabled?: boolean; hidden?: boolean}) {
+    this.enabled = !args.disabled;
+    this._visible = !args.hidden;
   }
 
   addMenuItem(item: SidebarMenuItem): Disposable {
diff --git a/ui/src/core/timeline.ts b/ui/src/core/timeline.ts
index d91503c..0efc660 100644
--- a/ui/src/core/timeline.ts
+++ b/ui/src/core/timeline.ts
@@ -12,14 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertTrue} from '../base/logging';
+import {assertTrue, assertUnreachable} from '../base/logging';
 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 {DurationPrecision, Timeline, TimestampFormat} from '../public/timeline';
+import {
+  durationPrecision,
+  setDurationPrecision,
+  setTimestampFormat,
+  timestampFormat,
+} from './timestamp_format';
 import {TraceInfo} from '../public/trace_info';
 
 const MIN_DURATION = 10;
@@ -46,7 +51,7 @@
 
   set highlightedSliceId(x) {
     this._highlightedSliceId = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredNoteTimestamp() {
@@ -55,7 +60,7 @@
 
   set hoveredNoteTimestamp(x) {
     this._hoveredNoteTimestamp = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredUtid() {
@@ -64,7 +69,7 @@
 
   set hoveredUtid(x) {
     this._hoveredUtid = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get hoveredPid() {
@@ -73,7 +78,7 @@
 
   set hoveredPid(x) {
     this._hoveredPid = x;
-    raf.scheduleFullRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // This is used to calculate the tracks within a Y range for area selection.
@@ -95,7 +100,7 @@
       .scale(ratio, centerPoint, MIN_DURATION)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   panVisibleWindow(delta: number) {
@@ -103,7 +108,7 @@
       .translate(delta)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Given a timestamp, if |ts| is not currently in view move the view to
@@ -136,7 +141,7 @@
 
   deselectArea() {
     this._selectedArea = undefined;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   get selectedArea(): Area | undefined {
@@ -160,7 +165,7 @@
       .clampDuration(MIN_DURATION)
       .fitWithin(this.traceInfo.start, this.traceInfo.end);
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Get the bounds of the visible window as a high-precision time span
@@ -174,7 +179,7 @@
 
   set hoverCursorTimestamp(t: time | undefined) {
     this._hoverCursorTimestamp = t;
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   // Offset between t=0 and the configured time domain.
@@ -183,7 +188,7 @@
     switch (fmt) {
       case TimestampFormat.Timecode:
       case TimestampFormat.Seconds:
-      case TimestampFormat.Milliseoncds:
+      case TimestampFormat.Milliseconds:
       case TimestampFormat.Microseconds:
         return this.traceInfo.start;
       case TimestampFormat.TraceNs:
@@ -194,8 +199,7 @@
       case TimestampFormat.TraceTz:
         return this.traceInfo.traceTzOffset;
       default:
-        const x: never = fmt;
-        throw new Error(`Unsupported format ${x}`);
+        assertUnreachable(fmt);
     }
   }
 
@@ -203,4 +207,20 @@
   toDomainTime(ts: time): time {
     return Time.sub(ts, this.timestampOffset());
   }
+
+  get timestampFormat() {
+    return timestampFormat();
+  }
+
+  set timestampFormat(format: TimestampFormat) {
+    setTimestampFormat(format);
+  }
+
+  get durationPrecision() {
+    return durationPrecision();
+  }
+
+  set durationPrecision(precision: DurationPrecision) {
+    setDurationPrecision(precision);
+  }
 }
diff --git a/ui/src/core/timestamp_format.ts b/ui/src/core/timestamp_format.ts
index f4d7d80..25a8ba1 100644
--- a/ui/src/core/timestamp_format.ts
+++ b/ui/src/core/timestamp_format.ts
@@ -13,17 +13,7 @@
 // limitations under the License.
 
 import {isEnumValue} from '../base/object_utils';
-
-export enum TimestampFormat {
-  Timecode = 'timecode',
-  TraceNs = 'traceNs',
-  TraceNsLocale = 'traceNsLocale',
-  Seconds = 'seconds',
-  Milliseoncds = 'milliseconds',
-  Microseconds = 'microseconds',
-  UTC = 'utc',
-  TraceTz = 'traceTz',
-}
+import {DurationPrecision, TimestampFormat} from '../public/timeline';
 
 let timestampFormatCached: TimestampFormat | undefined;
 
@@ -49,11 +39,6 @@
   localStorage.setItem(TIMESTAMP_FORMAT_KEY, format);
 }
 
-export enum DurationPrecision {
-  Full = 'full',
-  HumanReadable = 'human_readable',
-}
-
 let durationFormatCached: DurationPrecision | undefined;
 
 const DURATION_FORMAT_KEY = 'durationFormat';
diff --git a/ui/src/core/trace_impl.ts b/ui/src/core/trace_impl.ts
index b3ab662..abed7f5 100644
--- a/ui/src/core/trace_impl.ts
+++ b/ui/src/core/trace_impl.ts
@@ -48,6 +48,9 @@
 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';
+import {PerfManager} from './perf_manager';
 
 /**
  * Handles the per-trace state of the UI
@@ -423,6 +426,18 @@
     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],
@@ -446,6 +461,10 @@
     }
   }
 
+  get perfDebugging(): PerfManager {
+    return this.appImpl.perfDebugging;
+  }
+
   get trash(): DisposableStack {
     return this.traceCtx.trash;
   }
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index 369d2fe..e13d479 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -18,6 +18,7 @@
 import {AppImpl} from '../../core/app_impl';
 import {getTimeSpanOfSelectionOrVisibleWindow} from '../../public/utils';
 import {exists} from '../../base/utils';
+import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'perfetto.TrackUtils';
@@ -83,5 +84,57 @@
         id && ctx.workspace.getTrackById(id)?.pin();
       },
     });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.SelectNextTrackEvent',
+      name: 'Select next track event',
+      defaultHotkey: '.',
+      callback: async () => {
+        await selectAdjacentTrackEvent(ctx, 'next');
+      },
+    });
+
+    ctx.commands.registerCommand({
+      id: 'perfetto.SelectPreviousTrackEvent',
+      name: 'Select previous track event',
+      defaultHotkey: ',',
+      callback: async () => {
+        await selectAdjacentTrackEvent(ctx, 'prev');
+      },
+    });
   }
 }
+
+/**
+ * If a track event is currently selected, select the next or previous event on
+ * that same track chronologically ordered by `ts`.
+ */
+async function selectAdjacentTrackEvent(
+  ctx: Trace,
+  direction: 'next' | 'prev',
+) {
+  const selection = ctx.selection.selection;
+  if (selection.kind !== 'track_event') return;
+
+  const td = ctx.tracks.getTrack(selection.trackUri);
+  const dataset = td?.track.getDataset?.();
+  if (!dataset || !dataset.implements({id: NUM, ts: LONG})) return;
+
+  const windowFunc = direction === 'next' ? 'LEAD' : 'LAG';
+  const result = await ctx.engine.query(`
+      WITH
+        CTE AS (
+          SELECT
+            id,
+            ${windowFunc}(id) OVER (ORDER BY ts) AS resultId
+          FROM (${dataset.query()})
+        )
+      SELECT * FROM CTE WHERE id = ${selection.eventId}
+    `);
+  const resultId = result.maybeFirstRow({resultId: NUM_NULL})?.resultId;
+  if (!exists(resultId)) return;
+
+  ctx.selection.selectTrackEvent(selection.trackUri, resultId, {
+    scrollToSelection: true,
+  });
+}
diff --git a/ui/src/frontend/aggregation_panel.ts b/ui/src/frontend/aggregation_panel.ts
index 3d441ba..2e1d00f 100644
--- a/ui/src/frontend/aggregation_panel.ts
+++ b/ui/src/frontend/aggregation_panel.ts
@@ -20,7 +20,7 @@
   isEmptyData,
 } from '../public/aggregation';
 import {colorForState} from '../public/lib/colorizer';
-import {DurationWidget} from './widgets/duration';
+import {DurationWidget} from '../public/lib/widgets/duration';
 import {EmptyState} from '../widgets/empty_state';
 import {Anchor} from '../widgets/anchor';
 import {Icons} from '../base/semantic_icons';
diff --git a/ui/src/frontend/animation.ts b/ui/src/frontend/animation.ts
index c8428c4..74cf065 100644
--- a/ui/src/frontend/animation.ts
+++ b/ui/src/frontend/animation.ts
@@ -31,12 +31,12 @@
     }
     this.startMs = nowMs;
     this.endMs = nowMs + durationMs;
-    raf.start(this.boundOnAnimationFrame);
+    raf.startAnimation(this.boundOnAnimationFrame);
   }
 
   stop() {
     this.endMs = 0;
-    raf.stop(this.boundOnAnimationFrame);
+    raf.stopAnimation(this.boundOnAnimationFrame);
   }
 
   get startTimeMs(): number {
@@ -45,7 +45,7 @@
 
   private onAnimationFrame(nowMs: number) {
     if (nowMs >= this.endMs) {
-      raf.stop(this.boundOnAnimationFrame);
+      raf.stopAnimation(this.boundOnAnimationFrame);
       return;
     }
     this.onAnimationStep(Math.max(Math.round(nowMs - this.startMs), 0));
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index c09ccbc..2bea9d9 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -25,7 +25,6 @@
 import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
 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';
 
@@ -180,13 +179,7 @@
   unit?: string;
 }
 
-export type BaseCounterTrackArgs = NewTrackArgs & {
-  options?: Partial<CounterOptions>;
-};
-
 export abstract class BaseCounterTrack implements Track {
-  protected trace: Trace;
-  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -204,7 +197,6 @@
 
   private mousePos = {x: 0, y: 0};
   private hover?: CounterTooltipState;
-  private defaultOptions: Partial<CounterOptions>;
   private options?: CounterOptions;
 
   private readonly trash: AsyncDisposableStack;
@@ -244,10 +236,11 @@
     };
   }
 
-  constructor(args: BaseCounterTrackArgs) {
-    this.trace = args.trace;
-    this.uri = args.uri;
-    this.defaultOptions = args.options ?? {};
+  constructor(
+    protected readonly trace: Trace,
+    protected readonly uri: string,
+    protected readonly defaultOptions: Partial<CounterOptions> = {},
+  ) {
     this.trash = new AsyncDisposableStack();
   }
 
@@ -867,7 +860,7 @@
     this.countersKey = countersKey;
     this.counters = data;
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   private async createTableAndFetchLimits(
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 7ef1cd4..83c9d73 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -28,13 +28,13 @@
 import {LONG, NUM} from '../trace_processor/query_result';
 import {checkerboardExcept} from './checkerboard';
 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/track';
 import {Point2D, VerticalBounds} from '../base/geom';
 import {Trace} from '../public/trace';
+import {SourceDataset, Dataset} from '../trace_processor/dataset';
 
 // The common class that underpins all tracks drawing slices.
 
@@ -162,8 +162,6 @@
 > implements Track
 {
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
-  protected trace: Trace;
-  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -239,9 +237,10 @@
     _selectedSlice?: SliceT,
   ): void {}
 
-  constructor(args: NewTrackArgs) {
-    this.trace = args.trace;
-    this.uri = args.uri;
+  constructor(
+    protected readonly trace: Trace,
+    protected readonly uri: string,
+  ) {
     // Work out the extra columns.
     // This is the union of the embedder-defined columns and the base columns
     // we know about (ts, dur, ...).
@@ -694,7 +693,7 @@
     this.onUpdatedSlices(slices);
     this.slices = slices;
 
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 
   private rowToSliceInternal(row: RowT): CastInternal<SliceT> {
@@ -972,6 +971,17 @@
     });
     return {ts: Time.fromRaw(row.ts), dur: Duration.fromRaw(row.dur)};
   }
+
+  getDataset(): Dataset | undefined {
+    return new SourceDataset({
+      src: this.getSqlSource(),
+      schema: {
+        id: NUM,
+        ts: LONG,
+        dur: LONG,
+      },
+    });
+  }
 }
 
 // This is the argument passed to onSliceOver(args).
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index dbe4a02..3e27b07 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -14,9 +14,7 @@
 
 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';
@@ -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)')) {
@@ -246,7 +242,7 @@
       this.uploadStatus = '';
       const uploader = new GcsUploader(this.traceData, {
         onProgress: () => {
-          raf.scheduleFullRedraw();
+          raf.scheduleFullRedraw('force');
           this.uploadStatus = uploader.getEtaString();
           if (uploader.state === 'UPLOADED') {
             this.traceState = 'UPLOADED';
@@ -356,135 +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('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
-      m('pre', message),
-    ),
-  });
-}
-
-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',
@@ -534,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/help_modal.ts b/ui/src/frontend/help_modal.ts
index 819f271..322748e 100644
--- a/ui/src/frontend/help_modal.ts
+++ b/ui/src/frontend/help_modal.ts
@@ -54,7 +54,7 @@
     nativeKeyboardLayoutMap()
       .then((keyMap: KeyboardLayoutMap) => {
         this.keyMap = keyMap;
-        AppImpl.instance.scheduleFullRedraw();
+        AppImpl.instance.scheduleFullRedraw('force');
       })
       .catch((e) => {
         if (
@@ -69,7 +69,7 @@
           // The alternative would be to show key mappings for all keyboard
           // layouts which is not feasible.
           this.keyMap = new EnglishQwertyKeyboardLayoutMap();
-          AppImpl.instance.scheduleFullRedraw();
+          AppImpl.instance.scheduleFullRedraw('force');
         } else {
           // Something unexpected happened. Either the browser doesn't conform
           // to the keyboard API spec, or the keyboard API spec has changed!
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index 198f08e..600f08b 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -20,7 +20,7 @@
 import m from 'mithril';
 import {defer} from '../base/deferred';
 import {addErrorHandler, reportError} from '../base/logging';
-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';
@@ -33,8 +33,6 @@
 import {globals} from './globals';
 import {HomePage} from './home_page';
 import {postMessageHandler} from './post_message_handler';
-import {RecordPage} from './record_page';
-import {RecordPageV2} from './record_page_v2';
 import {Route, Router} from '../core/router';
 import {CheckHttpRpcConnection} from './rpc_http_dialog';
 import {maybeOpenTraceFromRoute} from './trace_url_handler';
@@ -65,19 +63,18 @@
 });
 
 function routeChange(route: Route) {
-  raf.scheduleFullRedraw();
-  maybeOpenTraceFromRoute(route);
-  if (route.fragment) {
-    // This needs to happen after the next redraw call. It's not enough
-    // to use setTimeout(..., 0); since that may occur before the
-    // redraw scheduled above.
-    raf.addPendingCallback(() => {
+  raf.scheduleFullRedraw('force', () => {
+    if (route.fragment) {
+      // This needs to happen after the next redraw call. It's not enough
+      // to use setTimeout(..., 0); since that may occur before the
+      // redraw scheduled above.
       const e = document.getElementById(route.fragment);
       if (e) {
         e.scrollIntoView();
       }
-    });
-  }
+    }
+  });
+  maybeOpenTraceFromRoute(route);
 }
 
 function setupContentSecurityPolicy() {
@@ -151,7 +148,7 @@
   });
 
   // Wire up raf for widgets.
-  setScheduleFullRedraw(() => raf.scheduleFullRedraw());
+  setScheduleFullRedraw((force?: 'force') => raf.scheduleFullRedraw(force));
 
   // Load the css. The load is asynchronous and the CSS is not ready by the time
   // appendChild returns.
@@ -224,18 +221,12 @@
   const pages = AppImpl.instance.pages;
   const traceless = true;
   pages.registerPage({route: '/', traceless, page: HomePage});
-  const recordPage = RECORDING_V2_FLAG.get() ? RecordPageV2 : RecordPage;
-  pages.registerPage({route: '/record', traceless, page: recordPage});
   pages.registerPage({route: '/viewer', page: ViewerPage});
   const router = new Router();
   router.onRouteChanged = routeChange;
 
-  raf.domRedraw = () => {
-    m.render(
-      document.body,
-      m(UiMain, pages.renderPageForCurrentRoute(AppImpl.instance.trace)),
-    );
-  };
+  // Mount the main mithril component. This also forces a sync render pass.
+  raf.mount(document.body, UiMain);
 
   if (
     (location.origin.startsWith('http://localhost:') ||
@@ -274,12 +265,6 @@
     routeChange(route);
   });
 
-  // Force one initial render to get everything in place
-  m.render(
-    document.body,
-    m(UiMain, AppImpl.instance.pages.renderPageForCurrentRoute(undefined)),
-  );
-
   // Initialize plugins, now that we are ready to go.
   const pluginManager = AppImpl.instance.plugins;
   CORE_PLUGINS.forEach((p) => pluginManager.registerPlugin(p));
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
index ed9b5f0..6d79af2 100644
--- a/ui/src/frontend/named_slice_track.ts
+++ b/ui/src/frontend/named_slice_track.ts
@@ -16,7 +16,7 @@
 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 {LONG, NUM, STR, STR_NULL} from '../trace_processor/query_result';
 import {
   BASE_ROW,
   BaseSliceTrack,
@@ -26,10 +26,11 @@
   SLICE_FLAGS_INSTANT,
 } from './base_slice_track';
 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';
+import {SourceDataset, Dataset} from '../trace_processor/dataset';
+import {formatDuration} from '../public/lib/time_utils';
+import {Trace} from '../public/trace';
 
 export const NAMED_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -44,8 +45,8 @@
   SliceType extends Slice = Slice,
   RowType extends NamedRow = NamedRow,
 > extends BaseSliceTrack<SliceType, RowType> {
-  constructor(args: NewTrackArgs) {
-    super(args);
+  constructor(trace: Trace, uri: string) {
+    super(trace, uri);
   }
 
   // Converts a SQL result row to an "Impl" Slice.
@@ -65,7 +66,7 @@
     } else if (flags & SLICE_FLAGS_INSTANT) {
       duration = 'Instant';
     } else {
-      duration = renderDuration(dur);
+      duration = formatDuration(this.trace, dur);
     }
     args.tooltip = [`${title} - [${duration}]`];
   }
@@ -80,4 +81,16 @@
     // because this class is exposed to plugins (which see only Trace).
     return new ThreadSliceDetailsPanel(assertIsInstance(this.trace, TraceImpl));
   }
+
+  override getDataset(): Dataset | undefined {
+    return new SourceDataset({
+      src: this.getSqlSource(),
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+      },
+    });
+  }
 }
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 21dc29a..c4d76b7 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -23,7 +23,7 @@
 import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
 import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {Timestamp} from './widgets/timestamp';
+import {Timestamp} from '../public/lib/widgets/timestamp';
 import {assertUnreachable} from '../base/logging';
 import {DetailsPanel} from '../public/details_panel';
 import {TimeScale} from '../base/time_scale';
@@ -89,11 +89,11 @@
         onmousemove: (e: MouseEvent) => {
           this.mouseDragging = true;
           this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
-          raf.scheduleRedraw();
+          raf.scheduleCanvasRedraw();
         },
         onmouseenter: (e: MouseEvent) => {
           this.hoveredX = currentTargetOffset(e).x - TRACK_SHELL_WIDTH;
-          raf.scheduleRedraw();
+          raf.scheduleCanvasRedraw();
         },
         onmouseout: () => {
           this.hoveredX = null;
diff --git a/ui/src/frontend/omnibox.ts b/ui/src/frontend/omnibox.ts
index 5f1e29a..c94ee3f 100644
--- a/ui/src/frontend/omnibox.ts
+++ b/ui/src/frontend/omnibox.ts
@@ -328,7 +328,13 @@
     document.removeEventListener('mousedown', this.onMouseDown);
   }
 
+  // This is defined as an arrow function to have a single handler that can be
+  // added/remove while keeping `this` bound.
   private onMouseDown = (e: Event) => {
+    // We need to schedule a redraw manually as this event handler was added
+    // manually to the DOM and doesn't use Mithril's auto-redraw system.
+    raf.scheduleFullRedraw('force');
+
     // Don't close if the click was within ourselves or our popup.
     if (e.target instanceof Node) {
       if (this.popupElement && this.popupElement.contains(e.target)) {
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index e3798e1..0d04ce2 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 import {Duration, Time, TimeSpan, duration, time} from '../base/time';
 import {colorForCpu} from '../public/lib/colorizer';
-import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
+import {timestampFormat} from '../core/timestamp_format';
 import {
   OVERVIEW_TIMELINE_NON_VISIBLE_COLOR,
   TRACK_SHELL_WIDTH,
@@ -39,6 +39,8 @@
 import {LONG, NUM} from '../trace_processor/query_result';
 import {raf} from '../core/raf_scheduler';
 import {getOrCreate} from '../base/utils';
+import {assertUnreachable} from '../base/logging';
+import {TimestampFormat} from '../public/timeline';
 
 const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
 
@@ -241,7 +243,7 @@
 
     const cb = (vizTime: HighPrecisionTimeSpan) => {
       this.trace.timeline.updateVisibleTimeHP(vizTime);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     };
     const pixelBounds = this.extractBounds(this.timeScale);
     const timeScale = this.timeScale;
@@ -299,15 +301,14 @@
     case TimestampFormat.Seconds:
       ctx.fillText(Time.formatSeconds(time), x, y, minWidth);
       break;
-    case TimestampFormat.Milliseoncds:
+    case TimestampFormat.Milliseconds:
       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}`);
+      assertUnreachable(fmt);
   }
 }
 
@@ -445,6 +446,6 @@
         this.overviewData.get(key)!.push(value);
       }
     }
-    raf.scheduleRedraw();
+    raf.scheduleCanvasRedraw();
   }
 }
diff --git a/ui/src/frontend/pan_and_zoom_handler.ts b/ui/src/frontend/pan_and_zoom_handler.ts
index 4536b9e..0009335 100644
--- a/ui/src/frontend/pan_and_zoom_handler.ts
+++ b/ui/src/frontend/pan_and_zoom_handler.ts
@@ -259,12 +259,12 @@
   private onWheel(e: WheelEvent) {
     if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
       this.onPanned(e.deltaX * HORIZONTAL_WHEEL_PAN_SPEED);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     } else if (e.ctrlKey && this.mousePositionX !== null) {
       const sign = e.deltaY < 0 ? -1 : 1;
       const deltaY = sign * Math.log2(1 + Math.abs(e.deltaY));
       this.onZoomed(this.mousePositionX, deltaY * WHEEL_ZOOM_SPEED);
-      raf.scheduleRedraw();
+      raf.scheduleCanvasRedraw();
     }
   }
 
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index ab6de73..760e098 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -16,13 +16,10 @@
 import {findRef, toHTMLElement} from '../base/dom_utils';
 import {assertExists, assertFalse} from '../base/logging';
 import {
-  PerfStatsSource,
-  RunningStatistics,
-  debugNow,
-  perfDebug,
-  perfDisplay,
+  PerfStats,
+  PerfStatsContainer,
   runningStatStr,
-} from '../core/perf';
+} from '../core/perf_stats';
 import {raf} from '../core/raf_scheduler';
 import {SimpleResizeObserver} from '../base/resize_observer';
 import {canvasClip} from '../base/canvas_utils';
@@ -94,7 +91,7 @@
 }
 
 export class PanelContainer
-  implements m.ClassComponent<PanelContainerAttrs>, PerfStatsSource
+  implements m.ClassComponent<PanelContainerAttrs>, PerfStatsContainer
 {
   private readonly trace: TraceImpl;
   private attrs: PanelContainerAttrs;
@@ -105,11 +102,12 @@
   // Updated every render cycle in the oncreate/onupdate hook
   private panelInfos: PanelInfo[] = [];
 
-  private panelPerfStats = new WeakMap<Panel, RunningStatistics>();
+  private perfStatsEnabled = false;
+  private panelPerfStats = new WeakMap<Panel, PerfStats>();
   private perfStats = {
     totalPanels: 0,
     panelsOnCanvas: 0,
-    renderStats: new RunningStatistics(10),
+    renderStats: new PerfStats(10),
   };
 
   private ctx?: CanvasRenderingContext2D;
@@ -122,16 +120,8 @@
   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);
-    });
+    this.trash.use(raf.addCanvasRedrawCallback(() => this.renderCanvas()));
+    this.trash.use(attrs.trace.perfDebugging.addContainer(this));
   }
 
   getPanelsInRegion(
@@ -352,7 +342,7 @@
 
     const ctx = this.ctx;
     const vc = this.virtualCanvas;
-    const redrawStart = debugNow();
+    const redrawStart = performance.now();
 
     ctx.resetTransform();
     ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
@@ -367,7 +357,7 @@
     this.drawTopLayerOnCanvas(ctx, vc);
 
     // Collect performance as the last thing we do.
-    const redrawDur = debugNow() - redrawStart;
+    const redrawDur = performance.now() - redrawStart;
     this.updatePerfStats(
       redrawDur,
       this.panelInfos.length,
@@ -407,12 +397,12 @@
         ctx.save();
         ctx.translate(0, panelTop);
         canvasClip(ctx, 0, 0, panelWidth, panelHeight);
-        const beforeRender = debugNow();
+        const beforeRender = performance.now();
         panel.renderCanvas(ctx, panelSize);
         this.updatePanelStats(
           i,
           panel,
-          debugNow() - beforeRender,
+          performance.now() - beforeRender,
           ctx,
           panelSize,
         );
@@ -505,10 +495,10 @@
     ctx: CanvasRenderingContext2D,
     size: Size2D,
   ) {
-    if (!perfDebug()) return;
+    if (!this.perfStatsEnabled) return;
     let renderStats = this.panelPerfStats.get(panel);
     if (renderStats === undefined) {
-      renderStats = new RunningStatistics();
+      renderStats = new PerfStats();
       this.panelPerfStats.set(panel, renderStats);
     }
     renderStats.addValue(renderTime);
@@ -537,12 +527,16 @@
     totalPanels: number,
     panelsOnCanvas: number,
   ) {
-    if (!perfDebug()) return;
+    if (!this.perfStatsEnabled) return;
     this.perfStats.renderStats.addValue(renderTime);
     this.perfStats.totalPanels = totalPanels;
     this.perfStats.panelsOnCanvas = panelsOnCanvas;
   }
 
+  setPerfStatsEnabled(enable: boolean): void {
+    this.perfStatsEnabled = enable;
+  }
+
   renderPerfStats() {
     return [
       m(
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
index 73c9a9d..b69916f 100644
--- a/ui/src/frontend/permalink.ts
+++ b/ui/src/frontend/permalink.ts
@@ -24,7 +24,7 @@
   MIME_BINARY,
   MIME_JSON,
   GcsUploader,
-} from '../common/gcs_uploader';
+} from '../base/gcs_uploader';
 import {
   SERIALIZED_STATE_VERSION,
   SerializedAppState,
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index a096140..734d2a0 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -34,14 +34,9 @@
   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 {DurationWidget} from '../public/lib/widgets/duration';
 import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
 import {assertExists, assertFalse} from '../base/logging';
 import {Filter, SqlColumn} from './widgets/sql/table/column';
@@ -49,6 +44,9 @@
 import {TraceImpl} from '../core/trace_impl';
 import {PivotTableManager} from '../core/pivot_table_manager';
 import {extensions} from '../public/lib/extensions';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+import {Button} from '../widgets/button';
+import {popupMenuIcon} from '../widgets/table';
 
 interface PathItem {
   tree: PivotTree;
@@ -293,15 +291,14 @@
     return m('tr', overallValuesRow);
   }
 
-  sortingItem(aggregationIndex: number, order: SortDirection): PopupMenuItem {
+  sortingItem(aggregationIndex: number, order: SortDirection): m.Child {
     const pivotMgr = this.pivotMgr;
-    return {
-      itemType: 'regular',
-      text: order === 'DESC' ? 'Highest first' : 'Lowest first',
-      callback() {
+    return m(MenuItem, {
+      label: order === 'DESC' ? 'Highest first' : 'Lowest first',
+      onclick: () => {
         pivotMgr.setSortColumn(aggregationIndex, order);
       },
-    };
+    });
   }
 
   readableAggregationName(aggregation: Aggregation) {
@@ -317,20 +314,21 @@
     aggregation: Aggregation,
     index: number,
     nameOverride?: string,
-  ): PopupMenuItem {
-    return {
-      itemType: 'regular',
-      text: nameOverride ?? readableColumnName(aggregation.column),
-      callback: () => this.pivotMgr.addAggregation(aggregation, index),
-    };
+  ): m.Child {
+    return m(MenuItem, {
+      label: nameOverride ?? readableColumnName(aggregation.column),
+      onclick: () => {
+        this.pivotMgr.addAggregation(aggregation, index);
+      },
+    });
   }
 
   aggregationPopupTableGroup(
     table: string,
     columns: string[],
     index: number,
-  ): PopupMenuItem | undefined {
-    const items = [];
+  ): m.Child | undefined {
+    const items: m.Child[] = [];
     for (const column of columns) {
       const tableColumn: TableColumn = {kind: 'regular', table, column};
       items.push(
@@ -345,12 +343,7 @@
       return undefined;
     }
 
-    return {
-      itemType: 'group',
-      itemId: `aggregations-${table}`,
-      text: `Add ${table} aggregation`,
-      children: items,
-    };
+    return m(MenuItem, {label: `Add ${table} aggregation`}, items);
   }
 
   renderAggregationHeaderCell(
@@ -358,7 +351,7 @@
     index: number,
     removeItem: boolean,
   ): ReorderableCell {
-    const popupItems: PopupMenuItem[] = [];
+    const popupItems: m.Child[] = [];
     if (aggregation.sortDirection === undefined) {
       popupItems.push(
         this.sortingItem(index, 'DESC'),
@@ -381,22 +374,26 @@
           continue;
         }
         const pivotMgr = this.pivotMgr;
-        popupItems.push({
-          itemType: 'regular',
-          text: otherAgg,
-          callback() {
-            pivotMgr.setAggregationFunction(index, otherAgg);
-          },
-        });
+        popupItems.push(
+          m(MenuItem, {
+            label: otherAgg,
+            onclick: () => {
+              pivotMgr.setAggregationFunction(index, otherAgg);
+            },
+          }),
+        );
       }
     }
 
     if (removeItem) {
-      popupItems.push({
-        itemType: 'regular',
-        text: 'Remove',
-        callback: () => this.pivotMgr.removeAggregation(index),
-      });
+      popupItems.push(
+        m(MenuItem, {
+          label: 'Remove',
+          onclick: () => {
+            this.pivotMgr.removeAggregation(index);
+          },
+        }),
+      );
     }
 
     let hasCount = false;
@@ -429,10 +426,15 @@
       extraClass: '.aggregation' + markFirst(index),
       content: [
         this.readableAggregationName(aggregation),
-        m(PopupMenuButton, {
-          icon: popupMenuIcon(aggregation.sortDirection),
-          items: popupItems,
-        }),
+        m(
+          PopupMenu2,
+          {
+            trigger: m(Button, {
+              icon: popupMenuIcon(aggregation.sortDirection),
+            }),
+          },
+          popupItems,
+        ),
       ],
     };
   }
@@ -445,27 +447,27 @@
     selectedPivots: Set<string>,
   ): ReorderableCell {
     const pivotMgr = this.pivotMgr;
-    const items: PopupMenuItem[] = [
-      {
-        itemType: 'regular',
-        text: 'Add argument pivot',
-        callback: () => {
+    const items: m.Child[] = [
+      m(MenuItem, {
+        label: 'Add argument pivot',
+        onclick: () => {
           this.attributeModalHolder.start();
         },
-      },
+      }),
     ];
     if (queryResult.metadata.pivotColumns.length > 1) {
-      items.push({
-        itemType: 'regular',
-        text: 'Remove',
-        callback() {
-          pivotMgr.setPivotSelected({column: pivot, selected: false});
-        },
-      });
+      items.push(
+        m(MenuItem, {
+          label: 'Remove',
+          onclick: () => {
+            pivotMgr.setPivotSelected({column: pivot, selected: false});
+          },
+        }),
+      );
     }
 
     for (const table of tables) {
-      const group: PopupMenuItem[] = [];
+      const group: m.Child[] = [];
       for (const columnName of table.columns) {
         const column: TableColumn = {
           kind: 'regular',
@@ -475,26 +477,30 @@
         if (selectedPivots.has(columnKey(column))) {
           continue;
         }
-        group.push({
-          itemType: 'regular',
-          text: columnName,
-          callback() {
-            pivotMgr.setPivotSelected({column, selected: true});
-          },
-        });
+        group.push(
+          m(MenuItem, {
+            label: columnName,
+            onclick: () => {
+              pivotMgr.setPivotSelected({column, selected: true});
+            },
+          }),
+        );
       }
-      items.push({
-        itemType: 'group',
-        itemId: `pivot-${table.name}`,
-        text: `Add ${table.displayName} pivot`,
-        children: group,
-      });
+      items.push(
+        m(
+          MenuItem,
+          {
+            label: `Add ${table.displayName} pivot`,
+          },
+          group,
+        ),
+      );
     }
 
     return {
       content: [
         readableColumnName(pivot),
-        m(PopupMenuButton, {icon: 'more_horiz', items}),
+        m(PopupMenu2, {trigger: m(Button, {icon: 'more_horiz'})}, items),
       ],
     };
   }
@@ -551,20 +557,20 @@
           }),
           m(
             'td.menu',
-            m(PopupMenuButton, {
-              icon: 'menu',
-              items: [
-                {
-                  itemType: 'regular',
-                  text: state.constrainToArea
-                    ? 'Query data for the whole timeline'
-                    : 'Constrain to selected area',
-                  callback: () => {
-                    this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
-                  },
+            m(
+              PopupMenu2,
+              {
+                trigger: m(Button, {icon: 'menu'}),
+              },
+              m(MenuItem, {
+                label: state.constrainToArea
+                  ? 'Query data for the whole timeline'
+                  : 'Constrain to selected area',
+                onclick: () => {
+                  this.pivotMgr.setConstrainedToArea(!state.constrainToArea);
                 },
-              ],
-            }),
+              }),
+            ),
           ),
         ),
       ),
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index 9f71bcc..9c03eb7 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {getCurrentChannel} from '../core/channels';
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../public/trace';
 import {
   disableMetatracingAndGetTrace,
   enableMetatracing,
@@ -352,7 +352,9 @@
 }
 
 export class Sidebar implements m.ClassComponent<OptionalTraceImplAttrs> {
-  private _redrawWhileAnimating = new Animation(() => raf.scheduleFullRedraw());
+  private _redrawWhileAnimating = new Animation(() =>
+    raf.scheduleFullRedraw('force'),
+  );
   private _asyncJobPending = new Set<string>();
   private _sectionExpanded = new Map<string, boolean>();
 
@@ -523,7 +525,7 @@
       raf.scheduleFullRedraw();
       res.finally(() => {
         this._asyncJobPending.delete(itemId);
-        raf.scheduleFullRedraw();
+        raf.scheduleFullRedraw('force');
       });
     };
   }
@@ -554,13 +556,6 @@
   // TODO(primiano): The Open file / Open with legacy entries are registered by
   // the 'perfetto.CoreCommands' plugins. Make things consistent.
   app.sidebar.addMenuItem({
-    section: 'navigation',
-    text: 'Record new trace',
-    href: '#!/record',
-    icon: 'fiber_smart_record',
-    sortOrder: 2,
-  });
-  app.sidebar.addMenuItem({
     section: 'support',
     text: 'Keyboard shortcuts',
     action: toggleHelp,
diff --git a/ui/src/frontend/sql_table_tab.ts b/ui/src/frontend/sql_table_tab.ts
index 40f2012..b803148 100644
--- a/ui/src/frontend/sql_table_tab.ts
+++ b/ui/src/frontend/sql_table_tab.ts
@@ -28,6 +28,12 @@
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {addEphemeralTab} from '../common/add_ephemeral_tab';
 import {Tab} from '../public/tab';
+import {addChartTab} from './widgets/charts/chart_tab';
+import {
+  ChartOption,
+  createChartConfigFromSqlTableState,
+} from './widgets/charts/chart';
+import {AddChartMenuItem} from './widgets/charts/add_chart_menu';
 
 export interface AddSqlTableTabParams {
   table: SqlTableDescription;
@@ -122,6 +128,16 @@
       },
       m(SqlTable, {
         state: this.state,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              this.state,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => addChartTab(chart),
+          }),
       }),
     );
   }
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 0662ef8..04aee7e 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -163,7 +163,7 @@
         /* onDrag */ (_x, y) => {
           const deltaYSinceDragStart = dragStartY - y;
           this.resizableHeight = heightWhenDragStarted + deltaYSinceDragStart;
-          raf.scheduleFullRedraw();
+          raf.scheduleFullRedraw('force');
         },
         /* onDragStarted */ (_x, y) => {
           this.resizableHeight = this.height;
diff --git a/ui/src/frontend/thread_slice_details_tab.ts b/ui/src/frontend/thread_slice_details_tab.ts
index b549971..b872a6d 100644
--- a/ui/src/frontend/thread_slice_details_tab.ts
+++ b/ui/src/frontend/thread_slice_details_tab.ts
@@ -25,14 +25,14 @@
 import {Tree} from '../widgets/tree';
 import {Flow, FlowPoint} from '../core/flow_types';
 import {hasArgs, renderArguments} from './slice_args';
-import {renderDetails} from './slice_details';
+import {renderDetails} from '../public/lib/details/slice_details';
 import {getSlice, SliceDetails} from '../trace_processor/sql_utils/slice';
 import {
   BreakdownByThreadState,
   breakDownIntervalByThreadState,
-} from './sql/thread_state';
+} from '../public/lib/details/thread_state';
 import {asSliceSqlId} from '../trace_processor/sql_utils/core_types';
-import {DurationWidget} from './widgets/duration';
+import {DurationWidget} from '../public/lib/widgets/duration';
 import {SliceRef} from './widgets/slice';
 import {BasicTable} from '../widgets/basic_table';
 import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
diff --git a/ui/src/frontend/thread_slice_track.ts b/ui/src/frontend/thread_slice_track.ts
index 6e47c8a..10a829f 100644
--- a/ui/src/frontend/thread_slice_track.ts
+++ b/ui/src/frontend/thread_slice_track.ts
@@ -16,13 +16,13 @@
 import {clamp} from '../base/math_utils';
 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 '../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';
+import {Trace} from '../public/trace';
 
 export const THREAD_SLICE_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -35,12 +35,13 @@
 
 export class ThreadSliceTrack extends NamedSliceTrack<Slice, ThreadSliceRow> {
   constructor(
-    args: NewTrackArgs,
-    private trackId: number,
+    trace: Trace,
+    uri: string,
+    private readonly trackId: number,
     maxDepth: number,
-    private tableName: string = 'slice',
+    private readonly tableName: string = 'slice',
   ) {
-    super(args);
+    super(trace, uri);
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index 143e402..db7cf9c 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {Time, time, toISODateOnly} from '../base/time';
-import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
+import {timestampFormat} from '../core/timestamp_format';
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {
   getMaxMajorTicks,
@@ -27,6 +27,8 @@
 import {TimeScale} from '../base/time_scale';
 import {canvasClip} from '../base/canvas_utils';
 import {Trace} from '../public/trace';
+import {assertUnreachable} from '../base/logging';
+import {TimestampFormat} from '../public/timeline';
 
 export class TimeAxisPanel implements Panel {
   readonly kind = 'panel';
@@ -58,11 +60,14 @@
 
   private renderOffsetTimestamp(ctx: CanvasRenderingContext2D): void {
     const offset = this.trace.timeline.timestampOffset();
-    switch (timestampFormat()) {
+    const timestampFormat = this.trace.timeline.timestampFormat;
+    switch (timestampFormat) {
       case TimestampFormat.TraceNs:
       case TimestampFormat.TraceNsLocale:
         break;
       case TimestampFormat.Seconds:
+      case TimestampFormat.Milliseconds:
+      case TimestampFormat.Microseconds:
       case TimestampFormat.Timecode:
         const width = renderTimestamp(ctx, offset, 6, 10, MIN_PX_PER_STEP);
         ctx.fillText('+', 6 + width + 2, 10, 6);
@@ -83,6 +88,8 @@
         const dateTzStr = toISODateOnly(offsetTzDate);
         ctx.fillText(dateTzStr, 6, 10);
         break;
+      default:
+        assertUnreachable(timestampFormat);
     }
   }
 
@@ -130,7 +137,7 @@
       return renderRawTimestamp(ctx, time.toLocaleString(), x, y, minWidth);
     case TimestampFormat.Seconds:
       return renderRawTimestamp(ctx, Time.formatSeconds(time), x, y, minWidth);
-    case TimestampFormat.Milliseoncds:
+    case TimestampFormat.Milliseconds:
       return renderRawTimestamp(
         ctx,
         Time.formatMilliseconds(time),
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index 5621ff4..599ed5b 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {time, Time} from '../base/time';
-import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
+import {timestampFormat} from '../core/timestamp_format';
 import {
   BACKGROUND_COLOR,
   FOREGROUND_COLOR,
@@ -23,10 +23,12 @@
 import {getMaxMajorTicks, generateTicks, TickType} from './gridline_helper';
 import {Size2D} from '../base/geom';
 import {Panel} from './panel_container';
-import {renderDuration} from './widgets/duration';
 import {canvasClip} from '../base/canvas_utils';
 import {TimeScale} from '../base/time_scale';
 import {TraceImpl} from '../core/trace_impl';
+import {formatDuration} from '../public/lib/time_utils';
+import {TimestampFormat} from '../public/timeline';
+import {assertUnreachable} from '../base/logging';
 
 export interface BBox {
   x: number;
@@ -235,7 +237,7 @@
   ) {
     const xLeft = timescale.timeToPx(start);
     const xRight = timescale.timeToPx(end);
-    const label = renderDuration(end - start);
+    const label = formatDuration(this.trace, end - start);
     drawHBar(
       ctx,
       {
@@ -273,12 +275,11 @@
       return time.toLocaleString();
     case TimestampFormat.Seconds:
       return Time.formatSeconds(time);
-    case TimestampFormat.Milliseoncds:
+    case TimestampFormat.Milliseconds:
       return Time.formatMilliseconds(time);
     case TimestampFormat.Microseconds:
       return Time.formatMicroseconds(time);
     default:
-      const z: never = fmt;
-      throw new Error(`Invalid timestamp ${z}`);
+      assertUnreachable(fmt);
   }
 }
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
deleted file mode 100644
index f10e21e..0000000
--- a/ui/src/frontend/track.ts
+++ /dev/null
@@ -1,20 +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 {Trace} from '../public/trace';
-
-export interface NewTrackArgs {
-  uri: string;
-  trace: Trace;
-}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 7814674..fcae30c 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -133,15 +133,15 @@
           ...pos,
           timescale,
         });
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       onTrackContentMouseOut: () => {
         trackRenderer?.track.onMouseOut?.();
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       onTrackContentClick: (pos, bounds) => {
         const timescale = this.getTimescaleForBounds(bounds);
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
         return (
           trackRenderer?.track.onMouseClick?.({
             ...pos,
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 7d3caa3..c157267 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -14,11 +14,11 @@
 
 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/track';
 import {AsyncDisposableStack} from '../../base/disposable_stack';
 import {sqlNameSafe} from '../../base/string_utils';
+import {Trace} from '../../public/trace';
 
 export interface CustomSqlImportConfig {
   modules: string[];
@@ -39,9 +39,9 @@
 > {
   protected readonly tableName;
 
-  constructor(args: NewTrackArgs) {
-    super(args);
-    this.tableName = `customsqltableslicetrack_${sqlNameSafe(args.uri)}`;
+  constructor(trace: Trace, uri: string) {
+    super(trace, uri);
+    this.tableName = `customsqltableslicetrack_${sqlNameSafe(uri)}`;
   }
 
   getRowSpec(): NamedRow {
diff --git a/ui/src/frontend/ui_main.ts b/ui/src/frontend/ui_main.ts
index 954da1a..b28a41a 100644
--- a/ui/src/frontend/ui_main.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -19,10 +19,8 @@
 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';
@@ -46,15 +44,16 @@
 import {NotesEditorTab} from './notes_panel';
 import {NotesListEditor} from './notes_list_editor';
 import {getTimeSpanOfSelectionOrVisibleWindow} from '../public/utils';
+import {DurationPrecision, TimestampFormat} from '../public/timeline';
 
 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) {
+  view() {
     const currentTraceId = AppImpl.instance.trace?.engine.engineId ?? '';
-    return [m(UiMainPerTrace, {key: currentTraceId}, children)];
+    return [m(UiMainPerTrace, {key: currentTraceId})];
   }
 }
 
@@ -132,7 +131,7 @@
               displayName: 'Realtime (Trace TZ)',
             },
             {key: TimestampFormat.Seconds, displayName: 'Seconds'},
-            {key: TimestampFormat.Milliseoncds, displayName: 'Milliseconds'},
+            {key: TimestampFormat.Milliseconds, displayName: 'Milliseconds'},
             {key: TimestampFormat.Microseconds, displayName: 'Microseconds'},
             {key: TimestampFormat.TraceNs, displayName: 'Trace nanoseconds'},
             {
@@ -171,7 +170,8 @@
       {
         id: 'perfetto.TogglePerformanceMetrics',
         name: 'Toggle performance metrics',
-        callback: () => app.setPerfDebuggingEnabled(!app.perfDebugging),
+        callback: () =>
+          (app.perfDebugging.enabled = !app.perfDebugging.enabled),
       },
       {
         id: 'perfetto.ShareTrace',
@@ -628,12 +628,13 @@
     this.maybeFocusOmnibar();
   }
 
-  view({children}: m.Vnode): m.Children {
+  view(): m.Children {
+    const app = AppImpl.instance;
     const hotkeys: HotkeyConfig[] = [];
-    for (const {id, defaultHotkey} of AppImpl.instance.commands.commands) {
+    for (const {id, defaultHotkey} of app.commands.commands) {
       if (defaultHotkey) {
         hotkeys.push({
-          callback: () => AppImpl.instance.commands.runCommand(id),
+          callback: () => app.commands.runCommand(id),
           hotkey: defaultHotkey,
         });
       }
@@ -649,10 +650,10 @@
           omnibox: this.renderOmnibox(),
           trace: this.trace,
         }),
-        children,
+        app.pages.renderPageForCurrentRoute(app.trace),
         m(CookieConsent),
         maybeRenderFullscreenModalDialog(),
-        AppImpl.instance.perfDebugging && m('.perf-stats'),
+        app.perfDebugging.renderPerfStats(),
       ),
     );
   }
diff --git a/ui/src/frontend/value.ts b/ui/src/frontend/value.ts
index 40ad1f4..a57f2ea 100644
--- a/ui/src/frontend/value.ts
+++ b/ui/src/frontend/value.ts
@@ -14,7 +14,8 @@
 
 import m from 'mithril';
 import {Tree, TreeNode} from '../widgets/tree';
-import {PopupMenuButton, PopupMenuItem} from '../widgets/popup_menu';
+import {PopupMenu2} from '../widgets/menu';
+import {Button} from '../widgets/button';
 
 // This file implements a component for rendering JSON-like values (with
 // customisation options like context menu and action buttons).
@@ -109,7 +110,7 @@
 
 // Customisation parameters which apply to any Value (e.g. context menu).
 interface ValueParams {
-  contextMenu?: PopupMenuItem[];
+  contextMenu?: m.Child[];
 }
 
 // Customisation parameters which apply for a primitive value (e.g. showing
@@ -137,10 +138,15 @@
   const left = [
     name,
     value.contextMenu
-      ? m(PopupMenuButton, {
-          icon: 'arrow_drop_down',
-          items: value.contextMenu,
-        })
+      ? m(
+          PopupMenu2,
+          {
+            trigger: m(Button, {
+              icon: 'arrow_drop_down',
+            }),
+          },
+          value.contextMenu,
+        )
       : null,
   ];
   if (isArray(value)) {
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 75f2aba..9509c1d 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -145,7 +145,7 @@
         const rect = dom.getBoundingClientRect();
         const centerPoint = zoomPx / (rect.width - TRACK_SHELL_WIDTH);
         timeline.zoomVisibleWindow(1 - zoomRatio, centerPoint);
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       editSelection: (currentPx: number) => {
         if (this.timelineWidthPx === undefined) return false;
@@ -257,7 +257,7 @@
           }
           this.showPanningHint = true;
         }
-        raf.scheduleRedraw();
+        raf.scheduleCanvasRedraw();
       },
       endSelection: (edit: boolean) => {
         this.selectedContainer = undefined;
diff --git a/ui/src/frontend/visualized_args_track.ts b/ui/src/frontend/visualized_args_track.ts
index fdea5b3..4b52b9d 100644
--- a/ui/src/frontend/visualized_args_track.ts
+++ b/ui/src/frontend/visualized_args_track.ts
@@ -46,7 +46,7 @@
     const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
     const viewName = `__arg_visualisation_helper_${escapedArgName}_${uuid}_slice`;
 
-    super({trace, uri}, trackId, maxDepth, viewName);
+    super(trace, uri, trackId, maxDepth, viewName);
     this.viewName = viewName;
     this.argName = argName;
     this.onClose = onClose;
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..cb3bb17
--- /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 {Chart, ChartConfig, ChartOption, toTitleCase} from './chart';
+
+interface AddChartMenuItemAttrs {
+  readonly chartConfig: ChartConfig;
+  readonly chartOptions: Array<ChartOption>;
+  readonly addChart: (chart: Chart) => void;
+}
+
+export class AddChartMenuItem
+  implements m.ClassComponent<AddChartMenuItemAttrs>
+{
+  private renderAddChartOptions(
+    config: ChartConfig,
+    chartOptions: Array<ChartOption>,
+    addChart: (chart: Chart) => 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
index 1a78c41..124a52e 100644
--- a/ui/src/frontend/widgets/charts/chart.ts
+++ b/ui/src/frontend/widgets/charts/chart.ts
@@ -11,9 +11,13 @@
 // WITHOUT WARRANTIES 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 {TableColumn, TableColumnSet} from '../sql/table/column';
+import {Filter, TableColumn, TableColumnSet} from '../sql/table/column';
+import {Histogram} from './histogram/histogram';
+import {SqlTableState} from '../sql/table/state';
+import {columnTitle} from '../sql/table/table';
 
 export interface VegaLiteChartSpec {
   $schema: string;
@@ -41,6 +45,26 @@
   };
 }
 
+// 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 Chart {
+  readonly option: ChartOption;
+  readonly config: ChartConfig;
+}
+
 export interface ChartData {
   readonly rows: Row[];
   readonly error?: string;
@@ -65,3 +89,32 @@
 
   return words.join(' ');
 }
+
+// renderChartComponent will take a chart option and config and map
+// to the corresponding chart class component.
+export function renderChartComponent(chart: Chart) {
+  switch (chart.option) {
+    case ChartOption.HISTOGRAM:
+      return m(Histogram, chart.config);
+    default:
+      return;
+  }
+}
+
+export function createChartConfigFromSqlTableState(
+  column: TableColumn,
+  columnAlias: string,
+  sqlTableState: SqlTableState,
+) {
+  return {
+    engine: sqlTableState.trace.engine,
+    columnTitle: columnTitle(column),
+    sqlColumn: [columnAlias],
+    filters: sqlTableState?.getFilters(),
+    tableDisplay: sqlTableState.config.displayName ?? sqlTableState.config.name,
+    query: sqlTableState.getSqlQuery(
+      Object.fromEntries([[columnAlias, column.primaryColumn()]]),
+    ),
+    aggregationType: column.aggregation?.().dataType,
+  };
+}
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..6d802e6
--- /dev/null
+++ b/ui/src/frontend/widgets/charts/chart_tab.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 {DetailsShell} from '../../../widgets/details_shell';
+import {filterTitle} from '../sql/table/column';
+import {addEphemeralTab} from '../../../common/add_ephemeral_tab';
+import {Tab} from '../../../public/tab';
+import {Chart, renderChartComponent, toTitleCase} from './chart';
+
+export function addChartTab(chart: Chart): void {
+  addEphemeralTab('histogramTab', new ChartTab(chart));
+}
+
+export class ChartTab implements Tab {
+  constructor(private readonly chart: Chart) {}
+
+  render() {
+    return m(
+      DetailsShell,
+      {
+        title: this.getTitle(),
+        description: this.getDescription(),
+      },
+      renderChartComponent(this.chart),
+    );
+  }
+
+  getTitle(): string {
+    return `${toTitleCase(this.chart.config.columnTitle)} Histogram`;
+  }
+
+  private getDescription(): string {
+    let desc = `Count distribution for ${this.chart.config.tableDisplay ?? ''} table`;
+
+    if (this.chart.config.filters && this.chart.config.filters.length > 0) {
+      desc += ' where ';
+      desc += this.chart.config.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
index e4dd0d0..38f4a65 100644
--- a/ui/src/frontend/widgets/charts/histogram/histogram.ts
+++ b/ui/src/frontend/widgets/charts/histogram/histogram.ts
@@ -15,25 +15,14 @@
 import m from 'mithril';
 import {stringifyJsonWithBigints} from '../../../../base/json_utils';
 import {VegaView} from '../../../../widgets/vega_view';
-import {Filter} from '../../../widgets/sql/table/column';
 import {HistogramState} from './state';
 import {Spinner} from '../../../../widgets/spinner';
-import {Engine} from '../../../../trace_processor/engine';
+import {ChartConfig} from '../chart';
 
-export interface HistogramConfig {
-  engine: Engine;
-  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
-  aggregationType?: 'nominal' | 'quantitative'; // Aggregation type.
-}
-
-export class Histogram implements m.ClassComponent<HistogramConfig> {
+export class Histogram implements m.ClassComponent<ChartConfig> {
   private readonly state: HistogramState;
 
-  constructor({attrs}: m.Vnode<HistogramConfig>) {
+  constructor({attrs}: m.Vnode<ChartConfig>) {
     this.state = new HistogramState(
       attrs.engine,
       attrs.query,
diff --git a/ui/src/frontend/widgets/charts/histogram/tab.ts b/ui/src/frontend/widgets/charts/histogram/tab.ts
deleted file mode 100644
index 096245e..0000000
--- a/ui/src/frontend/widgets/charts/histogram/tab.ts
+++ /dev/null
@@ -1,55 +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 {filterTitle} from '../../../widgets/sql/table/column';
-import {addEphemeralTab} from '../../../../common/add_ephemeral_tab';
-import {Tab} from '../../../../public/tab';
-import {Histogram, HistogramConfig} from './histogram';
-import {toTitleCase} from '../chart';
-
-export function addHistogramTab(config: HistogramConfig): void {
-  addEphemeralTab('histogramTab', new HistogramTab(config));
-}
-
-export class HistogramTab implements Tab {
-  constructor(private readonly config: HistogramConfig) {}
-
-  render() {
-    return m(
-      DetailsShell,
-      {
-        title: this.getTitle(),
-        description: this.getDescription(),
-      },
-      m(Histogram, this.config),
-    );
-  }
-
-  getTitle(): string {
-    return `${toTitleCase(this.config.columnTitle)} Histogram`;
-  }
-
-  private getDescription(): string {
-    let desc = `Count distribution for ${this.config.tableDisplay ?? ''} table`;
-
-    if (this.config.filters && this.config.filters.length > 0) {
-      desc += ' where ';
-      desc += this.config.filters.map((f) => filterTitle(f)).join(', ');
-    }
-
-    return desc;
-  }
-}
diff --git a/ui/src/frontend/widgets/duration.ts b/ui/src/frontend/widgets/duration.ts
deleted file mode 100644
index a623406..0000000
--- a/ui/src/frontend/widgets/duration.ts
+++ /dev/null
@@ -1,145 +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 {Icons} from '../../base/semantic_icons';
-import {Duration, duration} from '../../base/time';
-import {
-  DurationPrecision,
-  durationPrecision,
-  setDurationPrecision,
-  TimestampFormat,
-  timestampFormat,
-} from '../../core/timestamp_format';
-import {raf} from '../../core/raf_scheduler';
-import {Anchor} from '../../widgets/anchor';
-import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
-import {menuItemForFormat} from './timestamp';
-
-interface DurationWidgetAttrs {
-  dur: duration;
-  extraMenuItems?: m.Child[];
-}
-
-export class DurationWidget implements m.ClassComponent<DurationWidgetAttrs> {
-  view({attrs}: m.Vnode<DurationWidgetAttrs>) {
-    const {dur} = attrs;
-    if (dur === -1n) {
-      return '(Did not end)';
-    }
-    return m(
-      PopupMenu2,
-      {
-        trigger: m(Anchor, renderDuration(dur)),
-      },
-      m(MenuItem, {
-        icon: Icons.Copy,
-        label: `Copy raw value`,
-        onclick: () => {
-          copyToClipboard(dur.toString());
-        },
-      }),
-      m(
-        MenuItem,
-        {
-          label: 'Set time format',
-        },
-        menuItemForFormat(TimestampFormat.Timecode, 'Timecode'),
-        menuItemForFormat(TimestampFormat.UTC, 'Realtime (UTC)'),
-        menuItemForFormat(TimestampFormat.TraceTz, 'Realtime (Trace TZ)'),
-        menuItemForFormat(TimestampFormat.Seconds, 'Seconds'),
-        menuItemForFormat(TimestampFormat.Milliseoncds, 'Milliseconds'),
-        menuItemForFormat(TimestampFormat.Microseconds, 'Microseconds'),
-        menuItemForFormat(TimestampFormat.TraceNs, 'Raw'),
-        menuItemForFormat(
-          TimestampFormat.TraceNsLocale,
-          'Raw (with locale-specific formatting)',
-        ),
-      ),
-      m(
-        MenuItem,
-        {
-          label: 'Duration precision',
-          disabled: !durationPrecisionHasEffect(),
-          title: 'Not configurable with current time format',
-        },
-        menuItemForPrecision(DurationPrecision.Full, 'Full'),
-        menuItemForPrecision(DurationPrecision.HumanReadable, 'Human readable'),
-      ),
-      attrs.extraMenuItems ? [m(MenuDivider), attrs.extraMenuItems] : null,
-    );
-  }
-}
-
-function menuItemForPrecision(
-  value: DurationPrecision,
-  label: string,
-): m.Children {
-  return m(MenuItem, {
-    label,
-    active: value === durationPrecision(),
-    onclick: () => {
-      setDurationPrecision(value);
-      raf.scheduleFullRedraw();
-    },
-  });
-}
-
-function durationPrecisionHasEffect(): boolean {
-  switch (timestampFormat()) {
-    case TimestampFormat.Timecode:
-    case TimestampFormat.UTC:
-    case TimestampFormat.TraceTz:
-      return true;
-    default:
-      return false;
-  }
-}
-
-export function renderDuration(dur: duration): string {
-  const fmt = timestampFormat();
-  switch (fmt) {
-    case TimestampFormat.UTC:
-    case TimestampFormat.TraceTz:
-    case TimestampFormat.Timecode:
-      return renderFormattedDuration(dur);
-    case TimestampFormat.TraceNs:
-      return dur.toString();
-    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}`);
-  }
-}
-
-function renderFormattedDuration(dur: duration): string {
-  const fmt = durationPrecision();
-  switch (fmt) {
-    case DurationPrecision.HumanReadable:
-      return Duration.humanise(dur);
-    case DurationPrecision.Full:
-      return Duration.format(dur);
-    default:
-      const x: never = fmt;
-      throw new Error(`Invalid format ${x}`);
-  }
-}
diff --git a/ui/src/frontend/widgets/sql/details/details.ts b/ui/src/frontend/widgets/sql/details/details.ts
index 069b723..b05ed41 100644
--- a/ui/src/frontend/widgets/sql/details/details.ts
+++ b/ui/src/frontend/widgets/sql/details/details.ts
@@ -30,8 +30,8 @@
 import {SqlRef} from '../../../../widgets/sql_ref';
 import {Tree, TreeNode} from '../../../../widgets/tree';
 import {hasArgs, renderArguments} from '../../../slice_args';
-import {DurationWidget} from '../../../widgets/duration';
-import {Timestamp as TimestampWidget} from '../../../widgets/timestamp';
+import {DurationWidget} from '../../../../public/lib/widgets/duration';
+import {Timestamp as TimestampWidget} from '../../../../public/lib/widgets/timestamp';
 import {sqlIdRegistry} from './sql_ref_renderer_registry';
 import {Trace} from '../../../../public/trace';
 
diff --git a/ui/src/frontend/widgets/sql/table/state.ts b/ui/src/frontend/widgets/sql/table/state.ts
index 1f513b8..4540214 100644
--- a/ui/src/frontend/widgets/sql/table/state.ts
+++ b/ui/src/frontend/widgets/sql/table/state.ts
@@ -331,8 +331,15 @@
       this.rowCount = undefined;
     }
 
-    // Run a delayed UI update to avoid flickering if the query returns quickly.
-    raf.scheduleDelayedFullRedraw();
+    // Schedule a full redraw to happen after a short delay (50 ms).
+    // This is done to prevent flickering / visual noise and allow the UI to fetch
+    // the initial data from the Trace Processor.
+    // There is a chance that someone else schedules a full redraw in the
+    // meantime, forcing the flicker, but in practice it works quite well and
+    // avoids a lot of complexity for the callers.
+    // 50ms is half of the responsiveness threshold (100ms):
+    // https://web.dev/rail/#response-process-events-in-under-50ms
+    setTimeout(() => raf.scheduleFullRedraw(), 50);
 
     if (!filtersMatch) {
       this.rowCount = await this.loadRowCount();
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index acd5ffe..761a32c 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -40,14 +40,20 @@
 import {SqlTableState} from './state';
 import {SqlTableDescription} from './table_description';
 import {Intent} from '../../../../widgets/common';
-import {addHistogramTab} from '../../charts/histogram/tab';
 import {Form} from '../../../../widgets/form';
 import {TextInput} from '../../../../widgets/text_input';
 
 export interface SqlTableConfig {
   readonly state: SqlTableState;
+  // For additional menu items to add to the column header menus
+  readonly addColumnMenuItems?: (
+    column: TableColumn,
+    columnAlias: string,
+  ) => m.Children;
 }
 
+type AdditionalColumnMenuItems = Record<string, m.Children>;
+
 function renderCell(
   column: TableColumn,
   row: Row,
@@ -65,7 +71,7 @@
   return column.renderCell(sqlValue, getTableManager(state), additionalValues);
 }
 
-function columnTitle(column: TableColumn): string {
+export function columnTitle(column: TableColumn): string {
   if (column.getTitle !== undefined) {
     const title = column.getTitle();
     if (title !== undefined) return title;
@@ -274,7 +280,11 @@
     );
   }
 
-  renderColumnHeader(column: TableColumn, index: number) {
+  renderColumnHeader(
+    column: TableColumn,
+    index: number,
+    additionalColumnHeaderMenuItems?: m.Children,
+  ) {
     const sorted = this.state.isSortedBy(column);
     const icon =
       sorted === 'ASC'
@@ -327,27 +337,7 @@
         {label: 'Add filter', icon: Icons.Filter},
         this.renderColumnFilterOptions(column),
       ),
-      m(MenuItem, {
-        label: 'Create histogram',
-        icon: Icons.Chart,
-        onclick: () => {
-          const columnAlias =
-            this.state.getCurrentRequest().columns[
-              sqlColumnId(column.primaryColumn())
-            ];
-          addHistogramTab({
-            engine: this.state.trace.engine,
-            sqlColumn: [columnAlias],
-            columnTitle: columnTitle(column),
-            filters: this.state.getFilters(),
-            tableDisplay: this.table.displayName ?? this.table.name,
-            query: this.state.getSqlQuery(
-              Object.fromEntries([[columnAlias, column.primaryColumn()]]),
-            ),
-            aggregationType: column.aggregation?.().dataType,
-          });
-        },
-      }),
+      additionalColumnHeaderMenuItems,
       // Menu items before divider apply to selected column
       m(MenuDivider),
       // Menu items after divider apply to entire table
@@ -355,13 +345,49 @@
     );
   }
 
-  view() {
+  getAdditionalColumnMenuItems(
+    addColumnMenuItems?: (
+      column: TableColumn,
+      columnAlias: string,
+    ) => m.Children,
+  ) {
+    if (addColumnMenuItems === undefined) return;
+
+    const additionalColumnMenuItems: AdditionalColumnMenuItems = {};
+    this.state.getSelectedColumns().forEach((column) => {
+      const columnAlias =
+        this.state.getCurrentRequest().columns[
+          sqlColumnId(column.primaryColumn())
+        ];
+
+      additionalColumnMenuItems[columnAlias] = addColumnMenuItems(
+        column,
+        columnAlias,
+      );
+    });
+
+    return additionalColumnMenuItems;
+  }
+
+  view({attrs}: m.Vnode<SqlTableConfig>) {
     const rows = this.state.getDisplayedRows();
+    const additionalColumnMenuItems = this.getAdditionalColumnMenuItems(
+      attrs.addColumnMenuItems,
+    );
 
     const columns = this.state.getSelectedColumns();
     const columnDescriptors = columns.map((column, i) => {
       return {
-        title: this.renderColumnHeader(column, i),
+        title: this.renderColumnHeader(
+          column,
+          i,
+          additionalColumnMenuItems &&
+            additionalColumnMenuItems[
+              this.state.getCurrentRequest().columns[
+                sqlColumnId(column.primaryColumn())
+              ]
+            ],
+        ),
         render: (row: Row) => renderCell(column, row, this.state),
       };
     });
diff --git a/ui/src/frontend/widgets/sql/table/well_known_columns.ts b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
index 7c1eb98..8ed593e 100644
--- a/ui/src/frontend/widgets/sql/table/well_known_columns.ts
+++ b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
@@ -29,13 +29,13 @@
 import {Anchor} from '../../../../widgets/anchor';
 import {renderError} from '../../../../widgets/error';
 import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
-import {DurationWidget} from '../../duration';
+import {DurationWidget} from '../../../../public/lib/widgets/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 {Timestamp} from '../../../../public/lib/widgets/timestamp';
 import {
   AggregationConfig,
   SourceTable,
diff --git a/ui/src/frontend/widgets/timestamp.ts b/ui/src/frontend/widgets/timestamp.ts
deleted file mode 100644
index 7fd44f1..0000000
--- a/ui/src/frontend/widgets/timestamp.ts
+++ /dev/null
@@ -1,145 +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 {Icons} from '../../base/semantic_icons';
-import {time, Time} from '../../base/time';
-import {
-  setTimestampFormat,
-  TimestampFormat,
-  timestampFormat,
-} from '../../core/timestamp_format';
-import {raf} from '../../core/raf_scheduler';
-import {Anchor} from '../../widgets/anchor';
-import {MenuDivider, MenuItem, PopupMenu2} from '../../widgets/menu';
-import {Trace} from '../../public/trace';
-import {AppImpl} from '../../core/app_impl';
-import {assertExists} from '../../base/logging';
-
-// import {MenuItem, PopupMenu2} from './menu';
-
-interface TimestampAttrs {
-  // The timestamp to print, this should be the absolute, raw timestamp as
-  // found in trace processor.
-  ts: time;
-  // Custom text value to show instead of the default HH:MM:SS.mmm uuu nnn
-  // formatting.
-  display?: m.Children;
-  extraMenuItems?: m.Child[];
-}
-
-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: () => (timeline.hoverCursorTimestamp = ts),
-            onmouseout: () => (timeline.hoverCursorTimestamp = undefined),
-          },
-          attrs.display ?? renderTimestamp(timeline.toDomainTime(ts)),
-        ),
-      },
-      m(MenuItem, {
-        icon: Icons.Copy,
-        label: `Copy raw value`,
-        onclick: () => {
-          copyToClipboard(ts.toString());
-        },
-      }),
-      m(
-        MenuItem,
-        {
-          label: 'Time format',
-        },
-        menuItemForFormat(TimestampFormat.Timecode, 'Timecode'),
-        menuItemForFormat(TimestampFormat.UTC, 'Realtime (UTC)'),
-        menuItemForFormat(TimestampFormat.TraceTz, 'Realtime (Trace TZ)'),
-        menuItemForFormat(TimestampFormat.Seconds, 'Seconds'),
-        menuItemForFormat(TimestampFormat.Milliseoncds, 'Milliseconds'),
-        menuItemForFormat(TimestampFormat.Microseconds, 'Microseconds'),
-        menuItemForFormat(TimestampFormat.TraceNs, 'Raw'),
-        menuItemForFormat(
-          TimestampFormat.TraceNsLocale,
-          'Raw (with locale-specific formatting)',
-        ),
-      ),
-      attrs.extraMenuItems ? [m(MenuDivider), attrs.extraMenuItems] : null,
-    );
-  }
-}
-
-export function menuItemForFormat(
-  value: TimestampFormat,
-  label: string,
-): m.Children {
-  return m(MenuItem, {
-    label,
-    active: value === timestampFormat(),
-    onclick: () => {
-      setTimestampFormat(value);
-      raf.scheduleFullRedraw();
-    },
-  });
-}
-
-function renderTimestamp(domainTime: time): m.Children {
-  const fmt = timestampFormat();
-  switch (fmt) {
-    case TimestampFormat.UTC:
-    case TimestampFormat.TraceTz:
-    case TimestampFormat.Timecode:
-      return renderTimecode(domainTime);
-    case TimestampFormat.TraceNs:
-      return domainTime.toString();
-    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}`);
-  }
-}
-
-export function renderTimecode(time: time): m.Children {
-  const {dhhmmss, millis, micros, nanos} = Time.toTimecode(time);
-  return m(
-    'span.pf-timecode',
-    m('span.pf-timecode-hms', dhhmmss),
-    '.',
-    m('span.pf-timecode-millis', millis),
-    m('span.pf-timecode-micros', micros),
-    m('span.pf-timecode-nanos', nanos),
-  );
-}
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/dev.perfetto.AndroidLog/logs_panel.ts b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
index 48714ba..5c79fdd 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLog/logs_panel.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 import {time, Time, TimeSpan} from '../../base/time';
 import {DetailsShell} from '../../widgets/details_shell';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
 import {Monitor} from '../../base/monitor';
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
index 1bb31a5..0d55ff1 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/async_slice_track.ts
@@ -16,10 +16,17 @@
 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 {Trace} from '../../public/trace';
 import {Slice} from '../../public/track';
-import {LONG_NULL} from '../../trace_processor/query_result';
+import {SourceDataset, Dataset} from '../../trace_processor/dataset';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../trace_processor/query_result';
 
 export const THREAD_SLICE_ROW = {
   // Base columns (tsq, ts, dur, id, depth).
@@ -32,11 +39,12 @@
 
 export class AsyncSliceTrack extends NamedSliceTrack<Slice, ThreadSliceRow> {
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     maxDepth: number,
     private readonly trackIds: number[],
   ) {
-    super(args);
+    super(trace, uri);
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
@@ -104,4 +112,21 @@
       tableName: 'slice',
     };
   }
+
+  override getDataset(): Dataset {
+    return new SourceDataset({
+      src: `slice`,
+      filter: {
+        col: 'track_id',
+        in: this.trackIds,
+      },
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+        parent_id: NUM_NULL,
+      },
+    });
+  }
 }
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
index 9b0c6e7..ecd8dab 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
@@ -197,7 +197,7 @@
             kind: SLICE_TRACK_KIND,
             scope: 'global',
           },
-          track: new AsyncSliceTrack({trace: ctx, uri}, maxDepth, trackIds),
+          track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
         });
         const trackNode = new TrackNode({
           uri,
@@ -283,7 +283,7 @@
           scope: 'process',
           upid,
         },
-        track: new AsyncSliceTrack({trace: ctx, uri}, maxDepth, trackIds),
+        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
       });
       const track = new TrackNode({uri, title, sortOrder: 30});
       trackIds.forEach((id) => {
@@ -384,7 +384,7 @@
         chips: removeFalsyValues([
           isKernelThread === 0 && isMainThread === 1 && 'main thread',
         ]),
-        track: new AsyncSliceTrack({trace: ctx, uri}, maxDepth, trackIds),
+        track: new AsyncSliceTrack(ctx, uri, maxDepth, trackIds),
       });
       const track = new TrackNode({uri, title, sortOrder: 20});
       trackIds.forEach((id) => {
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
index 55f7c95..23226bc 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/slice_selection_aggregator.ts
@@ -16,16 +16,27 @@
 import {AreaSelection} from '../../public/selection';
 import {Engine} from '../../trace_processor/engine';
 import {AreaSelectionAggregator} from '../../public/selection';
-import {SLICE_TRACK_KIND} from '../../public/track_kinds';
+import {UnionDataset} from '../../trace_processor/dataset';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 
 export class SliceSelectionAggregator implements AreaSelectionAggregator {
   readonly id = 'slice_aggregation';
 
   async createAggregateView(engine: Engine, area: AreaSelection) {
-    const selectedTrackKeys = getSelectedTrackSqlIds(area);
-
-    if (selectedTrackKeys.length === 0) return false;
-
+    const desiredSchema = {
+      id: NUM,
+      name: STR,
+      ts: LONG,
+      dur: LONG,
+    };
+    const validDatasets = area.tracks
+      .map((track) => track.track.getDataset?.())
+      .filter((ds) => ds !== undefined)
+      .filter((ds) => ds.implements(desiredSchema));
+    if (validDatasets.length === 0) {
+      return false;
+    }
+    const unionDataset = new UnionDataset(validDatasets);
     await engine.query(`
       create or replace perfetto table ${this.id} as
       select
@@ -33,12 +44,13 @@
         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}
+        from (${unionDataset.optimize().query()})
+      where
+        ts + dur > ${area.start}
         and ts < ${area.end}
       group by name
     `);
+
     return true;
   }
 
@@ -83,14 +95,3 @@
     ];
   }
 }
-
-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.Counter/counter_details_panel.ts b/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
index e0ff568..48649e9 100644
--- a/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.Counter/counter_details_panel.ts
@@ -27,8 +27,8 @@
 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 {Timestamp} from '../../public/lib/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
 import {TrackEventSelection} from '../../public/selection';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
 import {asArgSetId} from '../../trace_processor/sql_utils/core_types';
diff --git a/ui/src/plugins/dev.perfetto.Counter/index.ts b/ui/src/plugins/dev.perfetto.Counter/index.ts
index 074e2c4..ef063ca 100644
--- a/ui/src/plugins/dev.perfetto.Counter/index.ts
+++ b/ui/src/plugins/dev.perfetto.Counter/index.ts
@@ -85,10 +85,6 @@
   //   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:
   {
@@ -163,16 +159,16 @@
           kind: COUNTER_TRACK_KIND,
           trackIds: [trackId],
         },
-        track: new TraceProcessorCounterTrack({
-          trace: ctx,
+        track: new TraceProcessorCounterTrack(
+          ctx,
           uri,
-          trackId,
-          trackName: title,
-          options: {
+          {
             ...getDefaultCounterOptions(title),
             unit,
           },
-        }),
+          trackId,
+          title,
+        ),
       });
       const track = new TrackNode({uri, title});
       ctx.workspace.addChildInOrder(track);
@@ -242,13 +238,13 @@
           trackIds: [trackId],
           scope,
         },
-        track: new TraceProcessorCounterTrack({
-          trace: ctx,
+        track: new TraceProcessorCounterTrack(
+          ctx,
           uri,
-          trackId: trackId,
-          trackName: name,
-          options: getDefaultCounterOptions(name),
-        }),
+          getDefaultCounterOptions(name),
+          trackId,
+          name,
+        ),
       });
       const trackNode = new TrackNode({uri, title: name, sortOrder: -20});
       ctx.workspace.addChildInOrder(trackNode);
@@ -309,13 +305,13 @@
           upid: upid ?? undefined,
           scope: 'thread',
         },
-        track: new TraceProcessorCounterTrack({
-          trace: ctx,
+        track: new TraceProcessorCounterTrack(
+          ctx,
           uri,
-          trackId: trackId,
-          trackName: name,
-          options: getDefaultCounterOptions(name),
-        }),
+          getDefaultCounterOptions(name),
+          trackId,
+          name,
+        ),
       });
       const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode({uri, title: name, sortOrder: 30});
@@ -367,13 +363,13 @@
           upid,
           scope: 'process',
         },
-        track: new TraceProcessorCounterTrack({
-          trace: ctx,
+        track: new TraceProcessorCounterTrack(
+          ctx,
           uri,
-          trackId: trackId,
-          trackName: name,
-          options: getDefaultCounterOptions(name),
-        }),
+          getDefaultCounterOptions(name),
+          trackId,
+          name,
+        ),
       });
       const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode({uri, title: name, sortOrder: 20});
@@ -402,13 +398,13 @@
           trackIds: [it.id],
           scope: 'gpuFreq',
         },
-        track: new TraceProcessorCounterTrack({
-          trace: ctx,
+        track: new TraceProcessorCounterTrack(
+          ctx,
           uri,
-          trackId: it.id,
-          trackName: name,
-          options: getDefaultCounterOptions(name),
-        }),
+          getDefaultCounterOptions(name),
+          it.id,
+          name,
+        ),
       });
       const track = new TrackNode({uri, title: name, sortOrder: -20});
       ctx.workspace.addChildInOrder(track);
diff --git a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts b/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
index ddbdbcb..a76fcef 100644
--- a/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
+++ b/ui/src/plugins/dev.perfetto.Counter/trace_processor_counter_track.ts
@@ -12,33 +12,27 @@
 // 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 {Time} from '../../base/time';
 import {
   BaseCounterTrack,
-  BaseCounterTrackArgs,
+  CounterOptions,
 } from '../../frontend/base_counter_track';
-
-import {TrackMouseEvent} from '../../public/track';
 import {TrackEventDetails} from '../../public/selection';
-import {Time} from '../../base/time';
+import {Trace} from '../../public/trace';
+import {TrackMouseEvent} from '../../public/track';
+import {LONG, LONG_NULL, NUM} from '../../trace_processor/query_result';
 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;
+  constructor(
+    trace: Trace,
+    uri: string,
+    options: Partial<CounterOptions>,
+    private readonly trackId: number,
+    private readonly trackName: string,
+    private readonly rootTable: string = 'counter',
+  ) {
+    super(trace, uri, options);
   }
 
   getSqlSource() {
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
index 060746e..91c6d2b 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_details_panel.ts
@@ -18,7 +18,7 @@
   metricsFromTableOrSubquery,
   QueryFlamegraph,
 } from '../../public/lib/query_flamegraph';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {
   TrackEventDetailsPanel,
   TrackEventDetailsPanelSerializeArgs,
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
index 4baeddf..0bd6fe8 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/cpu_profile_track.ts
@@ -20,10 +20,10 @@
   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';
+import {Trace} from '../../public/trace';
 
 interface CpuProfileRow extends NamedRow {
   callsiteId: number;
@@ -31,10 +31,11 @@
 
 export class CpuProfileTrack extends BaseSliceTrack<Slice, CpuProfileRow> {
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     private utid: number,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   protected getRowSpec(): CpuProfileRow {
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
index 31e49fe..e33e341 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
@@ -60,13 +60,7 @@
           utid,
           ...(exists(upid) && {upid}),
         },
-        track: new CpuProfileTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          utid,
-        ),
+        track: new CpuProfileTrack(ctx, uri, utid),
       });
       const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode({uri, title, sortOrder: -40});
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
index 2c5b456..95e35b5 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/cpu_slice_track.ts
@@ -42,6 +42,7 @@
 import {Trace} from '../../public/trace';
 import {exists} from '../../base/utils';
 import {ThreadMap} from '../dev.perfetto.Thread/threads';
+import {Dataset, SourceDataset} from '../../trace_processor/dataset';
 
 export interface Data extends TrackData {
   // Slices are stored in a columnar fashion. All fields have the same length.
@@ -96,6 +97,21 @@
     this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
   }
 
+  getDataset(): Dataset | undefined {
+    return new SourceDataset({
+      src: 'select id, ts, dur, cpu from sched where utid != 0',
+      schema: {
+        id: NUM,
+        ts: LONG,
+        dur: LONG,
+      },
+      filter: {
+        col: 'cpu',
+        eq: this.cpu,
+      },
+    });
+  }
+
   async onUpdate({
     visibleWindow,
     resolution,
diff --git a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
index f4b3b93..1aaaf1a 100644
--- a/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
+++ b/ui/src/plugins/dev.perfetto.CpuSlices/sched_details_tab.ts
@@ -19,8 +19,8 @@
 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 {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {asSchedSqlId} from '../../trace_processor/sql_utils/core_types';
 import {
   getSched,
diff --git a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
index 5b1d0d4..1c60d4f 100644
--- a/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
+++ b/ui/src/plugins/dev.perfetto.ExplorePage/explore_page.ts
@@ -33,8 +33,15 @@
 import {Button} from '../../widgets/button';
 import {Icons} from '../../base/semantic_icons';
 import {DetailsShell} from '../../widgets/details_shell';
+import {
+  Chart,
+  ChartOption,
+  createChartConfigFromSqlTableState,
+  renderChartComponent,
+} from '../../frontend/widgets/charts/chart';
+import {AddChartMenuItem} from '../../frontend/widgets/charts/add_chart_menu';
 
-interface ExplorePageState {
+interface ExploreTableState {
   sqlTableState?: SqlTableState;
   selectedTable?: ExplorableTable;
 }
@@ -46,13 +53,12 @@
 }
 
 export class ExplorePage implements m.ClassComponent<PageWithTraceAttrs> {
-  private readonly state: ExplorePageState;
+  private readonly state: ExploreTableState;
+  private readonly charts: Chart[];
 
   constructor() {
-    this.state = {
-      sqlTableState: undefined,
-      selectedTable: undefined,
-    };
+    this.charts = [];
+    this.state = {};
   }
 
   // Show menu with standard library tables
@@ -115,7 +121,7 @@
 
           this.state.selectedTable = table;
 
-          const sqlTableState = new SqlTableState(
+          this.state.sqlTableState = new SqlTableState(
             trace,
             {
               name: table.name,
@@ -123,7 +129,6 @@
             },
             {imports: [table.module]},
           );
-          this.state.sqlTableState = sqlTableState;
         },
       });
     });
@@ -162,6 +167,16 @@
       },
       m(SqlTable, {
         state: sqlTableState,
+        addColumnMenuItems: (column, columnAlias) =>
+          m(AddChartMenuItem, {
+            chartConfig: createChartConfigFromSqlTableState(
+              column,
+              columnAlias,
+              sqlTableState,
+            ),
+            chartOptions: [ChartOption.HISTOGRAM],
+            addChart: (chart) => this.charts.push(chart),
+          }),
       }),
     );
   }
@@ -170,6 +185,7 @@
     return m(
       '.explore-page',
       m(Menu, this.renderSelectableTablesMenuItems(attrs.trace)),
+      this.charts.map((chart) => renderChartComponent(chart)),
       this.state.selectedTable && this.renderSqlTable(),
     );
   }
diff --git a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
index 03be4f2..c6ef4b3 100644
--- a/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/actual_frames_track.ts
@@ -20,6 +20,7 @@
 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
@@ -54,7 +55,7 @@
     uri: string,
     private trackIds: number[],
   ) {
-    super({trace, uri});
+    super(trace, uri);
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
@@ -90,6 +91,26 @@
       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',
+    };
+  }
+
+  // Override dataset from base class NamedSliceTrack as we don't want these
+  // tracks to participate in generic area selection aggregation (frames tracks
+  // have their own dedicated aggregation panel).
+  // TODO(stevegolton): In future CLs this will be handled with aggregation keys
+  // instead, as this track will have to expose a dataset anyway.
+  override getDataset() {
+    return undefined;
+  }
 }
 
 function getColorSchemeForJank(
diff --git a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
index bd41be4..27557fc 100644
--- a/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/expected_frames_track.ts
@@ -22,6 +22,7 @@
 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
 
@@ -32,7 +33,7 @@
     uri: string,
     private trackIds: number[],
   ) {
-    super({trace, uri});
+    super(trace, uri);
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
@@ -61,4 +62,15 @@
   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.Ftrace/ftrace_explorer.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
index b4036e5..09e45f1 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_explorer.ts
@@ -22,7 +22,7 @@
   PopupMultiSelect,
 } from '../../widgets/multiselect';
 import {PopupPosition} from '../../widgets/popup';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {FtraceFilter, FtraceStat} from './common';
 import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
diff --git a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
index 31ea7de..ea87d0e 100644
--- a/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
+++ b/ui/src/plugins/dev.perfetto.Ftrace/ftrace_track.ts
@@ -20,18 +20,22 @@
 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 {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {FtraceFilter} from './common';
 import {Monitor} from '../../base/monitor';
 import {TrackRenderContext} from '../../public/track';
+import {SourceDataset, Dataset} from '../../trace_processor/dataset';
 
 const MARGIN = 2;
 const RECT_HEIGHT = 18;
+const RECT_WIDTH = 8;
 const TRACK_HEIGHT = RECT_HEIGHT + 2 * MARGIN;
 
-export interface Data extends TrackData {
-  timestamps: BigInt64Array;
-  names: string[];
+interface Data extends TrackData {
+  events: Array<{
+    timestamp: time;
+    color: string;
+  }>;
 }
 
 export interface Config {
@@ -53,6 +57,25 @@
     this.monitor = new Monitor([() => store.state]);
   }
 
+  getDataset(): Dataset {
+    return new SourceDataset({
+      // 'ftrace_event' doesn't have a dur column, but injecting dur=0 (all
+      // ftrace events are effectively 'instant') allows us to participate in
+      // generic slice aggregations
+      src: 'select id, ts, 0 as dur, name, cpu from ftrace_event',
+      schema: {
+        id: NUM,
+        name: STR,
+        ts: LONG,
+        dur: LONG,
+      },
+      filter: {
+        col: 'cpu',
+        eq: this.cpu,
+      },
+    });
+  }
+
   async onUpdate({
     visibleWindow,
     resolution,
@@ -92,21 +115,22 @@
       order by tsQuant limit ${LIMIT};`);
 
     const rowCount = queryRes.numRows();
-    const result: Data = {
+
+    const it = queryRes.iter({tsQuant: LONG, name: STR});
+    const events = [];
+    for (let row = 0; it.valid(); it.next(), row++) {
+      events.push({
+        timestamp: Time.fromRaw(it.tsQuant),
+        color: colorForFtrace(it.name).base.cssString,
+      });
+    }
+    return {
       start,
       end,
       resolution,
       length: rowCount,
-      timestamps: new BigInt64Array(rowCount),
-      names: [],
+      events,
     };
-
-    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 {
@@ -125,21 +149,10 @@
       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();
+    for (const e of data.events) {
+      ctx.fillStyle = e.color;
+      const xPos = Math.floor(timescale.timeToPx(e.timestamp));
+      ctx.fillRect(xPos - RECT_WIDTH / 2, MARGIN, RECT_WIDTH, RECT_HEIGHT);
     }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
index 9705156..b34717e 100644
--- a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
+++ b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
@@ -21,13 +21,14 @@
   NamedRow,
   NamedSliceTrack,
 } from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
 import {TrackNode} from '../../public/workspace';
 class GpuPidTrack extends NamedSliceTrack {
-  upid: number;
-
-  constructor(args: NewTrackArgs, upid: number) {
-    super(args);
+  constructor(
+    trace: Trace,
+    uri: string,
+    protected readonly upid: number,
+  ) {
+    super(trace, uri);
     this.upid = upid;
   }
 
@@ -84,7 +85,7 @@
       ctx.tracks.registerTrack({
         uri,
         title,
-        track: new GpuPidTrack({trace: ctx, uri}, upid),
+        track: new GpuPidTrack(ctx, uri, upid),
       });
       const track = new TrackNode({uri, title});
       track.uri = uri;
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
index c3e145e..5b45256 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_details_panel.ts
@@ -21,7 +21,7 @@
   metricsFromTableOrSubquery,
 } from '../../public/lib/query_flamegraph';
 import {convertTraceToPprofAndDownload} from '../../frontend/trace_converter';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {
   TrackEventDetailsPanel,
   TrackEventDetailsPanelSerializeArgs,
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts
index d4effa4..45f1d0e 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/heap_profile_track.ts
@@ -19,13 +19,13 @@
   OnSliceClickArgs,
   OnSliceOverArgs,
 } from '../../frontend/base_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
 import {
   ProfileType,
   profileType,
   TrackEventDetails,
   TrackEventSelection,
 } from '../../public/selection';
+import {Trace} from '../../public/trace';
 import {Slice} from '../../public/track';
 import {LONG, STR} from '../../trace_processor/query_result';
 import {HeapProfileFlamegraphDetailsPanel} from './heap_profile_details_panel';
@@ -44,12 +44,13 @@
   HeapProfileRow
 > {
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     private readonly tableName: string,
     private readonly upid: number,
     private readonly heapProfileIsIncomplete: boolean,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   getSqlSource(): string {
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/index.ts b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
index f4f9121..2e0591f 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
@@ -92,15 +92,7 @@
           kind: HEAP_PROFILE_TRACK_KIND,
           upid,
         },
-        track: new HeapProfileTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          tableName,
-          upid,
-          incomplete,
-        ),
+        track: new HeapProfileTrack(ctx, uri, tableName, upid, incomplete),
       });
       const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode({uri, title, sortOrder: -30});
diff --git a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
index 26ac305..04e8b13 100644
--- a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
@@ -57,13 +57,7 @@
           kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
           upid,
         },
-        track: new ProcessPerfSamplesProfileTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          upid,
-        ),
+        track: new ProcessPerfSamplesProfileTrack(ctx, uri, upid),
       });
       const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode({uri, title, sortOrder: -40});
@@ -103,13 +97,7 @@
           utid,
           upid: upid ?? undefined,
         },
-        track: new ThreadPerfSamplesProfileTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          utid,
-        ),
+        track: new ThreadPerfSamplesProfileTrack(ctx, uri, utid),
       });
       const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode({uri, title, sortOrder: -50});
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
index 839d0d1..5157569 100644
--- a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/perf_samples_profile_track.ts
+++ b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/perf_samples_profile_track.ts
@@ -19,7 +19,6 @@
   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 {
@@ -33,10 +32,11 @@
   QueryFlamegraph,
 } from '../../public/lib/query_flamegraph';
 import {DetailsShell} from '../../widgets/details_shell';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {time} from '../../base/time';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {Flamegraph, FLAMEGRAPH_STATE_SCHEMA} from '../../widgets/flamegraph';
+import {Trace} from '../../public/trace';
 
 interface PerfSampleRow extends NamedRow {
   callsiteId: number;
@@ -46,8 +46,8 @@
   Slice,
   PerfSampleRow
 > {
-  constructor(args: NewTrackArgs) {
-    super(args);
+  constructor(trace: Trace, uri: string) {
+    super(trace, uri);
   }
 
   protected getRowSpec(): PerfSampleRow {
@@ -75,10 +75,11 @@
 
 export class ProcessPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
   constructor(
-    args: NewTrackArgs,
-    private upid: number,
+    trace: Trace,
+    uri: string,
+    private readonly upid: number,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   getSqlSource(): string {
@@ -170,10 +171,11 @@
 
 export class ThreadPerfSamplesProfileTrack extends BasePerfSamplesProfileTrack {
   constructor(
-    args: NewTrackArgs,
-    private utid: number,
+    trace: Trace,
+    uri: string,
+    private readonly utid: number,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   getSqlSource(): string {
diff --git a/ui/src/controller/adb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
similarity index 98%
rename from ui/src/controller/adb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
index e188ea7..5197d23 100644
--- a/ui/src/controller/adb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb.ts
@@ -12,9 +12,9 @@
 // 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 {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;
diff --git a/ui/src/controller/adb_base_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
similarity index 96%
rename from ui/src/controller/adb_base_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
index 2a72a33..c447df5 100644
--- a/ui/src/controller/adb_base_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_base_controller.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {exists} from '../base/utils';
-import {RecordingState, RecordingTarget, isAdbTarget} from '../common/state';
+import {exists} from '../../base/utils';
+import {RecordingState, RecordingTarget, isAdbTarget} from './state';
 import {
   extractDurationFromTraceConfig,
   extractTraceConfig,
-} from '../core/trace_config_utils';
+} from './trace_config_utils';
 import {Adb} from './adb_interfaces';
 import {ReadBuffersResponse} from './consumer_port_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
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/controller/adb_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
similarity index 97%
rename from ui/src/controller/adb_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
index 9f51a97..1d228a5 100644
--- a/ui/src/controller/adb_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_jsdomtest.ts
@@ -19,7 +19,7 @@
   DEFAULT_MAX_PAYLOAD_BYTES,
   VERSION_WITH_CHECKSUM,
 } from './adb';
-import {utf8Encode} from '../base/string_utils';
+import {utf8Encode} from '../../base/string_utils';
 
 test('startAuthentication', async () => {
   const adb = new AdbOverWebUsb();
diff --git a/ui/src/controller/adb_record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
similarity index 95%
rename from ui/src/controller/adb_record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
index e404397..6078a59 100644
--- a/ui/src/controller/adb_record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_record_controller_jsdomtest.ts
@@ -13,12 +13,12 @@
 // limitations under the License.
 
 import {dingus} from 'dingusjs';
-import {utf8Encode} from '../base/string_utils';
-import {EnableTracingRequest, TraceConfig} from '../protos';
+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 '../common/empty_state';
+import {createEmptyState} from './empty_state';
 
 function generateMockConsumer(): Consumer {
   return {
diff --git a/ui/src/controller/adb_shell_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
similarity index 96%
rename from ui/src/controller/adb_shell_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
index 5d1d156..623dc5d 100644
--- a/ui/src/controller/adb_shell_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_shell_controller.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {base64Encode, utf8Decode} from '../base/string_utils';
-import {RecordingState} from '../common/state';
-import {extractTraceConfig} from '../core/trace_config_utils';
+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';
diff --git a/ui/src/controller/adb_socket_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
similarity index 98%
rename from ui/src/controller/adb_socket_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
index 715be0d..a676747 100644
--- a/ui/src/controller/adb_socket_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/adb_socket_controller.ts
@@ -20,14 +20,14 @@
   GetTraceStatsResponse,
   IPCFrame,
   ReadBuffersResponse,
-} from '../protos';
+} 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 '../common/state';
+import {exists} from '../../base/utils';
+import {assertTrue} from '../../base/logging';
+import {RecordingState} from './state';
 
 enum SocketState {
   DISCONNECTED,
diff --git a/ui/src/frontend/recording/advanced_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/advanced_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
index d762338..35e6fe2 100644
--- a/ui/src/frontend/recording/advanced_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/advanced_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 const FTRACE_CATEGORIES = new Map<string, string>();
diff --git a/ui/src/frontend/recording/android_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/android_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
index 4154fa7..7c0d741 100644
--- a/ui/src/frontend/recording/android_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/android_settings.ts
@@ -14,9 +14,9 @@
 
 import m from 'mithril';
 import {AtomId, DataSourceDescriptor} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
-import {RecordConfig} from '../../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
 
 const PUSH_ATOM_IDS = new Map<string, string>();
 const PULL_ATOM_IDS = new Map<string, string>();
diff --git a/ui/src/controller/chrome_proxy_record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
similarity index 96%
rename from ui/src/controller/chrome_proxy_record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
index d1e1b63..ef0b999 100644
--- a/ui/src/controller/chrome_proxy_record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_proxy_record_controller.ts
@@ -12,8 +12,8 @@
 // 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 {binaryDecode, binaryEncode} from '../../base/string_utils';
+import {TRACE_SUFFIX} from '../../public/trace';
 import {
   ConsumerPortResponse,
   hasProperty,
diff --git a/ui/src/frontend/recording/chrome_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/chrome_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
index 8ac14cd..fd09d82 100644
--- a/ui/src/frontend/recording/chrome_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/chrome_settings.ts
@@ -13,19 +13,19 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
 import {
   RecordingState,
   getBuiltinChromeCategoryList,
   isChromeTarget,
-} from '../../common/state';
+} from './state';
 import {
   MultiSelect,
   MultiSelectDiff,
   Option as MultiSelectOption,
 } from '../../widgets/multiselect';
 import {Section} from '../../widgets/section';
-import {CategoryGetter, CompactProbe, Toggle} from '../record_widgets';
+import {CategoryGetter, CompactProbe, Toggle} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 function extractChromeCategories(
diff --git a/ui/src/controller/consumer_port_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
similarity index 98%
rename from ui/src/controller/consumer_port_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
index 973205f..732e9e8 100644
--- a/ui/src/controller/consumer_port_types.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/consumer_port_types.ts
@@ -18,7 +18,7 @@
   IFreeBuffersResponse,
   IGetTraceStatsResponse,
   IReadBuffersResponse,
-} from '../protos';
+} from '../../protos';
 
 export interface Typed {
   type: string;
diff --git a/ui/src/frontend/recording/cpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/cpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
index ba12267..06b2713 100644
--- a/ui/src/frontend/recording/cpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/cpu_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe, Slider} from '../record_widgets';
+import {Probe, Slider} from './record_widgets';
 import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
 
 export class CpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/common/empty_state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
similarity index 92%
rename from ui/src/common/empty_state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
index c356b00..bfafe3f 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/empty_state.ts
@@ -12,10 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {
-  autosaveConfigStore,
-  recordTargetStore,
-} from '../frontend/record_config';
+import {autosaveConfigStore, recordTargetStore} from './record_config';
 import {RecordingState} from './state';
 
 export function createEmptyState(): RecordingState {
diff --git a/ui/src/frontend/recording/etw_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/etw_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
index 70c1ab1..eefb8ac 100644
--- a/ui/src/frontend/recording/etw_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/etw_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 export class EtwSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/gpu_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
similarity index 97%
rename from ui/src/frontend/recording/gpu_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
index 745af99..1040f75 100644
--- a/ui/src/frontend/recording/gpu_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/gpu_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe} from '../record_widgets';
+import {Probe} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 
 export class GpuSettings implements m.ClassComponent<RecordingSectionAttrs> {
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/frontend/recording/linux_perf_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/linux_perf_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
index 7f9c6d4..a0fcf9f 100644
--- a/ui/src/frontend/recording/linux_perf_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/linux_perf_settings.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Probe, Slider, Textarea} from '../record_widgets';
+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.:
diff --git a/ui/src/frontend/recording/memory_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
similarity index 98%
rename from ui/src/frontend/recording/memory_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
index ccfa9ef..231306f 100644
--- a/ui/src/frontend/recording/memory_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/memory_settings.ts
@@ -14,7 +14,7 @@
 
 import m from 'mithril';
 import {MeminfoCounters, VmstatCounters} from '../../protos';
-import {Dropdown, Probe, Slider, Textarea, Toggle} from '../record_widgets';
+import {Dropdown, Probe, Slider, Textarea, Toggle} from './record_widgets';
 import {POLL_INTERVAL_MS, RecordingSectionAttrs} from './recording_sections';
 
 class HeapSettings implements m.ClassComponent<RecordingSectionAttrs> {
diff --git a/ui/src/frontend/recording/power_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
similarity index 93%
rename from ui/src/frontend/recording/power_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
index be2f6ed..bf88217 100644
--- a/ui/src/frontend/recording/power_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/power_settings.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {globals} from '../globals';
-import {Probe, Slider} from '../record_widgets';
+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> {
@@ -34,6 +34,7 @@
         m('span', ')'),
       ),
     ];
+    // TODO(primiano): figure out a better story for isInternalUser.
     if (globals.isInternalUser) {
       descr.push(
         m(
diff --git a/ui/src/frontend/record_config.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
similarity index 97%
rename from ui/src/frontend/record_config.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
index f3bf3c5..ae41d9c 100644
--- a/ui/src/frontend/record_config.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_config.ts
@@ -12,15 +12,15 @@
 // 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 {exists} from '../../base/utils';
+import {getDefaultRecordingTargets, RecordingTarget} from './state';
 import {
   createEmptyRecordConfig,
   NamedRecordConfig,
   NAMED_RECORD_CONFIG_SCHEMA,
   RecordConfig,
   RECORD_CONFIG_SCHEMA,
-} from '../controller/record_config_types';
+} from './record_config_types';
 
 const LOCAL_STORAGE_RECORD_CONFIGS_KEY = 'recordConfigs';
 const LOCAL_STORAGE_AUTOSAVE_CONFIG_KEY = 'autosaveConfig';
diff --git a/ui/src/controller/record_config_types.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
similarity index 100%
rename from ui/src/controller/record_config_types.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_config_types.ts
diff --git a/ui/src/controller/record_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
similarity index 95%
rename from ui/src/controller/record_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
index 1bfa074..8062650 100644
--- a/ui/src/controller/record_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller.ts
@@ -13,19 +13,19 @@
 // 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 '../common/constants';
-import {genTraceConfig} from '../common/recordingV2/recording_config_utils';
-import {TargetInfo} from '../common/recordingV2/recording_interfaces_v2';
+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 '../common/state';
-import {ConsumerPort, TraceConfig} from '../protos';
+} from './state';
+import {ConsumerPort, TraceConfig} from '../../protos';
 import {AdbOverWebUsb} from './adb';
 import {AdbConsumerPort} from './adb_shell_controller';
 import {AdbSocketConsumerPort} from './adb_socket_controller';
@@ -41,9 +41,9 @@
 } from './consumer_port_types';
 import {RecordConfig} from './record_config_types';
 import {Consumer, RpcConsumerPort} from './record_controller_interfaces';
-import {AppImpl} from '../core/app_impl';
 import {RecordingManager} from './recording_manager';
-import {raf} from '../core/raf_scheduler';
+import {scheduleFullRedraw} from '../../widgets/raf';
+import {App} from '../../public/app';
 
 type RPCImplMethod = Method | rpc.ServiceMethod<Message<{}>, Message<{}>>;
 
@@ -189,6 +189,7 @@
 }
 
 export class RecordController implements Consumer {
+  private app: App;
   private recMgr: RecordingManager;
   private config: RecordConfig | null = null;
   private readonly extensionPort: MessagePort;
@@ -206,7 +207,8 @@
   // char, it is the 'targetOS'
   private controllerPromises = new Map<string, Promise<RpcConsumerPort>>();
 
-  constructor(recMgr: RecordingManager, extensionPort: MessagePort) {
+  constructor(app: App, recMgr: RecordingManager, extensionPort: MessagePort) {
+    this.app = app;
     this.recMgr = recMgr;
     this.consumerPort = ConsumerPort.create(this.rpcImpl.bind(this));
     this.extensionPort = extensionPort;
@@ -219,7 +221,7 @@
   refreshOnStateChange() {
     // TODO(eseckler): Use ConsumerPort's QueryServiceState instead
     // of posting a custom extension message to retrieve the category list.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
     if (this.state.fetchChromeCategories && !this.fetchedCategories) {
       this.fetchedCategories = true;
       if (this.state.extensionInstalled) {
@@ -320,7 +322,7 @@
       return;
     }
     const trace = this.generateTrace();
-    AppImpl.instance.openTraceFromBuffer({
+    this.app.openTraceFromBuffer({
       title: 'Recorded trace',
       buffer: trace.buffer,
       fileName: `recorded_trace${this.recordedTraceSuffix}`,
diff --git a/ui/src/controller/record_controller_interfaces.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
similarity index 97%
rename from ui/src/controller/record_controller_interfaces.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
index e9662fd..f29940a 100644
--- a/ui/src/controller/record_controller_interfaces.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_interfaces.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TRACE_SUFFIX} from '../common/constants';
+import {TRACE_SUFFIX} from '../../public/trace';
 import {ConsumerPortResponse} from './consumer_port_types';
 
 export type ErrorCallback = (_: string) => void;
diff --git a/ui/src/controller/record_controller_jsdomtest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
similarity index 99%
rename from ui/src/controller/record_controller_jsdomtest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
index 442e4b8..1035369 100644
--- a/ui/src/controller/record_controller_jsdomtest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_controller_jsdomtest.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../base/logging';
-import {TraceConfig} from '../protos';
+import {assertExists} from '../../base/logging';
+import {TraceConfig} from '../../protos';
 import {createEmptyRecordConfig} from './record_config_types';
 import {genConfigProto, toPbtxt} from './record_controller';
 
diff --git a/ui/src/frontend/record_page.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
similarity index 92%
rename from ui/src/frontend/record_page.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
index aed42e3..021a5db 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page.ts
@@ -26,35 +26,32 @@
   LoadedConfig,
   MAX_TIME,
   RecordingTarget,
-} from '../common/state';
-import {AdbOverWebUsb} from '../controller/adb';
-import {
-  RECORD_CONFIG_SCHEMA,
-  RecordConfig,
-} from '../controller/record_config_types';
-import {raf} from '../core/raf_scheduler';
-import {PageAttrs} from '../public/page';
+} 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 './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 {RecordingSettings} from './recording/recording_settings';
-import {EtwSettings} from './recording/etw_settings';
-import {AppImpl} from '../core/app_impl';
-import {RecordingManager} from '../controller/recording_manager';
-import {BUCKET_NAME, GcsUploader, MIME_JSON} from '../common/gcs_uploader';
-import {showModal} from '../widgets/modal';
-import {CopyableLink} from '../widgets/copyable_link';
+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',
@@ -155,7 +152,7 @@
 
   recMgr.setRecordingTarget(recordingTarget);
   recordTargetStore.save(target);
-  raf.scheduleFullRedraw();
+  scheduleFullRedraw();
 }
 
 function Instructions(recMgr: RecordingManager, cssClass: string) {
@@ -198,7 +195,7 @@
       disabled: loadedConfigEqual(configType, recMgr.state.lastLoadedConfig),
       onclick: () => {
         recMgr.setRecordConfig(config, configType);
-        raf.scheduleFullRedraw();
+        scheduleFullRedraw();
       },
     },
     m('i.material-icons', 'file_upload'),
@@ -244,7 +241,7 @@
                   type: 'NAMED',
                   name: item.title,
                 });
-                raf.scheduleFullRedraw();
+                scheduleFullRedraw();
               }
             },
           },
@@ -257,7 +254,7 @@
             title: 'Remove configuration',
             onclick: () => {
               recordConfigStore.delete(item.key);
-              raf.scheduleFullRedraw();
+              scheduleFullRedraw();
             },
           },
           m('i.material-icons', 'delete'),
@@ -292,7 +289,7 @@
         placeholder: 'Title for config',
         oninput() {
           ConfigTitleState.setTitle(this.value);
-          raf.scheduleFullRedraw();
+          scheduleFullRedraw();
         },
       }),
       m(
@@ -308,7 +305,7 @@
               recMgr.state.recordConfig,
               ConfigTitleState.getTitle(),
             );
-            raf.scheduleFullRedraw();
+            scheduleFullRedraw();
             ConfigTitleState.clearTitle();
           },
         },
@@ -326,7 +323,7 @@
               )
             ) {
               recMgr.clearRecordConfig();
-              raf.scheduleFullRedraw();
+              scheduleFullRedraw();
             }
           },
         },
@@ -573,7 +570,7 @@
 
 function onStartRecordingPressed(recMgr: RecordingManager) {
   location.href = '#!/record/instructions';
-  raf.scheduleFullRedraw();
+  scheduleFullRedraw();
   autosaveConfigStore.save(recMgr.state.recordConfig);
 
   const target = recMgr.state.recordingTarget;
@@ -582,7 +579,7 @@
     isChromeTarget(target) ||
     isWindowsTarget(target)
   ) {
-    AppImpl.instance.analytics.logEvent(
+    recMgr.app.analytics.logEvent(
       'Record Trace',
       `Record trace (${target.os})`,
     );
@@ -742,7 +739,7 @@
     '.record-menu',
     {
       class: recInProgress ? 'disabled' : '',
-      onclick: () => raf.scheduleFullRedraw(),
+      onclick: () => scheduleFullRedraw(),
     },
     m('header', 'Trace config'),
     m(
@@ -789,20 +786,29 @@
   return routePage === section ? '.active' : '';
 }
 
-export class RecordPage implements m.ClassComponent<PageAttrs> {
-  private readonly recMgr = RecordingManager.instance;
+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;
 
-  oninit({attrs}: m.CVnode<PageAttrs>) {
+  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);
-      AppImpl.instance.navigate('#!/record/instructions');
+      attrs.app.navigate('#!/record/instructions');
     }
   }
 
-  view({attrs}: m.CVnode<PageAttrs>) {
+  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
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
similarity index 80%
rename from ui/src/frontend/record_page_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
index e0faccd..3559332 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_page_v2.ts
@@ -14,24 +14,20 @@
 
 import m from 'mithril';
 import {Attributes} from 'mithril';
-import {assertExists} from '../base/logging';
-import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
+import {assertExists} from '../../base/logging';
+import {RecordingConfigUtils} from './recordingV2/recording_config_utils';
 import {
   ChromeTargetInfo,
   RecordingTargetV2,
   TargetInfo,
-} from '../common/recordingV2/recording_interfaces_v2';
+} from './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 {PageAttrs} from '../public/page';
+} 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,
@@ -41,34 +37,29 @@
   uploadRecordingConfig,
 } 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 {RecordingSettings} from './recording/recording_settings';
-import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
-import {showAddNewTargetModal} from './recording/reset_target_modal';
-import {RecordingManager} from '../controller/recording_manager';
-import {RecordConfig} from '../controller/record_config_types';
-import {AppImpl} from '../core/app_impl';
+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;
-function controller(): RecordingPageController {
-  if (_controller === undefined) {
-    _controller = new RecordingPageController(RecordingManager.instance);
-  }
-  return _controller;
-}
-const recordConfigUtils = new RecordingConfigUtils();
+let controller: RecordingPageController;
+let recordConfigUtils: RecordingConfigUtils;
 
 // Options for displaying a target selection menu.
 export interface TargetSelectionOptions {
@@ -107,13 +98,13 @@
 
 function RecordingPlatformSelection() {
   // Don't show the platform selector while we are recording a trace.
-  if (controller().getState() >= RecordingState.RECORDING) return undefined;
+  if (controller.getState() >= RecordingState.RECORDING) return undefined;
 
   return m(
     '.target',
     m(
       '.chip',
-      {onclick: () => showAddNewTargetModal(controller())},
+      {onclick: () => showAddNewTargetModal(controller)},
       m('button', 'Add new recording target'),
       m('i.material-icons', 'add'),
     ),
@@ -122,13 +113,13 @@
 }
 
 export function targetSelection(): m.Vnode | undefined {
-  if (!controller().shouldShowTargetSelection()) {
+  if (!controller.shouldShowTargetSelection()) {
     return undefined;
   }
 
   const targets: RecordingTargetV2[] = targetFactoryRegistry.listTargets();
   const targetNames = [];
-  const targetInfo = controller().getTargetInfo();
+  const targetInfo = controller.getTargetInfo();
   if (!targetInfo) {
     targetNames.push(m('option', 'PLEASE_SELECT_TARGET'));
   }
@@ -150,7 +141,7 @@
       {
         selectedIndex,
         onchange: (e: Event) => {
-          controller().onTargetSelection((e.target as HTMLSelectElement).value);
+          controller.onTargetSelection((e.target as HTMLSelectElement).value);
         },
         onupdate: (select) => {
           // Work around mithril bug
@@ -179,11 +170,11 @@
 }
 
 function Instructions(recCfg: RecordConfig, cssClass: string) {
-  if (controller().getState() < RecordingState.TARGET_SELECTED) {
+  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());
+  const targetInfo = assertExists(controller.getTargetInfo());
 
   return m(
     `.record-section.instructions${cssClass}`,
@@ -203,13 +194,13 @@
 
 function BufferUsageProgressBar() {
   // Show the Buffer Usage bar only after we start recording a trace.
-  if (controller().getState() !== RecordingState.RECORDING) {
+  if (controller.getState() !== RecordingState.RECORDING) {
     return undefined;
   }
 
-  controller().fetchBufferUsage();
+  controller.fetchBufferUsage();
 
-  const bufferUsage = controller().getBufferUsagePercentage();
+  const bufferUsage = controller.getBufferUsagePercentage();
   // Buffer usage is not available yet on Android.
   if (bufferUsage === 0) return undefined;
 
@@ -221,11 +212,11 @@
 }
 
 function RecordingNotes(recCfg: RecordConfig) {
-  if (controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED) {
+  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 targetInfo = assertExists(controller.getTargetInfo());
 
   const linuxUrl = 'https://perfetto.dev/docs/quickstart/linux-tracing';
   const cmdlineUrl =
@@ -320,7 +311,7 @@
 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 (controller.getState() > RecordingState.AUTH_P2) {
       // If the UI has started tracing, don't display a message guiding the user
       // to start recording.
       return undefined;
@@ -372,14 +363,14 @@
 
 function RecordingButton(recCfg: RecordConfig) {
   if (
-    controller().getState() !== RecordingState.TARGET_INFO_DISPLAYED ||
-    !controller().canCreateTracingSession()
+    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 targetInfo = assertExists(controller.getTargetInfo());
   const hasDataSources = recordConfigUtils.fetchLatestRecordCommand(
     recCfg,
     targetInfo,
@@ -394,7 +385,7 @@
       'button',
       {
         class: 'selected',
-        onclick: () => controller().onStartRecordingPressed(),
+        onclick: () => controller.onStartRecordingPressed(),
       },
       START_RECORDING_MESSAGE,
     ),
@@ -403,21 +394,17 @@
 
 function StopCancelButtons() {
   // Show the Stop/Cancel buttons only while we are recording a trace.
-  if (!controller().shouldShowStopCancelButtons()) {
+  if (!controller.shouldShowStopCancelButtons()) {
     return undefined;
   }
 
   const stop = m(
     `button.selected`,
-    {onclick: () => controller().onStop()},
+    {onclick: () => controller.onStop()},
     'Stop',
   );
 
-  const cancel = m(
-    `button`,
-    {onclick: () => controller().onCancel()},
-    'Cancel',
-  );
+  const cancel = m(`button`, {onclick: () => controller.onCancel()}, 'Cancel');
 
   return [stop, cancel];
 }
@@ -507,7 +494,7 @@
 
   // 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 targetType = assertExists(controller.getTargetInfo()).targetType;
   const probes = [];
   if (targetType === 'LINUX') {
     probes.push(cpuProbe, powerProbe, memoryProbe, chromeProbe, advancedProbe);
@@ -532,10 +519,10 @@
     '.record-menu',
     {
       class:
-        controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+        controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
           ? 'disabled'
           : '',
-      onclick: () => raf.scheduleFullRedraw(),
+      onclick: () => scheduleFullRedraw(),
     },
     m('header', 'Trace config'),
     m(
@@ -581,10 +568,10 @@
 function getRecordContainer(recMgr: RecordingManager, subpage?: string) {
   const recCfg = recMgr.state.recordConfig;
   const components: m.Children[] = [RecordHeader(recMgr)];
-  if (controller().getState() === RecordingState.NO_TARGET) {
+  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) {
+  } else if (controller.getState() <= RecordingState.ASK_TO_FORCE_P1) {
     components.push(
       m(
         '.full-centered',
@@ -594,13 +581,13 @@
       ),
     );
     return m('.record-container', components);
-  } else if (controller().getState() === RecordingState.AUTH_P1) {
+  } 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
+    controller.getState() === RecordingState.WAITING_FOR_TRACE_DISPLAY
   ) {
     components.push(
       m('.full-centered', 'Waiting for the trace to be collected.'),
@@ -642,7 +629,7 @@
   for (const [section, component] of settingsSections.entries()) {
     pages.push(
       m(component, {
-        dataSources: controller().getTargetInfo()?.dataSources || [],
+        dataSources: controller.getTargetInfo()?.dataSources || [],
         cssClass: maybeGetActiveCss(routePage, section),
         recState: recMgr.state,
       }),
@@ -653,34 +640,43 @@
   return m('.record-container', components);
 }
 
-export class RecordPageV2 implements m.ClassComponent<PageAttrs> {
-  private readonly recMgr = RecordingManager.instance;
+export interface RecordPageV2Attrs extends PageAttrs {
+  app: App;
+  recCtl: RecordingPageController;
+  recMgr: RecordingManager;
+}
+
+export class RecordPageV2 implements m.ClassComponent<RecordPageV2Attrs> {
   private lastSubpage: string | undefined = undefined;
 
-  oninit({attrs}: m.CVnode<PageAttrs>) {
-    controller().initFactories();
+  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(this.recMgr, hash);
-      AppImpl.instance.navigate('#!/record/instructions');
+      loadRecordConfig(attrs.recMgr, hash);
+      attrs.app.navigate('#!/record/instructions');
     }
   }
 
-  view({attrs}: m.CVnode<PageAttrs>) {
+  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.
-      this.recMgr.setRecordConfig(this.recMgr.state.recordConfig);
+      attrs.recMgr.setRecordConfig(attrs.recMgr.state.recordConfig);
     }
 
     return m(
       '.record-page',
-      controller().getState() > RecordingState.TARGET_INFO_DISPLAYED
+      controller.getState() > RecordingState.TARGET_INFO_DISPLAYED
         ? m('.hider')
         : [],
-      getRecordContainer(this.recMgr, attrs.subpage),
+      getRecordContainer(attrs.recMgr, attrs.subpage),
     );
   }
 }
diff --git a/ui/src/frontend/record_widgets.ts b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
similarity index 96%
rename from ui/src/frontend/record_widgets.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
index 90f3c3d..325237b 100644
--- a/ui/src/frontend/record_widgets.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/record_widgets.ts
@@ -13,11 +13,11 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {copyToClipboard} from '../base/clipboard';
-import {assertExists} from '../base/logging';
-import {RecordConfig} from '../controller/record_config_types';
-import {raf} from '../core/raf_scheduler';
-import {assetSrc} from '../base/assets';
+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;
@@ -63,7 +63,7 @@
   view({attrs, children}: m.CVnode<ProbeAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -130,7 +130,7 @@
   view({attrs}: m.CVnode<ToggleAttrs>) {
     const onToggle = (enabled: boolean) => {
       attrs.setEnabled(attrs.recCfg, enabled);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
     };
 
     const enabled = attrs.isEnabled(attrs.recCfg);
@@ -175,7 +175,7 @@
 export class Slider implements m.ClassComponent<SliderAttrs> {
   onValueChange(attrs: SliderAttrs, newVal: number) {
     attrs.set(attrs.recCfg, newVal);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   onTimeValueChange(attrs: SliderAttrs, hms: string) {
@@ -276,7 +276,7 @@
       selKeys.push(item.value);
     }
     attrs.set(attrs.recCfg, selKeys);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<DropdownAttrs>) {
@@ -326,7 +326,7 @@
 export class Textarea implements m.ClassComponent<TextareaAttrs> {
   onChange(attrs: TextareaAttrs, dom: HTMLTextAreaElement) {
     attrs.set(attrs.recCfg, dom.value);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<TextareaAttrs>) {
@@ -400,7 +400,7 @@
     if (!enabled && index !== -1) {
       values.splice(index, 1);
     }
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   view({attrs}: m.CVnode<CategoriesCheckboxListParams>) {
diff --git a/ui/src/common/recordingV2/adb_connection_impl.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_connection_impl.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
index 99ef224..33e0dc1 100644
--- a/ui/src/common/recordingV2/adb_connection_impl.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_impl.ts
@@ -12,8 +12,8 @@
 // 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 {defer} from '../../../base/deferred';
+import {ArrayBufferBuilder} from '../../../base/array_buffer_builder';
 import {AdbFileHandler} from './adb_file_handler';
 import {
   AdbConnection,
@@ -21,7 +21,7 @@
   OnDisconnectCallback,
   OnMessageCallback,
 } from './recording_interfaces_v2';
-import {utf8Decode} from '../../base/string_utils';
+import {utf8Decode} from '../../../base/string_utils';
 
 export abstract class AdbConnectionImpl implements AdbConnection {
   // onStatus and onDisconnect are set to callbacks passed from the caller.
diff --git a/ui/src/common/recordingV2/adb_connection_over_websocket.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_websocket.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
index 160b257..9c9d139 100644
--- a/ui/src/common/recordingV2/adb_connection_over_websocket.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_websocket.ts
@@ -12,8 +12,8 @@
 // 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 {defer, Deferred} from '../../../base/deferred';
+import {utf8Decode} from '../../../base/string_utils';
 import {AdbConnectionImpl} from './adb_connection_impl';
 import {RecordingError} from './recording_error_handling';
 import {
diff --git a/ui/src/common/recordingV2/adb_connection_over_webusb.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
similarity index 98%
rename from ui/src/common/recordingV2/adb_connection_over_webusb.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
index 713d8b3..715d366 100644
--- a/ui/src/common/recordingV2/adb_connection_over_webusb.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_connection_over_webusb.ts
@@ -12,11 +12,11 @@
 // 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 {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';
diff --git a/ui/src/common/recordingV2/adb_file_handler.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
similarity index 94%
rename from ui/src/common/recordingV2/adb_file_handler.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
index 1016fe7..078726f 100644
--- a/ui/src/common/recordingV2/adb_file_handler.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/adb_file_handler.ts
@@ -12,16 +12,16 @@
 // 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 {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';
+import {utf8Decode} from '../../../base/string_utils';
 
 // https://cs.android.com/android/platform/superproject/+/main:packages/
 // modules/adb/file_sync_protocol.h;l=144
diff --git a/ui/src/common/recordingV2/auth/adb_auth.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
similarity index 97%
rename from ui/src/common/recordingV2/auth/adb_auth.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
index aec8752..7ed275e 100644
--- a/ui/src/common/recordingV2/auth/adb_auth.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_auth.ts
@@ -13,12 +13,12 @@
 // limitations under the License.
 
 import {BigInteger, RSAKey} from 'jsbn-rsa';
-import {assertExists, assertTrue} from '../../../base/logging';
+import {assertExists, assertTrue} from '../../../../base/logging';
 import {
   base64Decode,
   base64Encode,
   hexEncode,
-} from '../../../base/string_utils';
+} from '../../../../base/string_utils';
 import {RecordingError} from '../recording_error_handling';
 
 const WORD_SIZE = 4;
diff --git a/ui/src/common/recordingV2/auth/adb_key_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
similarity index 98%
rename from ui/src/common/recordingV2/auth/adb_key_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
index 53e233f..0ce297b 100644
--- a/ui/src/common/recordingV2/auth/adb_key_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/auth/adb_key_manager.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assetSrc} from '../../../base/assets';
+import {assetSrc} from '../../../../base/assets';
 import {AdbKey} from './adb_auth';
 
 function isPasswordCredential(
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/common/recordingV2/chrome_traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
similarity index 95%
rename from ui/src/common/recordingV2/chrome_traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
index f8ecd03..9461190 100644
--- a/ui/src/common/recordingV2/chrome_traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/chrome_traced_tracing_session.ts
@@ -12,28 +12,28 @@
 // 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 {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';
+} from '../chrome_proxy_record_controller';
 import {
   isDisableTracingResponse,
   isEnableTracingResponse,
   isFreeBuffersResponse,
   isGetTraceStatsResponse,
   isReadBuffersResponse,
-} from '../../controller/consumer_port_types';
+} from '../consumer_port_types';
 import {
   EnableTracingRequest,
   IBufferStats,
   ISlice,
   TraceConfig,
-} from '../../protos';
+} from '../../../protos';
 import {RecordingError} from './recording_error_handling';
 import {
   TracingSession,
diff --git a/ui/src/common/recordingV2/host_os_byte_stream.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
similarity index 97%
rename from ui/src/common/recordingV2/host_os_byte_stream.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
index 3c43630..a03b791 100644
--- a/ui/src/common/recordingV2/host_os_byte_stream.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/host_os_byte_stream.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {defer} from '../../base/deferred';
+import {defer} from '../../../base/deferred';
 import {
   ByteStream,
   OnStreamCloseCallback,
diff --git a/ui/src/common/recordingV2/recording_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
similarity index 98%
rename from ui/src/common/recordingV2/recording_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
index bc3262e..e4eca50 100644
--- a/ui/src/common/recordingV2/recording_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils.ts
@@ -12,10 +12,10 @@
 // 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 {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,
@@ -41,7 +41,7 @@
   TraceConfig,
   TrackEventConfig,
   VmstatCounters,
-} from '../../protos';
+} from '../../../protos';
 import {TargetInfo} from './recording_interfaces_v2';
 import PerfClock = PerfEvents.PerfClock;
 import Timebase = PerfEvents.Timebase;
@@ -463,7 +463,7 @@
 
     if (uiCfg.androidStatsdPushedAtoms.length > 0) {
       ds.config.statsdTracingConfig.pushAtomId =
-        uiCfg.androidStatsdPushedAtoms.map((atom) => atom as any as AtomId);
+        uiCfg.androidStatsdPushedAtoms.map((atom) => atom as unknown as AtomId);
     }
 
     const needPulledAtomConfig =
@@ -471,7 +471,7 @@
       uiCfg.androidStatsdPulledAtoms.length > 0;
 
     if (needPulledAtomConfig) {
-      let pullAtomConfig = new StatsdPullAtomConfig();
+      const pullAtomConfig = new StatsdPullAtomConfig();
       if (uiCfg.androidStatsdRawPulledAtoms.length > 0) {
         for (const line of uiCfg.androidStatsdRawPulledAtoms.split('\n')) {
           if (line.trim().length > 0) {
@@ -479,8 +479,9 @@
           }
         }
       }
-      pullAtomConfig.pullAtomId =
-        uiCfg.androidStatsdPulledAtoms.map((atom) => atom as any as AtomId);
+      pullAtomConfig.pullAtomId = uiCfg.androidStatsdPulledAtoms.map(
+        (atom) => atom as unknown as AtomId,
+      );
       pullAtomConfig.pullFrequencyMs =
         uiCfg.androidStatsdPulledAtomPullFrequencyMs;
       if (uiCfg.androidStatsdPulledAtomPackages.length > 0) {
diff --git a/ui/src/common/recordingV2/recording_config_utils_unittest.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
similarity index 96%
rename from ui/src/common/recordingV2/recording_config_utils_unittest.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
index 67ac112..dd96a69 100644
--- a/ui/src/common/recordingV2/recording_config_utils_unittest.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_config_utils_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyRecordConfig} from '../../controller/record_config_types';
+import {createEmptyRecordConfig} from '../record_config_types';
 import {genTraceConfig} from './recording_config_utils';
 import {AndroidTargetInfo} from './recording_interfaces_v2';
 
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/common/recordingV2/recording_interfaces_v2.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
similarity index 99%
rename from ui/src/common/recordingV2/recording_interfaces_v2.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
index 954a145..c8a030e 100644
--- a/ui/src/common/recordingV2/recording_interfaces_v2.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_interfaces_v2.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TraceConfig} from '../../protos';
+import {TraceConfig} from '../../../protos';
 
 // TargetFactory connects, disconnects and keeps track of targets.
 // There is one factory for AndroidWebusb, AndroidWebsocket, Chrome etc.
diff --git a/ui/src/common/recordingV2/recording_page_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
similarity index 94%
rename from ui/src/common/recordingV2/recording_page_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
index de568aa..76617d5 100644
--- a/ui/src/common/recordingV2/recording_page_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/recording_page_controller.ts
@@ -12,19 +12,17 @@
 // 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 '../../controller/recording_manager';
-import {AppImpl} from '../../core/app_impl';
-import {raf} from '../../core/raf_scheduler';
-import {autosaveConfigStore} from '../../frontend/record_config';
+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 '../../frontend/recording/recording_ui_utils';
-import {couldNotClaimInterface} from '../../frontend/recording/reset_interface_modal';
-import {TraceConfig} from '../../protos';
-import {TRACE_SUFFIX} from '../constants';
+} 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 {
@@ -47,6 +45,8 @@
   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:
@@ -250,6 +250,7 @@
 // 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
@@ -267,7 +268,8 @@
   // transitions don't override one another in async functions.
   private stateGeneration = 0;
 
-  constructor(recMgr: RecordingManager) {
+  constructor(app: App, recMgr: RecordingManager) {
+    this.app = app;
     this.recMgr = recMgr;
   }
 
@@ -296,7 +298,7 @@
     }
     this.setState(state);
     this.recMgr.setRecordingStatus(undefined);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   maybeClearRecordingState(tracingSessionWrapper: TracingSessionWrapper): void {
@@ -312,7 +314,7 @@
     if (this.tracingSessionWrapper !== tracingSessionWrapper) {
       return;
     }
-    AppImpl.instance.openTraceFromBuffer({
+    this.app.openTraceFromBuffer({
       title: 'Recorded trace',
       buffer: trace.buffer,
       fileName: `trace_${currentDateHourAndMinute()}${TRACE_SUFFIX}`,
@@ -390,11 +392,11 @@
 
     if (!this.target) {
       this.setState(RecordingState.NO_TARGET);
-      raf.scheduleFullRedraw();
+      scheduleFullRedraw();
       return;
     }
     this.setState(RecordingState.TARGET_SELECTED);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
 
     this.tracingSessionWrapper = this.createTracingSessionWrapper(this.target);
     this.tracingSessionWrapper.fetchTargetInfo();
@@ -431,7 +433,7 @@
 
     const target = this.getTarget();
     const targetInfo = target.getInfo();
-    AppImpl.instance.analytics.logEvent(
+    this.app.analytics.logEvent(
       'Record Trace',
       `Record trace (${targetInfo.targetType})`,
     );
@@ -484,7 +486,7 @@
     // We redraw if:
     // 1. We received a correct buffer usage value.
     // 2. We receive a RecordingError.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   initFactories() {
@@ -531,7 +533,7 @@
     // 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();
+      scheduleFullRedraw();
       return;
     }
     // If the change happens to a new target or the controller does not have a
@@ -552,7 +554,7 @@
     this.recMgr.setRecordingStatus(undefined);
     // Redrawing because this method has changed the RecordingState, which will
     // affect the display of the record_page.
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
   }
 
   private setState(state: RecordingState) {
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/common/recordingV2/target_factories/android_websocket_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
index 21097eb..03cda1f 100644
--- a/ui/src/common/recordingV2/target_factories/android_websocket_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_websocket_target_factory.ts
@@ -12,7 +12,6 @@
 // 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,
@@ -22,7 +21,6 @@
   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';
@@ -268,8 +266,3 @@
     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_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/common/recordingV2/target_factories/android_webusb_target_factory.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
similarity index 89%
rename from ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
index d27ab07..a969c31 100644
--- a/ui/src/common/recordingV2/target_factories/android_webusb_target_factory.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factories/android_webusb_target_factory.ts
@@ -12,9 +12,8 @@
 // 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 {getErrorMessage} from '../../../../base/errors';
+import {assertExists} from '../../../../base/logging';
 import {AdbKeyManager} from '../auth/adb_key_manager';
 import {RecordingError} from '../recording_error_handling';
 import {
@@ -23,7 +22,6 @@
   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';
@@ -155,11 +153,3 @@
     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_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/common/recordingV2/target_factory_registry.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
similarity index 96%
rename from ui/src/common/recordingV2/target_factory_registry.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
index e8de655..b34070d 100644
--- a/ui/src/common/recordingV2/target_factory_registry.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/target_factory_registry.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Registry} from '../../base/registry';
+import {Registry} from '../../../base/registry';
 import {RecordingTargetV2, TargetFactory} from './recording_interfaces_v2';
 
 export class TargetFactoryRegistry extends Registry<TargetFactory> {
diff --git a/ui/src/common/recordingV2/targets/android_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
index 926846d..0bac1e4 100644
--- a/ui/src/common/recordingV2/targets/android_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_target.ts
@@ -12,9 +12,9 @@
 // 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 {fetchWithTimeout} from '../../../../base/http_utils';
+import {exists} from '../../../../base/utils';
+import {VERSION} from '../../../../gen/perfetto_version';
 import {AdbConnectionImpl} from '../adb_connection_impl';
 import {
   DataSource,
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/common/recordingV2/targets/android_webusb_target.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
similarity index 96%
rename from ui/src/common/recordingV2/targets/android_webusb_target.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
index e70a19a..dc6e64d 100644
--- a/ui/src/common/recordingV2/targets/android_webusb_target.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/targets/android_webusb_target.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../../../base/logging';
+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';
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/common/recordingV2/traced_tracing_session.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
similarity index 98%
rename from ui/src/common/recordingV2/traced_tracing_session.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
index c0ba444..8687432 100644
--- a/ui/src/common/recordingV2/traced_tracing_session.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/traced_tracing_session.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import protobuf from 'protobufjs/minimal';
-import {defer, Deferred} from '../../base/deferred';
-import {assertExists, assertFalse, assertTrue} from '../../base/logging';
+import {defer, Deferred} from '../../../base/deferred';
+import {assertExists, assertFalse, assertTrue} from '../../../base/logging';
 import {
   DisableTracingRequest,
   DisableTracingResponse,
@@ -33,7 +33,7 @@
   ReadBuffersRequest,
   ReadBuffersResponse,
   TraceConfig,
-} from '../../protos';
+} from '../../../protos';
 import {RecordingError} from './recording_error_handling';
 import {
   ByteStream,
@@ -50,7 +50,7 @@
   PARSING_UNRECOGNIZED_PORT,
   RECORDING_IN_PROGRESS,
 } from './recording_utils';
-import {exists} from '../../base/utils';
+import {exists} from '../../../base/utils';
 
 // See wire_protocol.proto for more details.
 const WIRE_PROTOCOL_HEADER_SIZE = 4;
diff --git a/ui/src/common/recordingV2/websocket_menu_controller.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
similarity index 97%
rename from ui/src/common/recordingV2/websocket_menu_controller.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
index 8b800a7..2da8f5b 100644
--- a/ui/src/common/recordingV2/websocket_menu_controller.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recordingV2/websocket_menu_controller.ts
@@ -16,7 +16,7 @@
   ADB_ENDPOINT,
   DEFAULT_WEBSOCKET_URL,
   TRACED_ENDPOINT,
-} from '../../frontend/recording/recording_ui_utils';
+} from '../recording_ui_utils';
 import {TargetFactory} from './recording_interfaces_v2';
 import {
   ANDROID_WEBSOCKET_TARGET_FACTORY,
diff --git a/ui/src/controller/recording_manager.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
similarity index 87%
rename from ui/src/controller/recording_manager.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
index 5d96017..be29691 100644
--- a/ui/src/controller/recording_manager.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_manager.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {createEmptyState} from '../common/empty_state';
+import {createEmptyState} from './empty_state';
 import {
   AdbRecordingTarget,
   LoadedConfig,
@@ -20,39 +20,41 @@
   RecordingTarget,
   getDefaultRecordingTargets,
   isAdbTarget,
-} from '../common/state';
-import {RECORDING_V2_FLAG} from '../core/feature_flags';
-import {raf} from '../core/raf_scheduler';
+} 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;
 
-  // TODO(primiano): this singleton is temporary. RecordingManager shoudl be
-  // injected in all the recording pages and the instance should be created and
-  // owned by the recording plugin. But for now we don't have a plugin.
-  private static _instance: RecordingManager | undefined = undefined;
-  static get instance() {
-    if (this._instance === undefined) {
-      this._instance = new RecordingManager();
-    }
-    return this._instance;
-  }
-
-  constructor() {
+  constructor(app: App, useRecordingV2: boolean) {
+    this.app = app;
     const extensionLocalChannel = new MessageChannel();
-    this.recCtl = new RecordController(this, extensionLocalChannel.port1);
+    this.recCtl = new RecordController(app, this, extensionLocalChannel.port1);
     this.setupExtentionPort(extensionLocalChannel);
 
-    if (!RECORDING_V2_FLAG.get()) {
+    if (useRecordingV2) {
+      targetFactoryRegistry.register(new AndroidWebsocketTargetFactory());
+      if (exists(navigator.usb)) {
+        targetFactoryRegistry.register(
+          new AndroidWebusbTargetFactory(navigator.usb),
+        );
+      }
+    } else {
       this.updateAvailableAdbDevices();
       try {
         navigator.usb.addEventListener('connect', () =>
@@ -156,7 +158,7 @@
         (message: object, _port: chrome.runtime.Port) => {
           if (isGetCategoriesResponse(message)) {
             this._state.chromeCategories = message.categories;
-            raf.scheduleFullRedraw();
+            scheduleFullRedraw();
             return;
           }
           extensionLocalChannel.port2.postMessage(message);
@@ -191,7 +193,7 @@
 
     this.setAvailableAdbDevices(availableAdbDevices);
     this.selectAndroidDeviceIfAvailable(availableAdbDevices, recordingTarget);
-    raf.scheduleFullRedraw();
+    scheduleFullRedraw();
     return availableAdbDevices;
   }
 
diff --git a/ui/src/frontend/recording/recording_multiple_choice.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
similarity index 93%
rename from ui/src/frontend/recording/recording_multiple_choice.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
index 27a83fc..0e34f5c 100644
--- a/ui/src/frontend/recording/recording_multiple_choice.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_multiple_choice.ts
@@ -16,9 +16,9 @@
 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';
+} 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 {
diff --git a/ui/src/frontend/recording/recording_sections.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
similarity index 86%
rename from ui/src/frontend/recording/recording_sections.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
index 03ebff8..c83b9e0 100644
--- a/ui/src/frontend/recording/recording_sections.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_sections.ts
@@ -12,8 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {DataSource} from '../../common/recordingV2/recording_interfaces_v2';
-import {RecordingState} from '../../common/state';
+import {DataSource} from './recordingV2/recording_interfaces_v2';
+import {RecordingState} from './state';
 
 export interface RecordingSectionAttrs {
   recState: RecordingState;
diff --git a/ui/src/frontend/recording/recording_settings.ts b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
similarity index 96%
rename from ui/src/frontend/recording/recording_settings.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
index c61b49d..e3058be 100644
--- a/ui/src/frontend/recording/recording_settings.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/recording_settings.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {RecordMode} from '../../common/state';
-import {Slider} from '../record_widgets';
+import {RecordMode} from './state';
+import {Slider} from './record_widgets';
 import {RecordingSectionAttrs} from './recording_sections';
 import {assetSrc} from '../../base/assets';
 
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/frontend/recording/reset_interface_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
similarity index 100%
rename from ui/src/frontend/recording/reset_interface_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_interface_modal.ts
diff --git a/ui/src/frontend/recording/reset_target_modal.ts b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
similarity index 91%
rename from ui/src/frontend/recording/reset_target_modal.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
index 4d3feb3..4d3d048 100644
--- a/ui/src/frontend/recording/reset_target_modal.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/reset_target_modal.ts
@@ -13,19 +13,19 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {RecordingPageController} from '../../common/recordingV2/recording_page_controller';
+import {RecordingPageController} from './recordingV2/recording_page_controller';
 import {
   EXTENSION_URL,
   RECORDING_MODAL_DIALOG_KEY,
-} from '../../common/recordingV2/recording_utils';
+} from './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';
+} 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 {CodeSnippet} from './record_widgets';
 import {RecordingMultipleChoice} from './recording_multiple_choice';
 
 const RUN_WEBSOCKET_CMD =
diff --git a/ui/src/common/state.ts b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
similarity index 98%
rename from ui/src/common/state.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/state.ts
index 9c361f6..b94074b 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/state.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {RecordConfig} from '../controller/record_config_types';
+import {RecordConfig} from './record_config_types';
 
 export const MAX_TIME = 180;
 
diff --git a/ui/src/core/trace_config_utils.ts b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
similarity index 96%
rename from ui/src/core/trace_config_utils.ts
rename to ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
index e05f711..c7697dd 100644
--- a/ui/src/core/trace_config_utils.ts
+++ b/ui/src/plugins/dev.perfetto.RecordTrace/trace_config_utils.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EnableTracingRequest, TraceConfig} from '../protos';
+import {EnableTracingRequest, TraceConfig} from '../../protos';
 
 // In this file are contained a few functions to simplify the proto parsing.
 
diff --git a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
index 328d386..02ff0b2 100644
--- a/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/active_cpu_count.ts
@@ -33,10 +33,7 @@
   private readonly cpuType?: CPUType;
 
   constructor(ctx: TrackContext, trace: Trace, cpuType?: CPUType) {
-    super({
-      trace,
-      uri: ctx.trackUri,
-    });
+    super(trace, ctx.trackUri);
     this.cpuType = cpuType;
   }
 
diff --git a/ui/src/plugins/dev.perfetto.Sched/index.ts b/ui/src/plugins/dev.perfetto.Sched/index.ts
index a72c0f3..8be599d 100644
--- a/ui/src/plugins/dev.perfetto.Sched/index.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/index.ts
@@ -17,7 +17,10 @@
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
 import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
-import {RunnableThreadCountTrack} from './runnable_thread_count';
+import {
+  RunnableThreadCountTrack,
+  UninterruptibleSleepThreadCountTrack,
+} from './thread_count';
 import {getSchedTable} from './table';
 import {extensions} from '../../public/lib/extensions';
 
@@ -28,10 +31,7 @@
     ctx.tracks.registerTrack({
       uri: runnableThreadCountUri,
       title: 'Runnable thread count',
-      track: new RunnableThreadCountTrack({
-        trace: ctx,
-        uri: runnableThreadCountUri,
-      }),
+      track: new RunnableThreadCountTrack(ctx, runnableThreadCountUri),
     });
     ctx.commands.registerCommand({
       id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
@@ -40,6 +40,26 @@
         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(
+        ctx,
+        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({
diff --git a/ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
similarity index 71%
rename from ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts
rename to ui/src/plugins/dev.perfetto.Sched/thread_count.ts
index 9b5e9c5..73e7ee2 100644
--- a/ui/src/plugins/dev.perfetto.Sched/runnable_thread_count.ts
+++ b/ui/src/plugins/dev.perfetto.Sched/thread_count.ts
@@ -16,11 +16,11 @@
   BaseCounterTrack,
   CounterOptions,
 } from '../../frontend/base_counter_track';
-import {NewTrackArgs} from '../../frontend/track';
+import {Trace} from '../../public/trace';
 
-export class RunnableThreadCountTrack extends BaseCounterTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
+abstract class ThreadCountTrack extends BaseCounterTrack {
+  constructor(trace: Trace, uri: string) {
+    super(trace, uri);
   }
 
   protected getDefaultCounterOptions(): CounterOptions {
@@ -35,7 +35,9 @@
       `INCLUDE PERFETTO MODULE sched.thread_level_parallelism`,
     );
   }
+}
 
+export class RunnableThreadCountTrack extends ThreadCountTrack {
   getSqlSource() {
     return `
       select
@@ -45,3 +47,14 @@
     `;
   }
 }
+
+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
index 68c7d64..7cadbf7 100644
--- a/ui/src/plugins/dev.perfetto.Screenshots/index.ts
+++ b/ui/src/plugins/dev.perfetto.Screenshots/index.ts
@@ -35,10 +35,7 @@
       ctx.tracks.registerTrack({
         uri,
         title,
-        track: new ScreenshotsTrack({
-          trace: ctx,
-          uri,
-        }),
+        track: new ScreenshotsTrack(ctx, uri),
         tags: {
           kind: ScreenshotsTrack.kind,
         },
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/index.ts b/ui/src/plugins/dev.perfetto.SqlModules/index.ts
new file mode 100644
index 0000000..f2fde8d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SqlModules/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 {assetSrc} from '../../base/assets';
+import {assertExists} from '../../base/logging';
+import {PerfettoPlugin} from '../../public/plugin';
+import {SqlModules} from './sql_modules';
+import {SQL_MODULES_DOCS_SCHEMA, SqlModulesImpl} from './sql_modules_impl';
+
+export default class implements PerfettoPlugin {
+  static readonly id = 'dev.perfetto.SqlModules';
+  private sqlModules?: SqlModules;
+
+  async onTraceLoad() {
+    const resp = await fetch(assetSrc('stdlib_docs.json'));
+    const json = await resp.json();
+    const docs = SQL_MODULES_DOCS_SCHEMA.parse(json);
+    this.sqlModules = new SqlModulesImpl(docs);
+  }
+
+  getSqlModules() {
+    return assertExists(this.sqlModules);
+  }
+}
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
new file mode 100644
index 0000000..9f0503a
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules.ts
@@ -0,0 +1,90 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Handles the access to all of the Perfetto SQL modules accessible to Trace
+//  Processor.
+export interface SqlModules {
+  // Returns names of all tables/views between all loaded Perfetto SQL modules.
+  listTables(): string[];
+
+  // Returns Perfetto SQL table/view if it was loaded in one of the Perfetto
+  // SQL module.
+  getTable(tableName: string): SqlTable | undefined;
+}
+
+// Handles the access to a specific Perfetto SQL Package. Package consists of
+// Perfetto SQL modules.
+export interface SqlPackage {
+  readonly name: string;
+  readonly modules: SqlModule[];
+  listTables(): string[];
+  getTable(tableName: string): SqlTable | undefined;
+}
+
+// Handles the access to a specific Perfetto SQL module.
+export interface SqlModule {
+  readonly includeKey: string;
+  readonly dataObjects: SqlTable[];
+  readonly functions: SqlFunction[];
+  readonly tableFunctions: SqlTableFunction[];
+  readonly macros: SqlMacro[];
+}
+
+// The definition of Perfetto SQL table/view.
+export interface SqlTable {
+  readonly name: string;
+  readonly description: string;
+  readonly type: string;
+  readonly columns: SqlColumn[];
+}
+
+// The definition of Perfetto SQL function.
+export interface SqlFunction {
+  readonly name: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnType: string;
+  readonly returnDesc: string;
+}
+
+// The definition of Perfetto SQL table function.
+export interface SqlTableFunction {
+  readonly name: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnCols: SqlColumn[];
+}
+
+// The definition of Perfetto SQL macro.
+export interface SqlMacro {
+  readonly name: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnType: string;
+}
+
+// The definition of Perfetto SQL column.
+export interface SqlColumn {
+  readonly name: string;
+  readonly description: string;
+  readonly type: string;
+}
+
+// The definition of Perfetto SQL argument. Can be used for functions, table
+// functions or macros.
+export interface SqlArgument {
+  readonly name: string;
+  readonly description: string;
+  readonly type: string;
+}
diff --git a/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
new file mode 100644
index 0000000..ee6fe79
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.SqlModules/sql_modules_impl.ts
@@ -0,0 +1,254 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 {
+  SqlModules,
+  SqlColumn,
+  SqlFunction,
+  SqlArgument,
+  SqlMacro,
+  SqlModule,
+  SqlPackage,
+  SqlTable,
+  SqlTableFunction,
+} from './sql_modules';
+
+export class SqlModulesImpl implements SqlModules {
+  readonly packages: SqlPackage[];
+
+  constructor(docs: SqlModulesDocsSchema) {
+    this.packages = docs.map((json) => new StdlibPackageImpl(json));
+  }
+
+  listTables(): string[] {
+    const tables: string[] = [];
+    for (const stdlibPackage of this.packages) {
+      tables.concat(stdlibPackage.listTables());
+    }
+    return tables;
+  }
+
+  getTable(tableName: string): SqlTable | undefined {
+    for (const stdlibPackage of this.packages) {
+      const maybeTable = stdlibPackage.getTable(tableName);
+      if (maybeTable) {
+        return maybeTable;
+      }
+    }
+    return undefined;
+  }
+}
+
+export class StdlibPackageImpl implements SqlPackage {
+  readonly name: string;
+  readonly modules: SqlModule[];
+
+  constructor(docs: DocsPackageSchemaType) {
+    this.name = docs.name;
+    this.modules = [];
+    for (const moduleJson of docs.modules) {
+      this.modules.push(new StdlibModuleImpl(moduleJson));
+    }
+  }
+
+  getTable(tableName: string): SqlTable | undefined {
+    for (const module of this.modules) {
+      for (const dataObj of module.dataObjects) {
+        if (dataObj.name == tableName) {
+          return dataObj;
+        }
+      }
+    }
+    return undefined;
+  }
+
+  listTables(): string[] {
+    return this.modules.flatMap((module) =>
+      module.dataObjects.map((dataObj) => dataObj.name),
+    );
+  }
+}
+
+export class StdlibModuleImpl implements SqlModule {
+  readonly includeKey: string;
+  readonly dataObjects: SqlTable[];
+  readonly functions: SqlFunction[];
+  readonly tableFunctions: SqlTableFunction[];
+  readonly macros: SqlMacro[];
+
+  constructor(docs: DocsModuleSchemaType) {
+    this.includeKey = docs.module_name;
+    this.dataObjects = docs.data_objects.map(
+      (json) => new StdlibDataObjectImpl(json),
+    );
+    this.functions = docs.functions.map((json) => new StdlibFunctionImpl(json));
+    this.tableFunctions = docs.table_functions.map(
+      (json) => new StdlibTableFunctionImpl(json),
+    );
+    this.macros = docs.macros.map((json) => new StdlibMacroImpl(json));
+  }
+}
+
+class StdlibMacroImpl implements SqlMacro {
+  readonly name: string;
+  readonly summaryDesc: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnType: string;
+
+  constructor(docs: DocsMacroSchemaType) {
+    this.name = docs.name;
+    this.summaryDesc = docs.summary_desc;
+    this.description = docs.desc;
+    this.returnType = docs.return_type;
+    this.args = [];
+    this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json));
+  }
+}
+
+class StdlibTableFunctionImpl implements SqlTableFunction {
+  readonly name: string;
+  readonly summaryDesc: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnCols: SqlColumn[];
+
+  constructor(docs: DocsTableFunctionSchemaType) {
+    this.name = docs.name;
+    this.summaryDesc = docs.summary_desc;
+    this.description = docs.desc;
+    this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json));
+    this.returnCols = docs.cols.map((json) => new StdlibColumnImpl(json));
+  }
+}
+
+class StdlibFunctionImpl implements SqlFunction {
+  readonly name: string;
+  readonly summaryDesc: string;
+  readonly description: string;
+  readonly args: SqlArgument[];
+  readonly returnType: string;
+  readonly returnDesc: string;
+
+  constructor(docs: DocsFunctionSchemaType) {
+    this.name = docs.name;
+    this.summaryDesc = docs.summary_desc;
+    this.description = docs.desc;
+    this.returnType = docs.return_type;
+    this.returnDesc = docs.return_desc;
+    this.args = docs.args.map((json) => new StdlibFunctionArgImpl(json));
+  }
+}
+
+class StdlibDataObjectImpl implements SqlTable {
+  name: string;
+  description: string;
+  type: string;
+  columns: SqlColumn[];
+
+  constructor(docs: DocsDataObjectSchemaType) {
+    this.name = docs.name;
+    this.description = docs.desc;
+    this.type = docs.type;
+    this.columns = docs.cols.map((json) => new StdlibColumnImpl(json));
+  }
+}
+
+class StdlibColumnImpl implements SqlColumn {
+  name: string;
+  type: string;
+  description: string;
+
+  constructor(docs: DocsArgOrColSchemaType) {
+    this.type = docs.type;
+    this.description = docs.desc;
+    this.name = docs.name;
+  }
+}
+
+class StdlibFunctionArgImpl implements SqlArgument {
+  name: string;
+  description: string;
+  type: string;
+
+  constructor(docs: DocsArgOrColSchemaType) {
+    this.type = docs.type;
+    this.description = docs.desc;
+    this.name = docs.name;
+  }
+}
+
+const ARG_OR_COL_SCHEMA = z.object({
+  name: z.string(),
+  type: z.string(),
+  desc: z.string(),
+});
+type DocsArgOrColSchemaType = z.infer<typeof ARG_OR_COL_SCHEMA>;
+
+const DATA_OBJECT_SCHEMA = z.object({
+  name: z.string(),
+  desc: z.string(),
+  summary_desc: z.string(),
+  type: z.string(),
+  cols: z.array(ARG_OR_COL_SCHEMA),
+});
+type DocsDataObjectSchemaType = z.infer<typeof DATA_OBJECT_SCHEMA>;
+
+const FUNCTION_SCHEMA = z.object({
+  name: z.string(),
+  desc: z.string(),
+  summary_desc: z.string(),
+  args: z.array(ARG_OR_COL_SCHEMA),
+  return_type: z.string(),
+  return_desc: z.string(),
+});
+type DocsFunctionSchemaType = z.infer<typeof FUNCTION_SCHEMA>;
+
+const TABLE_FUNCTION_SCHEMA = z.object({
+  name: z.string(),
+  desc: z.string(),
+  summary_desc: z.string(),
+  args: z.array(ARG_OR_COL_SCHEMA),
+  cols: z.array(ARG_OR_COL_SCHEMA),
+});
+type DocsTableFunctionSchemaType = z.infer<typeof TABLE_FUNCTION_SCHEMA>;
+
+const MACRO_SCHEMA = z.object({
+  name: z.string(),
+  desc: z.string(),
+  summary_desc: z.string(),
+  return_desc: z.string(),
+  return_type: z.string(),
+  args: z.array(ARG_OR_COL_SCHEMA),
+});
+type DocsMacroSchemaType = z.infer<typeof MACRO_SCHEMA>;
+
+const MODULE_SCHEMA = z.object({
+  module_name: z.string(),
+  data_objects: z.array(DATA_OBJECT_SCHEMA),
+  functions: z.array(FUNCTION_SCHEMA),
+  table_functions: z.array(TABLE_FUNCTION_SCHEMA),
+  macros: z.array(MACRO_SCHEMA),
+});
+type DocsModuleSchemaType = z.infer<typeof MODULE_SCHEMA>;
+
+const PACKAGE_SCHEMA = z.object({
+  name: z.string(),
+  modules: z.array(MODULE_SCHEMA),
+});
+type DocsPackageSchemaType = z.infer<typeof PACKAGE_SCHEMA>;
+
+export const SQL_MODULES_DOCS_SCHEMA = z.array(PACKAGE_SCHEMA);
+export type SqlModulesDocsSchema = z.infer<typeof SQL_MODULES_DOCS_SCHEMA>;
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/index.ts b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
index a106654..155b35f 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
@@ -84,13 +84,7 @@
         chips: removeFalsyValues([
           isKernelThread === 0 && isMainThread === 1 && 'main thread',
         ]),
-        track: new ThreadStateTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          utid,
-        ),
+        track: new ThreadStateTrack(ctx, uri, utid),
       });
 
       const group = getOrCreateGroupForThread(ctx.workspace, utid);
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
index 0a2287b7..1ba8c3b 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_details_panel.ts
@@ -27,8 +27,8 @@
   getThreadStateFromConstraints,
   ThreadState,
 } from '../../trace_processor/sql_utils/thread_state';
-import {DurationWidget, renderDuration} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {getProcessName} from '../../trace_processor/sql_utils/process';
 import {
   getFullThreadName,
@@ -42,6 +42,7 @@
 import {goToSchedSlice} from '../../frontend/widgets/sched';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
 import {Trace} from '../../public/trace';
+import {formatDuration} from '../../public/lib/time_utils';
 
 interface RelatedThreadStates {
   prev?: ThreadState;
@@ -225,7 +226,7 @@
       });
 
     const nameForNextOrPrev = (threadState: ThreadState) =>
-      `${threadState.state} for ${renderDuration(threadState.dur)}`;
+      `${threadState.state} for ${formatDuration(this.trace, threadState.dur)}`;
 
     const renderWaker = (related: RelatedThreadStates) => {
       // Could be absent if:
@@ -274,7 +275,7 @@
           m(TreeNode, {
             left: m(Timestamp, {
               ts: state.ts,
-              display: `+${renderDuration(state.ts - startTs)}`,
+              display: `+${formatDuration(this.trace, state.ts - startTs)}`,
             }),
             right: renderRef(state, getFullThreadName(state.thread)),
           }),
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts
index 3935465..4fdb791 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/thread_state_track.ts
@@ -22,12 +22,12 @@
   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';
+import {Trace} from '../../public/trace';
 
 export const THREAD_STATE_ROW = {
   ...BASE_ROW,
@@ -41,10 +41,11 @@
   protected sliceLayout: SliceLayout = {...SLICE_LAYOUT_FLAT_DEFAULTS};
 
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     private utid: number,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   // This is used by the base class to call iter().
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index aef7a76..37206a3 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -272,6 +272,7 @@
   private onmessage(msg: MessageEvent) {
     if (this._ctx === undefined) return; // Trace unloaded
     if (!('perfettoSync' in msg.data)) return;
+    this._ctx.scheduleFullRedraw('force');
     const msgData = msg.data as SyncMessage;
     const sync = msgData.perfettoSync;
     switch (sync.cmd) {
diff --git a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
index 7bbcace..a5ff6d7 100644
--- a/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
+++ b/ui/src/plugins/dev.perfetto.WidgetsPage/widgets_page.ts
@@ -43,7 +43,6 @@
 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';
@@ -686,6 +685,7 @@
             icon: arg(icon, 'send'),
             rightIcon: arg(rightIcon, 'arrow_forward'),
             label: arg(label, 'Button', ''),
+            onclick: () => alert('button pressed'),
             ...rest,
           }),
         initialOpts: {
@@ -904,30 +904,6 @@
         },
       }),
       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(
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts
index 6b96209..a2b7981 100644
--- a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/index.ts
@@ -39,10 +39,10 @@
         kind: CriticalUserInteractionTrack.kind,
       },
       title: 'Chrome Interactions',
-      track: new CriticalUserInteractionTrack({
-        trace: ctx,
-        uri: CriticalUserInteractionTrack.kind,
-      }),
+      track: new CriticalUserInteractionTrack(
+        ctx,
+        CriticalUserInteractionTrack.kind,
+      ),
     });
   }
 }
diff --git a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts
index 0c26623..813e2e6 100644
--- a/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeCriticalUserInteractions/startup_details_panel.ts
@@ -14,8 +14,8 @@
 
 import m from 'mithril';
 import {duration, Time, time} from '../../base/time';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/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';
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
index 25e49aa..58896c2 100644
--- 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
@@ -29,8 +29,8 @@
 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 {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
similarity index 99%
rename from ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
index cfcdf87..8d803e4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_details_panel.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 import {Duration, duration, Time, time} from '../../base/time';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
-import {renderDetails} from '../../frontend/slice_details';
+import {renderDetails} from '../../public/lib/details/slice_details';
 import {
   getDescendantSliceTree,
   getSlice,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_track.ts
similarity index 94%
rename from ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_track.ts
index 33b42b4..03ab372 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/event_latency_track.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {NamedRow} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public/track';
 import {
   CustomSqlTableDefConfig,
@@ -22,15 +21,17 @@
 import {JANK_COLOR} from './jank_colors';
 import {TrackEventSelection} from '../../public/selection';
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
+import {Trace} from '../../public/trace';
 
 export const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
 export class EventLatencyTrack extends CustomSqlTableSliceTrack {
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     private baseTable: string,
   ) {
-    super(args);
+    super(trace, uri);
   }
 
   getSqlSource(): string {
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
similarity index 73%
rename from ui/src/core_plugins/chrome_scroll_jank/index.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
index 19a0c70..24bd4e2 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/index.ts
@@ -21,50 +21,9 @@
 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';
-
-// 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';
-
-    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.`,
-        );
-      }
-
-      // Just remove the original flag
-      delete flags[enableScrollJankPluginV2FlagKey];
-      localStorage.setItem(flagsKey, JSON.stringify(flags));
-    }
-  } catch {
-    // Ignore - this was very much best-effort.
-  }
-}
-
-patchChromeScrollJankFlag();
 
 export default class implements PerfettoPlugin {
-  static readonly id = 'perfetto.ChromeScrollJank';
+  static readonly id = 'org.chromium.ChromeScrollJank';
   async onTraceLoad(ctx: Trace): Promise<void> {
     const group = new TrackNode({
       title: 'Chrome Scroll Jank',
@@ -89,16 +48,13 @@
       INCLUDE PERFETTO MODULE chrome.event_latency;
     `);
 
-    const uri = 'perfetto.ChromeScrollJank#toplevelScrolls';
+    const uri = 'org.chromium.ChromeScrollJank#toplevelScrolls';
     const title = 'Chrome Scrolls';
 
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new TopLevelScrollTrack({
-        trace: ctx,
-        uri,
-      }),
+      track: new TopLevelScrollTrack(ctx, uri),
     });
 
     const track = new TrackNode({uri, title});
@@ -201,13 +157,13 @@
     );
     await ctx.engine.query(tableDefSql);
 
-    const uri = 'perfetto.ChromeScrollJank#eventLatency';
+    const uri = 'org.chromium.ChromeScrollJank#eventLatency';
     const title = 'Chrome Scroll Input Latencies';
 
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new EventLatencyTrack({trace: ctx, uri}, baseTable),
+      track: new EventLatencyTrack(ctx, uri, baseTable),
     });
 
     const track = new TrackNode({uri, title});
@@ -222,16 +178,13 @@
       `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`,
     );
 
-    const uri = 'perfetto.ChromeScrollJank#scrollJankV3';
+    const uri = 'org.chromium.ChromeScrollJank#scrollJankV3';
     const title = 'Chrome Scroll Janks';
 
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new ScrollJankV3Track({
-        trace: ctx,
-        uri,
-      }),
+      track: new ScrollJankV3Track(ctx, uri),
     });
 
     const track = new TrackNode({uri, title});
diff --git a/ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/jank_colors.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/jank_colors.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_delta_graph.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_delta_graph.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
similarity index 98%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
index eed111f..e9efe97 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_details_panel.ts
@@ -21,8 +21,8 @@
   TableData,
   widgetColumn,
 } from '../../widgets/table';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {
   LONG,
   LONG_NULL,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_cause_link_utils.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_cause_link_utils.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_map.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_cause_map.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_map.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_cause_map.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
similarity index 98%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
index fc51eb4..3265e4f 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_details_panel.ts
@@ -17,8 +17,8 @@
 import {exists} from '../../base/utils';
 import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
 import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
-import {DurationWidget} from '../../frontend/widgets/duration';
-import {Timestamp} from '../../frontend/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
+import {Timestamp} from '../../public/lib/widgets/timestamp';
 import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_track.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_jank_v3_track.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/scroll_track.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/scroll_track.ts
diff --git a/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts b/ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
similarity index 100%
rename from ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts
rename to ui/src/plugins/org.chromium.ChromeScrollJank/selection_utils.ts
diff --git a/ui/src/plugins/org.chromium.ChromeTasks/track.ts b/ui/src/plugins/org.chromium.ChromeTasks/track.ts
index ff2f361..030af9e 100644
--- a/ui/src/plugins/org.chromium.ChromeTasks/track.ts
+++ b/ui/src/plugins/org.chromium.ChromeTasks/track.ts
@@ -25,9 +25,9 @@
   constructor(
     trace: Trace,
     uri: string,
-    private utid: Utid,
+    private readonly utid: Utid,
   ) {
-    super({trace, uri});
+    super(trace, uri);
   }
 
   getSqlDataSource(): CustomSqlTableDefConfig {
diff --git a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
index 0bca424..8d7ff66 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelSubsystems/index.ts
@@ -66,14 +66,7 @@
       ctx.tracks.registerTrack({
         uri,
         title,
-        track: new AsyncSliceTrack(
-          {
-            trace: ctx,
-            uri,
-          },
-          0,
-          [trackId],
-        ),
+        track: new AsyncSliceTrack(ctx, uri, 0, [trackId]),
         tags: {
           kind: SLICE_TRACK_KIND,
           trackIds: [trackId],
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
index 96b0ddb..973af15 100644
--- a/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/index.ts
@@ -14,7 +14,6 @@
 
 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';
@@ -31,12 +30,13 @@
 // TODO(stevegolton): Remove this?
 class SuspendResumeSliceTrack extends AsyncSliceTrack {
   constructor(
-    args: NewTrackArgs,
+    trace: Trace,
+    uri: string,
     maxDepth: number,
     trackIds: number[],
     private readonly threads: ThreadMap,
   ) {
-    super(args, maxDepth, trackIds);
+    super(trace, uri, maxDepth, trackIds);
   }
 
   onSliceClick(args: OnSliceClickArgs<Slice>) {
@@ -96,12 +96,7 @@
         trackIds,
         kind: SLICE_TRACK_KIND,
       },
-      track: new SuspendResumeSliceTrack(
-        {uri, trace: ctx},
-        maxDepth,
-        trackIds,
-        threads,
-      ),
+      track: new SuspendResumeSliceTrack(ctx, uri, maxDepth, trackIds, threads),
     });
 
     // Display the track in the UI.
diff --git a/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
index 74bc497..d43f699 100644
--- a/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
+++ b/ui/src/plugins/org.kernel.SuspendResumeLatency/suspend_resume_details.ts
@@ -19,8 +19,8 @@
 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 {Timestamp} from '../../public/lib/widgets/timestamp';
+import {DurationWidget} from '../../public/lib/widgets/duration';
 import {Anchor} from '../../widgets/anchor';
 import {Engine} from '../../trace_processor/engine';
 import {TrackEventDetailsPanel} from '../../public/details_panel';
diff --git a/ui/src/plugins/org.kernel.Wattson/index.ts b/ui/src/plugins/org.kernel.Wattson/index.ts
index 539366d..ebbe946 100644
--- a/ui/src/plugins/org.kernel.Wattson/index.ts
+++ b/ui/src/plugins/org.kernel.Wattson/index.ts
@@ -94,10 +94,7 @@
   readonly queryKey: string;
 
   constructor(trace: Trace, uri: string, queryKey: string) {
-    super({
-      trace,
-      uri,
-    });
+    super(trace, uri);
     this.queryKey = queryKey;
   }
 
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index 5dcd368..0c8321b 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -54,10 +54,18 @@
 
   // TODO(primiano): this should be needed in extremely rare cases. We should
   // probably switch to mithril auto-redraw at some point.
-  scheduleFullRedraw(): void;
+  scheduleFullRedraw(force?: 'force'): 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/frontend/slice_details.ts b/ui/src/public/lib/details/slice_details.ts
similarity index 79%
rename from ui/src/frontend/slice_details.ts
rename to ui/src/public/lib/details/slice_details.ts
index 784e5b1..4468660 100644
--- a/ui/src/frontend/slice_details.ts
+++ b/ui/src/public/lib/details/slice_details.ts
@@ -13,27 +13,27 @@
 // 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 {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 {
   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 {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
-import {assertExists} from '../base/logging';
-import {Trace} from '../public/trace';
-import {extensions} from '../public/lib/extensions';
+} from './thread_state';
+import {DurationWidget} from '../widgets/duration';
+import {renderProcessRef} from '../../../frontend/widgets/process';
+import {renderThreadRef} from '../../../frontend/widgets/thread';
+import {Timestamp} from '../widgets/timestamp';
+import {getSqlTableDescription} from '../../../frontend/widgets/sql/table/sql_table_registry';
+import {assertExists} from '../../../base/logging';
+import {Trace} from '../../trace';
+import {extensions} from '../extensions';
 
 // Renders a widget storing all of the generic details for a slice from the
 // slice table.
diff --git a/ui/src/frontend/sql/thread_state.ts b/ui/src/public/lib/details/thread_state.ts
similarity index 93%
rename from ui/src/frontend/sql/thread_state.ts
rename to ui/src/public/lib/details/thread_state.ts
index faa464e..6545db0 100644
--- a/ui/src/frontend/sql/thread_state.ts
+++ b/ui/src/public/lib/details/thread_state.ts
@@ -13,16 +13,16 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {duration, TimeSpan} from '../../base/time';
-import {Engine} from '../../trace_processor/engine';
+import {duration, TimeSpan} from '../../../base/time';
+import {Engine} from '../../../trace_processor/engine';
 import {
   LONG,
   NUM_NULL,
   STR,
   STR_NULL,
-} from '../../trace_processor/query_result';
-import {TreeNode} from '../../widgets/tree';
-import {Utid} from '../../trace_processor/sql_utils/core_types';
+} from '../../../trace_processor/query_result';
+import {TreeNode} from '../../../widgets/tree';
+import {Utid} from '../../../trace_processor/sql_utils/core_types';
 import {DurationWidget} from '../widgets/duration';
 
 // An individual node of the thread state breakdown tree.
diff --git a/ui/src/public/lib/stdlib_docs.ts b/ui/src/public/lib/stdlib_docs.ts
deleted file mode 100644
index fcdcf18..0000000
--- a/ui/src/public/lib/stdlib_docs.ts
+++ /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 {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/time_utils.ts b/ui/src/public/lib/time_utils.ts
new file mode 100644
index 0000000..30a3c6a
--- /dev/null
+++ b/ui/src/public/lib/time_utils.ts
@@ -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.
+
+import m from 'mithril';
+import {Duration, duration, time, Time} from '../../base/time';
+import {Trace} from '../trace';
+import {DurationPrecision, TimestampFormat} from '../timeline';
+
+export function renderTimecode(time: time) {
+  const {dhhmmss, millis, micros, nanos} = Time.toTimecode(time);
+  return m(
+    'span.pf-timecode',
+    m('span.pf-timecode-hms', dhhmmss),
+    '.',
+    m('span.pf-timecode-millis', millis),
+    m('span.pf-timecode-micros', micros),
+    m('span.pf-timecode-nanos', nanos),
+  );
+}
+
+export function formatDuration(trace: Trace, dur: duration): string {
+  const fmt = trace.timeline.timestampFormat;
+  switch (fmt) {
+    case TimestampFormat.UTC:
+    case TimestampFormat.TraceTz:
+    case TimestampFormat.Timecode:
+      return renderFormattedDuration(trace, dur);
+    case TimestampFormat.TraceNs:
+      return dur.toString();
+    case TimestampFormat.TraceNsLocale:
+      return dur.toLocaleString();
+    case TimestampFormat.Seconds:
+      return Duration.formatSeconds(dur);
+    case TimestampFormat.Milliseconds:
+      return Duration.formatMilliseconds(dur);
+    case TimestampFormat.Microseconds:
+      return Duration.formatMicroseconds(dur);
+    default:
+      const x: never = fmt;
+      throw new Error(`Invalid format ${x}`);
+  }
+}
+
+function renderFormattedDuration(trace: Trace, dur: duration): string {
+  const fmt = trace.timeline.durationPrecision;
+  switch (fmt) {
+    case DurationPrecision.HumanReadable:
+      return Duration.humanise(dur);
+    case DurationPrecision.Full:
+      return Duration.format(dur);
+    default:
+      const x: never = fmt;
+      throw new Error(`Invalid format ${x}`);
+  }
+}
diff --git a/ui/src/public/lib/tracks/query_counter_track.ts b/ui/src/public/lib/tracks/query_counter_track.ts
index dc25d69..e412f7c 100644
--- a/ui/src/public/lib/tracks/query_counter_track.ts
+++ b/ui/src/public/lib/tracks/query_counter_track.ts
@@ -109,11 +109,7 @@
     private readonly sqlTableName: string,
     options?: Partial<CounterOptions>,
   ) {
-    super({
-      trace,
-      uri,
-      options,
-    });
+    super(trace, uri, options);
   }
 
   getSqlSource(): string {
diff --git a/ui/src/public/lib/tracks/query_slice_track.ts b/ui/src/public/lib/tracks/query_slice_track.ts
index f7455a9..c8065fb 100644
--- a/ui/src/public/lib/tracks/query_slice_track.ts
+++ b/ui/src/public/lib/tracks/query_slice_track.ts
@@ -137,10 +137,7 @@
     uri: string,
     private readonly sqlTableName: string,
   ) {
-    super({
-      trace,
-      uri,
-    });
+    super(trace, uri);
   }
 
   override async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
diff --git a/ui/src/public/lib/tracks/slice_layout.ts b/ui/src/public/lib/tracks/slice_layout.ts
new file mode 100644
index 0000000..d328d1c
--- /dev/null
+++ b/ui/src/public/lib/tracks/slice_layout.ts
@@ -0,0 +1,81 @@
+// 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 interface SliceLayoutBase {
+  readonly padding: number; // vertical pixel padding between slices and track.
+  readonly rowSpacing: number; // Spacing between rows.
+
+  // A *guess* at the depth
+  readonly depthGuess?: number;
+
+  // True iff the track is flat (all slices have the same depth
+  // we have an optimisation for this).
+  readonly isFlat?: boolean;
+
+  readonly titleSizePx?: number;
+  readonly subtitleSizePx?: number;
+}
+
+export const SLICE_LAYOUT_BASE_DEFAULTS: SliceLayoutBase = Object.freeze({
+  padding: 3,
+  rowSpacing: 0,
+});
+
+export interface SliceLayoutFixed extends SliceLayoutBase {
+  readonly heightMode: 'FIXED';
+  readonly fixedHeight: number; // Outer height of the track.
+}
+
+export const SLICE_LAYOUT_FIXED_DEFAULTS: SliceLayoutFixed = Object.freeze({
+  ...SLICE_LAYOUT_BASE_DEFAULTS,
+  heightMode: 'FIXED',
+  fixedHeight: 30,
+});
+
+export interface SliceLayoutFitContent extends SliceLayoutBase {
+  readonly heightMode: 'FIT_CONTENT';
+  readonly sliceHeight: number; // Only when heightMode = 'FIT_CONTENT'.
+}
+
+export const SLICE_LAYOUT_FIT_CONTENT_DEFAULTS: SliceLayoutFitContent =
+  Object.freeze({
+    ...SLICE_LAYOUT_BASE_DEFAULTS,
+    heightMode: 'FIT_CONTENT',
+    sliceHeight: 18,
+  });
+
+export interface SliceLayoutFlat extends SliceLayoutBase {
+  readonly heightMode: 'FIXED';
+  readonly fixedHeight: number; // Outer height of the track.
+  readonly depthGuess: 0;
+  readonly isFlat: true;
+}
+
+export const SLICE_LAYOUT_FLAT_DEFAULTS: SliceLayoutFlat = Object.freeze({
+  ...SLICE_LAYOUT_BASE_DEFAULTS,
+  depthGuess: 0,
+  isFlat: true,
+  heightMode: 'FIXED',
+  fixedHeight: 18,
+  titleSizePx: 10,
+  padding: 3,
+});
+
+export type SliceLayout =
+  | SliceLayoutFixed
+  | SliceLayoutFitContent
+  | SliceLayoutFlat;
+
+export const DEFAULT_SLICE_LAYOUT: SliceLayout =
+  SLICE_LAYOUT_FIT_CONTENT_DEFAULTS;
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
index 621d9d3..c5c1935 100644
--- 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
@@ -24,8 +24,8 @@
   getThreadState,
   ThreadState,
 } from '../../../trace_processor/sql_utils/thread_state';
-import {DurationWidget} from '../../../frontend/widgets/duration';
-import {Timestamp} from '../../../frontend/widgets/timestamp';
+import {DurationWidget} from '../widgets/duration';
+import {Timestamp} from '../widgets/timestamp';
 import {
   ColumnType,
   durationFromSql,
diff --git a/ui/src/public/lib/widgets/duration.ts b/ui/src/public/lib/widgets/duration.ts
new file mode 100644
index 0000000..409af5d
--- /dev/null
+++ b/ui/src/public/lib/widgets/duration.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 {copyToClipboard} from '../../../base/clipboard';
+import {assertExists} from '../../../base/logging';
+import {Icons} from '../../../base/semantic_icons';
+import {duration} from '../../../base/time';
+import {AppImpl} from '../../../core/app_impl';
+import {Anchor} from '../../../widgets/anchor';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../../widgets/menu';
+import {Trace} from '../../trace';
+import {formatDuration} from '../time_utils';
+import {DurationPrecisionMenuItem} from './duration_precision_menu_items';
+import {TimestampFormatMenuItem} from './timestamp_format_menu';
+
+interface DurationWidgetAttrs {
+  dur: duration;
+  extraMenuItems?: m.Child[];
+}
+
+export class DurationWidget implements m.ClassComponent<DurationWidgetAttrs> {
+  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<DurationWidgetAttrs>) {
+    const {dur} = attrs;
+
+    if (dur === -1n) {
+      return '(Did not end)';
+    }
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, formatDuration(this.trace, dur)),
+      },
+      m(MenuItem, {
+        icon: Icons.Copy,
+        label: `Copy raw value`,
+        onclick: () => {
+          copyToClipboard(dur.toString());
+        },
+      }),
+      m(TimestampFormatMenuItem, {trace: this.trace}),
+      m(DurationPrecisionMenuItem, {trace: this.trace}),
+      attrs.extraMenuItems ? [m(MenuDivider), attrs.extraMenuItems] : null,
+    );
+  }
+}
diff --git a/ui/src/public/lib/widgets/duration_precision_menu_items.ts b/ui/src/public/lib/widgets/duration_precision_menu_items.ts
new file mode 100644
index 0000000..666acba
--- /dev/null
+++ b/ui/src/public/lib/widgets/duration_precision_menu_items.ts
@@ -0,0 +1,61 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+import {MenuItem} from '../../../widgets/menu';
+import {Trace} from '../../trace';
+import {DurationPrecision, TimestampFormat} from '../../timeline';
+
+interface DurationPrecisionMenuItemAttrs {
+  trace: Trace;
+}
+
+export class DurationPrecisionMenuItem
+  implements m.ClassComponent<DurationPrecisionMenuItemAttrs>
+{
+  view({attrs}: m.Vnode<DurationPrecisionMenuItemAttrs>) {
+    function renderMenuItem(value: DurationPrecision, label: string) {
+      return m(MenuItem, {
+        label,
+        active: value === attrs.trace.timeline.durationPrecision,
+        onclick: () => {
+          attrs.trace.timeline.durationPrecision = value;
+          attrs.trace.scheduleFullRedraw();
+        },
+      });
+    }
+
+    function durationPrecisionHasEffect() {
+      switch (attrs.trace.timeline.timestampFormat) {
+        case TimestampFormat.Timecode:
+        case TimestampFormat.UTC:
+        case TimestampFormat.TraceTz:
+          return true;
+        default:
+          return false;
+      }
+    }
+
+    return m(
+      MenuItem,
+      {
+        label: 'Duration precision',
+        disabled: !durationPrecisionHasEffect(),
+        title: 'Not configurable with current time format',
+      },
+      renderMenuItem(DurationPrecision.Full, 'Full'),
+      renderMenuItem(DurationPrecision.HumanReadable, 'Human readable'),
+    );
+  }
+}
diff --git a/ui/src/public/lib/widgets/timestamp.ts b/ui/src/public/lib/widgets/timestamp.ts
new file mode 100644
index 0000000..ee416f8
--- /dev/null
+++ b/ui/src/public/lib/widgets/timestamp.ts
@@ -0,0 +1,100 @@
+// 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 {assertExists} from '../../../base/logging';
+import {Icons} from '../../../base/semantic_icons';
+import {time, Time} from '../../../base/time';
+import {AppImpl} from '../../../core/app_impl';
+import {Anchor} from '../../../widgets/anchor';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../../widgets/menu';
+import {Trace} from '../../trace';
+import {TimestampFormatMenuItem} from './timestamp_format_menu';
+import {renderTimecode} from '../time_utils';
+import {TimestampFormat} from '../../timeline';
+
+// import {MenuItem, PopupMenu2} from './menu';
+
+interface TimestampAttrs {
+  // The timestamp to print, this should be the absolute, raw timestamp as
+  // found in trace processor.
+  ts: time;
+  // Custom text value to show instead of the default HH:MM:SS.mmm uuu nnn
+  // formatting.
+  display?: m.Children;
+  extraMenuItems?: m.Child[];
+}
+
+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: () => (timeline.hoverCursorTimestamp = ts),
+            onmouseout: () => (timeline.hoverCursorTimestamp = undefined),
+          },
+          attrs.display ?? this.formatTimestamp(timeline.toDomainTime(ts)),
+        ),
+      },
+      m(MenuItem, {
+        icon: Icons.Copy,
+        label: `Copy raw value`,
+        onclick: () => {
+          copyToClipboard(ts.toString());
+        },
+      }),
+      m(TimestampFormatMenuItem),
+      attrs.extraMenuItems ? [m(MenuDivider), attrs.extraMenuItems] : null,
+    );
+  }
+
+  private formatTimestamp(time: time): m.Children {
+    const fmt = this.trace.timeline.timestampFormat;
+    switch (fmt) {
+      case TimestampFormat.UTC:
+      case TimestampFormat.TraceTz:
+      case TimestampFormat.Timecode:
+        return renderTimecode(time);
+      case TimestampFormat.TraceNs:
+        return time.toString();
+      case TimestampFormat.TraceNsLocale:
+        return time.toLocaleString();
+      case TimestampFormat.Seconds:
+        return Time.formatSeconds(time);
+      case TimestampFormat.Milliseconds:
+        return Time.formatMilliseconds(time);
+      case TimestampFormat.Microseconds:
+        return Time.formatMicroseconds(time);
+      default:
+        const x: never = fmt;
+        throw new Error(`Invalid timestamp ${x}`);
+    }
+  }
+}
diff --git a/ui/src/public/lib/widgets/timestamp_format_menu.ts b/ui/src/public/lib/widgets/timestamp_format_menu.ts
new file mode 100644
index 0000000..2b1a351
--- /dev/null
+++ b/ui/src/public/lib/widgets/timestamp_format_menu.ts
@@ -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.
+
+import m from 'mithril';
+import {MenuItem} from '../../../widgets/menu';
+import {Trace} from '../../trace';
+import {TimestampFormat} from '../../timeline';
+
+interface TimestampFormatMenuItemAttrs {
+  trace: Trace;
+}
+
+export class TimestampFormatMenuItem
+  implements m.ClassComponent<TimestampFormatMenuItemAttrs>
+{
+  view({attrs}: m.Vnode<TimestampFormatMenuItemAttrs>) {
+    function renderMenuItem(value: TimestampFormat, label: string) {
+      return m(MenuItem, {
+        label,
+        active: value === attrs.trace.timeline.timestampFormat,
+        onclick: () => {
+          attrs.trace.timeline.timestampFormat = value;
+          attrs.trace.scheduleFullRedraw();
+        },
+      });
+    }
+
+    return m(
+      MenuItem,
+      {
+        label: 'Time format',
+      },
+      renderMenuItem(TimestampFormat.Timecode, 'Timecode'),
+      renderMenuItem(TimestampFormat.UTC, 'Realtime (UTC)'),
+      renderMenuItem(TimestampFormat.TraceTz, 'Realtime (Trace TZ)'),
+      renderMenuItem(TimestampFormat.Seconds, 'Seconds'),
+      renderMenuItem(TimestampFormat.Milliseconds, 'Milliseconds'),
+      renderMenuItem(TimestampFormat.Microseconds, 'Microseconds'),
+      renderMenuItem(TimestampFormat.TraceNs, 'Raw'),
+      renderMenuItem(
+        TimestampFormat.TraceNsLocale,
+        'Raw (with locale-specific formatting)',
+      ),
+    );
+  }
+}
diff --git a/ui/src/public/timeline.ts b/ui/src/public/timeline.ts
index ca881e5..8ebad1e 100644
--- a/ui/src/public/timeline.ts
+++ b/ui/src/public/timeline.ts
@@ -15,6 +15,22 @@
 import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
 import {time} from '../base/time';
 
+export enum TimestampFormat {
+  Timecode = 'timecode',
+  TraceNs = 'traceNs',
+  TraceNsLocale = 'traceNsLocale',
+  Seconds = 'seconds',
+  Milliseconds = 'milliseconds',
+  Microseconds = 'microseconds',
+  UTC = 'utc',
+  TraceTz = 'traceTz',
+}
+
+export enum DurationPrecision {
+  Full = 'full',
+  HumanReadable = 'human_readable',
+}
+
 export interface Timeline {
   // Bring a timestamp into view.
   panToTimestamp(ts: time): void;
@@ -39,4 +55,8 @@
 
   // Get a time in the current domain as specified by timestampOffset.
   toDomainTime(ts: time): time;
+
+  // These control how timestamps and durations are formatted throughout the UI
+  timestampFormat: TimestampFormat;
+  durationPrecision: DurationPrecision;
 }
diff --git a/ui/src/public/trace.ts b/ui/src/public/trace.ts
index 6e60c79..95db546 100644
--- a/ui/src/public/trace.ts
+++ b/ui/src/public/trace.ts
@@ -96,3 +96,5 @@
 export interface TraceAttrs {
   trace: Trace;
 }
+
+export const TRACE_SUFFIX = '.perfetto-trace';
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 94ac9e7..6d1b1dc 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -20,6 +20,7 @@
 import {ColorScheme} from './color_scheme';
 import {TrackEventDetailsPanel} from './details_panel';
 import {TrackEventDetails, TrackEventSelection} from './selection';
+import {Dataset} from '../trace_processor/dataset';
 
 export interface TrackManager {
   /**
@@ -175,6 +176,12 @@
   onMouseOut?(): void;
 
   /**
+   * Optional: Returns a dataset that represents the events displayed on this
+   * track.
+   */
+  getDataset?(): Dataset | undefined;
+
+  /**
    * Optional: Get details of a track event given by eventId on this track.
    */
   getSelectionDetails?(eventId: number): Promise<TrackEventDetails | undefined>;
diff --git a/ui/src/trace_processor/dataset.ts b/ui/src/trace_processor/dataset.ts
new file mode 100644
index 0000000..25c64cb
--- /dev/null
+++ b/ui/src/trace_processor/dataset.ts
@@ -0,0 +1,290 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES 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 {getOrCreate} from '../base/utils';
+import {ColumnType, SqlValue} from './query_result';
+
+/**
+ * A dataset defines a set of rows in TraceProcessor and a schema of the
+ * resultant columns. Dataset implementations describe how to get the data in
+ * different ways - e.g. 'source' datasets define a dataset as a table name (or
+ * select statement) + filters, whereas a 'union' dataset defines a dataset as
+ * the union of other datasets.
+ *
+ * The idea is that users can build arbitrarily complex trees of datasets, then
+ * at any point call `optimize()` to create the smallest possible tree that
+ * represents the same dataset, and `query()` which produces a select statement
+ * for the resultant dataset.
+ *
+ * Users can also use the `schema` property and `implements()` to get and test
+ * the schema of a given dataset.
+ */
+export interface Dataset {
+  /**
+   * Get or calculate the resultant schema of this dataset.
+   */
+  readonly schema: DatasetSchema;
+
+  /**
+   * Produce a query for this dataset.
+   *
+   * @param schema - The schema to use for extracting columns - if undefined,
+   * the most specific possible schema is evaluated from the dataset first and
+   * used instead.
+   */
+  query(schema?: DatasetSchema): string;
+
+  /**
+   * Optimizes a dataset into the smallest possible expression.
+   *
+   * For example by combining elements of union data sets that have the same src
+   * and similar filters into a single set.
+   *
+   * For example, the following 'union' dataset...
+   *
+   * ```
+   * {
+   *   union: [
+   *     {
+   *       src: 'foo',
+   *       schema: {
+   *         'a': NUM,
+   *         'b': NUM,
+   *       },
+   *       filter: {col: 'a', eq: 1},
+   *     },
+   *     {
+   *       src: 'foo',
+   *       schema: {
+   *         'a': NUM,
+   *         'b': NUM,
+   *       },
+   *       filter: {col: 'a', eq: 2},
+   *     },
+   *   ]
+   * }
+   * ```
+   *
+   * ...will be combined into a single 'source' dataset...
+   *
+   * ```
+   * {
+   *   src: 'foo',
+   *   schema: {
+   *     'a': NUM,
+   *     'b': NUM,
+   *   },
+   *   filter: {col: 'a', in: [1, 2]},
+   * },
+   * ```
+   */
+  optimize(): Dataset;
+
+  /**
+   * Returns true if this dataset implements a given schema.
+   *
+   * @param schema - The schema to test against.
+   */
+  implements(schema: DatasetSchema): boolean;
+}
+
+/**
+ * Defines a list of columns and types that define the shape of the data
+ * represented by a dataset.
+ */
+export type DatasetSchema = Record<string, ColumnType>;
+
+/**
+ * A filter used to express that a column must equal a value.
+ */
+interface EqFilter {
+  readonly col: string;
+  readonly eq: SqlValue;
+}
+
+/**
+ * A filter used to express that column must be one of a set of values.
+ */
+interface InFilter {
+  readonly col: string;
+  readonly in: ReadonlyArray<SqlValue>;
+}
+
+/**
+ * Union of all filter types.
+ */
+type Filter = EqFilter | InFilter;
+
+/**
+ * Named arguments for a SourceDataset.
+ */
+interface SourceDatasetConfig {
+  readonly src: string;
+  readonly schema: DatasetSchema;
+  readonly filter?: Filter;
+}
+
+/**
+ * Defines a dataset with a source SQL select statement of table name, a
+ * schema describing the columns, and an optional filter.
+ */
+export class SourceDataset implements Dataset {
+  readonly src: string;
+  readonly schema: DatasetSchema;
+  readonly filter?: Filter;
+
+  constructor(config: SourceDatasetConfig) {
+    this.src = config.src;
+    this.schema = config.schema;
+    this.filter = config.filter;
+  }
+
+  query(schema?: DatasetSchema) {
+    schema = schema ?? this.schema;
+    const cols = Object.keys(schema);
+    const whereClause = this.filterToQuery();
+    return `select ${cols.join(', ')} from (${this.src}) ${whereClause}`.trim();
+  }
+
+  optimize() {
+    // Cannot optimize SourceDataset
+    return this;
+  }
+
+  implements(schema: DatasetSchema) {
+    return Object.entries(schema).every(([name, kind]) => {
+      return name in this.schema && this.schema[name] === kind;
+    });
+  }
+
+  private filterToQuery() {
+    const filter = this.filter;
+    if (filter === undefined) {
+      return '';
+    }
+    if ('eq' in filter) {
+      return `where ${filter.col} = ${filter.eq}`;
+    } else if ('in' in filter) {
+      return `where ${filter.col} in (${filter.in.join(',')})`;
+    } else {
+      assertUnreachable(filter);
+    }
+  }
+}
+
+/**
+ * A dataset that represents the union of multiple datasets.
+ */
+export class UnionDataset implements Dataset {
+  constructor(readonly union: ReadonlyArray<Dataset>) {}
+
+  get schema(): DatasetSchema {
+    // Find the minimal set of columns that are supported by all datasets of
+    // the union
+    let sch: Record<string, ColumnType> | undefined = undefined;
+    this.union.forEach((ds) => {
+      const dsSchema = ds.schema;
+      if (sch === undefined) {
+        // First time just use this one
+        sch = dsSchema;
+      } else {
+        const newSch: Record<string, ColumnType> = {};
+        for (const [key, kind] of Object.entries(sch)) {
+          if (key in dsSchema && dsSchema[key] === kind) {
+            newSch[key] = kind;
+          }
+        }
+        sch = newSch;
+      }
+    });
+    return sch ?? {};
+  }
+
+  query(schema?: DatasetSchema): string {
+    schema = schema ?? this.schema;
+    return this.union
+      .map((dataset) => dataset.query(schema))
+      .join(' union all ');
+  }
+
+  optimize(): Dataset {
+    // Recursively optimize each dataset of this union
+    const optimizedUnion = this.union.map((ds) => ds.optimize());
+
+    // Find all source datasets and combine then based on src
+    const combinedSrcSets = new Map<string, SourceDataset[]>();
+    const otherDatasets: Dataset[] = [];
+    for (const e of optimizedUnion) {
+      if (e instanceof SourceDataset) {
+        const set = getOrCreate(combinedSrcSets, e.src, () => []);
+        set.push(e);
+      } else {
+        otherDatasets.push(e);
+      }
+    }
+
+    const mergedSrcSets = Array.from(combinedSrcSets.values()).map(
+      (srcGroup) => {
+        if (srcGroup.length === 1) return srcGroup[0];
+
+        // Combine schema across all members in the union
+        const combinedSchema = srcGroup.reduce((acc, e) => {
+          Object.assign(acc, e.schema);
+          return acc;
+        }, {} as DatasetSchema);
+
+        // Merge filters for the same src
+        const inFilters: InFilter[] = [];
+        for (const {filter} of srcGroup) {
+          if (filter) {
+            if ('eq' in filter) {
+              inFilters.push({col: filter.col, in: [filter.eq]});
+            } else {
+              inFilters.push(filter);
+            }
+          }
+        }
+
+        const mergedFilter = mergeFilters(inFilters);
+        return new SourceDataset({
+          src: srcGroup[0].src,
+          schema: combinedSchema,
+          filter: mergedFilter,
+        });
+      },
+    );
+
+    const finalUnion = [...mergedSrcSets, ...otherDatasets];
+
+    if (finalUnion.length === 1) {
+      return finalUnion[0];
+    } else {
+      return new UnionDataset(finalUnion);
+    }
+  }
+
+  implements(schema: DatasetSchema) {
+    return Object.entries(schema).every(([name, kind]) => {
+      return name in this.schema && this.schema[name] === kind;
+    });
+  }
+}
+
+function mergeFilters(filters: InFilter[]): InFilter | undefined {
+  if (filters.length === 0) return undefined;
+  const col = filters[0].col;
+  const values = new Set(filters.flatMap((filter) => filter.in));
+  return {col, in: Array.from(values)};
+}
diff --git a/ui/src/trace_processor/dataset_unittest.ts b/ui/src/trace_processor/dataset_unittest.ts
new file mode 100644
index 0000000..2bd4e53
--- /dev/null
+++ b/ui/src/trace_processor/dataset_unittest.ts
@@ -0,0 +1,228 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {SourceDataset, UnionDataset} from './dataset';
+import {LONG, NUM, STR} from './query_result';
+
+test('get query for simple dataset', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+  });
+
+  expect(dataset.query()).toEqual('select id from (slice)');
+});
+
+test("get query for simple dataset with 'eq' filter", () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+    filter: {
+      col: 'id',
+      eq: 123,
+    },
+  });
+
+  expect(dataset.query()).toEqual('select id from (slice) where id = 123');
+});
+
+test("get query for simple dataset with an 'in' filter", () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM},
+    filter: {
+      col: 'id',
+      in: [123, 456],
+    },
+  });
+
+  expect(dataset.query()).toEqual(
+    'select id from (slice) where id in (123,456)',
+  );
+});
+
+test('get query for union dataset', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {id: NUM},
+      filter: {
+        col: 'id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {id: NUM},
+      filter: {
+        col: 'id',
+        eq: 456,
+      },
+    }),
+  ]);
+
+  expect(dataset.query()).toEqual(
+    'select id from (slice) where id = 123 union all select id from (slice) where id = 456',
+  );
+});
+
+test('doesImplement', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM, ts: LONG},
+  });
+
+  expect(dataset.implements({id: NUM})).toBe(true);
+  expect(dataset.implements({id: NUM, ts: LONG})).toBe(true);
+  expect(dataset.implements({id: NUM, ts: LONG, name: STR})).toBe(false);
+  expect(dataset.implements({id: LONG})).toBe(false);
+});
+
+test('find the schema of a simple dataset', () => {
+  const dataset = new SourceDataset({
+    src: 'slice',
+    schema: {id: NUM, ts: LONG},
+  });
+
+  expect(dataset.schema).toMatchObject({id: NUM, ts: LONG});
+});
+
+test('find the schema of a union where source sets differ in their names', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {bar: NUM},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({});
+});
+
+test('find the schema of a union with differing source sets', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: LONG},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({});
+});
+
+test('find the schema of a union with one column in common', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM, bar: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM, baz: NUM},
+    }),
+  ]);
+
+  expect(dataset.schema).toMatchObject({foo: NUM});
+});
+
+test('optimize a union dataset', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 456,
+      },
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    schema: {},
+    filter: {
+      col: 'track_id',
+      in: [123, 456],
+    },
+  });
+});
+
+test('optimize a union dataset with different types of filters', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        eq: 123,
+      },
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {},
+      filter: {
+        col: 'track_id',
+        in: [456, 789],
+      },
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    schema: {},
+    filter: {
+      col: 'track_id',
+      in: [123, 456, 789],
+    },
+  });
+});
+
+test('optimize a union dataset with different schemas', () => {
+  const dataset = new UnionDataset([
+    new SourceDataset({
+      src: 'slice',
+      schema: {foo: NUM},
+    }),
+    new SourceDataset({
+      src: 'slice',
+      schema: {bar: NUM},
+    }),
+  ]);
+
+  expect(dataset.optimize()).toEqual({
+    src: 'slice',
+    // The resultant schema is the combination of the union's member's schemas,
+    // as we know the source is the same as we know we can get all of the 'seen'
+    // columns from the source.
+    schema: {
+      foo: NUM,
+      bar: NUM,
+    },
+  });
+});
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index ccb8a03..58a3705 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -208,7 +208,7 @@
     let isFinalResponse = true;
 
     switch (rpc.response) {
-      case TPM.TPM_APPEND_TRACE_DATA:
+      case TPM.TPM_APPEND_TRACE_DATA: {
         const appendResult = assertExists(rpc.appendResult);
         const pendingPromise = assertExists(this.pendingParses.shift());
         if (exists(appendResult.error) && appendResult.error.length > 0) {
@@ -217,9 +217,17 @@
           pendingPromise.resolve();
         }
         break;
-      case TPM.TPM_FINALIZE_TRACE_DATA:
-        assertExists(this.pendingEOFs.shift()).resolve();
+      }
+      case TPM.TPM_FINALIZE_TRACE_DATA: {
+        const finalizeResult = assertExists(rpc.finalizeDataResult);
+        const pendingPromise = assertExists(this.pendingEOFs.shift());
+        if (exists(finalizeResult.error) && finalizeResult.error.length > 0) {
+          pendingPromise.reject(finalizeResult.error);
+        } else {
+          pendingPromise.resolve();
+        }
         break;
+      }
       case TPM.TPM_RESET_TRACE_PROCESSOR:
         assertExists(this.pendingResetTraceProcessors.shift()).resolve();
         break;
diff --git a/ui/src/widgets/editor.ts b/ui/src/widgets/editor.ts
index 58ea153..1187be0 100644
--- a/ui/src/widgets/editor.ts
+++ b/ui/src/widgets/editor.ts
@@ -21,6 +21,7 @@
 import {assertExists} from '../base/logging';
 import {DragGestureHandler} from '../base/drag_gesture_handler';
 import {DisposableStack} from '../base/disposable_stack';
+import {scheduleFullRedraw} from './raf';
 
 export interface EditorAttrs {
   // Initial state for the editor.
@@ -64,6 +65,7 @@
             text = selectedText;
           }
           onExecute(text);
+          scheduleFullRedraw('force');
           return true;
         },
       });
@@ -75,6 +77,7 @@
         view.update([tr]);
         const text = view.state.doc.toString();
         onUpdate(text);
+        scheduleFullRedraw('force');
       };
     }
 
diff --git a/ui/src/widgets/flamegraph.ts b/ui/src/widgets/flamegraph.ts
index a7252f5..3c831dd 100644
--- a/ui/src/widgets/flamegraph.ts
+++ b/ui/src/widgets/flamegraph.ts
@@ -966,7 +966,7 @@
   }
   return addFilter(state, {
     kind: 'SHOW_STACK',
-    filter: filter.split(': ', 2)[1],
+    filter: filter,
   });
 }
 
diff --git a/ui/src/widgets/hotkey_context.ts b/ui/src/widgets/hotkey_context.ts
index f4d702a..767683e 100644
--- a/ui/src/widgets/hotkey_context.ts
+++ b/ui/src/widgets/hotkey_context.ts
@@ -14,6 +14,7 @@
 
 import m from 'mithril';
 import {checkHotkey, Hotkey} from '../base/hotkeys';
+import {scheduleFullRedraw} from './raf';
 
 export interface HotkeyConfig {
   hotkey: Hotkey;
@@ -58,6 +59,7 @@
         if (checkHotkey(hotkey, e)) {
           e.preventDefault();
           callback();
+          scheduleFullRedraw('force');
         }
       });
     }
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index e32f329..c07e6fe 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -14,8 +14,8 @@
 
 import m from 'mithril';
 import {defer} from '../base/deferred';
-import {scheduleFullRedraw} from './raf';
 import {Icon} from './icon';
+import {scheduleFullRedraw} from './raf';
 
 // This module deals with modal dialogs. Unlike most components, here we want to
 // render the DOM elements outside of the corresponding vdom tree. For instance
@@ -79,7 +79,10 @@
 export class Modal implements m.ClassComponent<ModalAttrs> {
   onbeforeremove(vnode: m.VnodeDOM<ModalAttrs>) {
     const removePromise = defer<void>();
-    vnode.dom.addEventListener('animationend', () => removePromise.resolve());
+    vnode.dom.addEventListener('animationend', () => {
+      scheduleFullRedraw('force');
+      removePromise.resolve();
+    });
     vnode.dom.classList.add('modal-fadeout');
 
     // Retuning `removePromise` will cause Mithril to defer the actual component
@@ -94,7 +97,6 @@
       // in turn will: (1) call the user's original attrs.onClose; (2) resolve
       // the promise returned by showModal().
       vnode.attrs.onClose();
-      scheduleFullRedraw();
     }
   }
 
@@ -223,7 +225,7 @@
     },
   };
   currentModal = attrs;
-  scheduleFullRedraw();
+  redrawModal();
   return returnedClosePromise;
 }
 
@@ -232,7 +234,7 @@
 // evident why a redraw is requested.
 export function redrawModal() {
   if (currentModal !== undefined) {
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 }
 
@@ -251,7 +253,7 @@
     return;
   }
   currentModal = undefined;
-  scheduleFullRedraw();
+  scheduleFullRedraw('force');
 }
 
 export function getCurrentModalKey(): string | undefined {
diff --git a/ui/src/widgets/popup.ts b/ui/src/widgets/popup.ts
index ed16695..ac8b563 100644
--- a/ui/src/widgets/popup.ts
+++ b/ui/src/widgets/popup.ts
@@ -352,13 +352,13 @@
     if (this.isOpen) {
       this.isOpen = false;
       this.onChange(this.isOpen);
-      scheduleFullRedraw();
+      scheduleFullRedraw('force');
     }
   }
 
   private togglePopup() {
     this.isOpen = !this.isOpen;
     this.onChange(this.isOpen);
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 }
diff --git a/ui/src/widgets/popup_menu.ts b/ui/src/widgets/popup_menu.ts
deleted file mode 100644
index 737815c..0000000
--- a/ui/src/widgets/popup_menu.ts
+++ /dev/null
@@ -1,198 +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 {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/portal.ts b/ui/src/widgets/portal.ts
index 734fee6..91c3608 100644
--- a/ui/src/widgets/portal.ts
+++ b/ui/src/widgets/portal.ts
@@ -46,13 +46,22 @@
 export class Portal implements m.ClassComponent<PortalAttrs> {
   private portalElement?: HTMLElement;
   private containerElement?: Element;
+  private contentComponent: m.Component;
+
+  constructor({children}: m.CVnode<PortalAttrs>) {
+    // Create a temporary component that we can mount in oncreate, and unmount
+    // in onremove, but inject the new portal content (children) into it each
+    // render cycle. This is initialized here rather than in oncreate to avoid
+    // having to make it optional or use assertExists().
+    this.contentComponent = {view: () => children};
+  }
 
   view() {
     // Dummy element renders nothing but permits DOM access in lifecycle hooks.
     return m('span', {style: {display: 'none'}});
   }
 
-  oncreate({attrs, children, dom}: m.VnodeDOM<PortalAttrs, this>) {
+  oncreate({attrs, dom}: m.CVnodeDOM<PortalAttrs>) {
     const {
       onContentMount = () => {},
       onBeforeContentMount = (): MountOptions => ({}),
@@ -65,16 +74,21 @@
     container.appendChild(this.portalElement);
     this.applyPortalProps(attrs);
 
-    m.render(this.portalElement, children);
+    m.mount(this.portalElement, this.contentComponent);
 
     onContentMount(this.portalElement);
   }
 
-  onupdate({attrs, children}: m.VnodeDOM<PortalAttrs, this>) {
+  onbeforeupdate({children}: m.CVnode<PortalAttrs>) {
+    // Update the mounted content's view function to return the latest portal
+    // content passed in via children, without changing the component itself.
+    this.contentComponent.view = () => children;
+  }
+
+  onupdate({attrs}: m.CVnodeDOM<PortalAttrs>) {
     const {onContentUpdate = () => {}} = attrs;
     if (this.portalElement) {
       this.applyPortalProps(attrs);
-      m.render(this.portalElement, children);
       onContentUpdate(this.portalElement);
     }
   }
@@ -86,14 +100,14 @@
     }
   }
 
-  onremove({attrs}: m.VnodeDOM<PortalAttrs, this>) {
+  onremove({attrs}: m.CVnodeDOM<PortalAttrs>) {
     const {onContentUnmount = () => {}} = attrs;
     const container = this.containerElement ?? document.body;
     if (this.portalElement) {
       if (container.contains(this.portalElement)) {
         onContentUnmount(this.portalElement);
         // Rendering null ensures previous vnodes are removed properly.
-        m.render(this.portalElement, null);
+        m.mount(this.portalElement, null);
         container.removeChild(this.portalElement);
       }
     }
diff --git a/ui/src/widgets/raf.ts b/ui/src/widgets/raf.ts
index 20afb61..dc0d3ab 100644
--- a/ui/src/widgets/raf.ts
+++ b/ui/src/widgets/raf.ts
@@ -12,12 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-let FULL_REDRAW_FUNCTION = () => {};
+let FULL_REDRAW_FUNCTION = (_force?: 'force') => {};
 
 export function setScheduleFullRedraw(func: () => void) {
   FULL_REDRAW_FUNCTION = func;
 }
 
-export function scheduleFullRedraw() {
-  FULL_REDRAW_FUNCTION();
+export function scheduleFullRedraw(force?: 'force') {
+  FULL_REDRAW_FUNCTION(force);
 }
diff --git a/ui/src/widgets/table.ts b/ui/src/widgets/table.ts
index ee195d0..1389907 100644
--- a/ui/src/widgets/table.ts
+++ b/ui/src/widgets/table.ts
@@ -22,17 +22,28 @@
   SortDirection,
   withDirection,
 } from '../base/comparison_utils';
-import {
-  menuItem,
-  PopupMenuButton,
-  popupMenuIcon,
-  PopupMenuItem,
-} from './popup_menu';
 import {scheduleFullRedraw} from './raf';
+import {MenuItem, PopupMenu2} from './menu';
+import {Button} from './button';
+
+// 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';
+  }
+}
 
 export interface ColumnDescriptorAttrs<T> {
   // Context menu items displayed on the column header.
-  contextMenu?: PopupMenuItem[];
+  contextMenu?: m.Child[];
 
   // Unique column ID, used to identify which column is currently sorted.
   columnId?: string;
@@ -49,7 +60,7 @@
   name: string;
   render: (row: T) => m.Child;
   id: string;
-  contextMenu?: PopupMenuItem[];
+  contextMenu?: m.Child[];
   ordering?: ComparisonFn<T>;
 
   constructor(
@@ -81,7 +92,7 @@
 export function numberColumn<T>(
   name: string,
   getter: (t: T) => number,
-  contextMenu?: PopupMenuItem[],
+  contextMenu?: m.Child[],
 ): ColumnDescriptor<T> {
   return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
 }
@@ -89,7 +100,7 @@
 export function stringColumn<T>(
   name: string,
   getter: (t: T) => string,
-  contextMenu?: PopupMenuItem[],
+  contextMenu?: m.Child[],
 ): ColumnDescriptor<T> {
   return new ColumnDescriptor<T>(name, getter, {contextMenu, sortKey: getter});
 }
@@ -191,33 +202,42 @@
     if (column.ordering !== undefined) {
       const ordering = column.ordering;
       currDirection = directionOnIndex(column.id, vnode.attrs.data.sortingInfo);
-      const newItems: PopupMenuItem[] = [];
+      const newItems: m.Child[] = [];
       if (currDirection !== 'ASC') {
         newItems.push(
-          menuItem('Sort ascending', () => {
-            vnode.attrs.data.reorder({
-              columnId: column.id,
-              direction: 'ASC',
-              ordering,
-            });
+          m(MenuItem, {
+            label: 'Sort ascending',
+            onclick: () => {
+              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,
-            });
+          m(MenuItem, {
+            label: 'Sort descending',
+            onclick: () => {
+              vnode.attrs.data.reorder({
+                columnId: column.id,
+                direction: 'DESC',
+                ordering,
+              });
+            },
           }),
         );
       }
       if (currDirection !== undefined) {
         newItems.push(
-          menuItem('Restore original order', () => {
-            vnode.attrs.data.resetOrder();
+          m(MenuItem, {
+            label: 'Restore original order',
+            onclick: () => {
+              vnode.attrs.data.resetOrder();
+            },
           }),
         );
       }
@@ -227,12 +247,14 @@
     return m(
       'td',
       column.name,
-      items === undefined
-        ? null
-        : m(PopupMenuButton, {
-            icon: popupMenuIcon(currDirection),
-            items,
-          }),
+      items &&
+        m(
+          PopupMenu2,
+          {
+            trigger: m(Button, {icon: popupMenuIcon(currDirection)}),
+          },
+          items,
+        ),
     );
   }
 
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 7cbf533..1a8cb43 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -228,7 +228,7 @@
     }
     this._status = Status.Done;
     this.pending = undefined;
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 
   private handleError(pending: Promise<vega.View>, err: unknown) {
@@ -242,7 +242,7 @@
   private setError(err: unknown) {
     this._status = Status.Error;
     this._error = getErrorMessage(err);
-    scheduleFullRedraw();
+    scheduleFullRedraw('force');
   }
 
   [Symbol.dispose]() {
diff --git a/ui/src/widgets/virtual_scroll_helper.ts b/ui/src/widgets/virtual_scroll_helper.ts
index 475c360..6172c94 100644
--- a/ui/src/widgets/virtual_scroll_helper.ts
+++ b/ui/src/widgets/virtual_scroll_helper.ts
@@ -14,6 +14,7 @@
 
 import {DisposableStack} from '../base/disposable_stack';
 import {Bounds2D, Rect2D} from '../base/geom';
+import {scheduleFullRedraw} from './raf';
 
 export interface VirtualScrollHelperOpts {
   overdrawPx: number;
@@ -46,6 +47,7 @@
       this._data.forEach((data) =>
         recalculatePuckRect(sliderElement, containerElement, data),
       );
+      scheduleFullRedraw('force');
     };
 
     containerElement.addEventListener('scroll', recalculateRects, {