Merge "[jumbo] Const-correct SharedMemory::start() and AlignedBufferTest::buf()" into main
diff --git a/Android.bp b/Android.bp
index f3b1593..c7412d3 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2412,6 +2412,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -2512,6 +2514,7 @@
     shared_libs: [
         "heapprofd_client_api",
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -6648,6 +6651,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7075,6 +7079,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7164,6 +7169,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.gen.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.cc",
@@ -7253,6 +7259,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.gen.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.gen.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.gen.h",
@@ -7338,6 +7345,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7426,6 +7434,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pb.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.cc",
@@ -7514,6 +7523,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pb.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pb.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pb.h",
@@ -7599,6 +7609,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -7688,6 +7699,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.cc",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.cc",
@@ -7777,6 +7789,7 @@
         "external/perfetto/protos/perfetto/trace/ftrace/oom.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/panel.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/perf_trace_counters.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/ftrace/pixel_mm.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/power.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/printk.pbzero.h",
         "external/perfetto/protos/perfetto/trace/ftrace/raw_syscalls.pbzero.h",
@@ -12234,6 +12247,7 @@
         "src/trace_processor/importers/common/flow_tracker.cc",
         "src/trace_processor/importers/common/global_args_tracker.cc",
         "src/trace_processor/importers/common/jit_cache.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.cc",
@@ -12322,6 +12336,7 @@
         "src/trace_processor/importers/ftrace/gpu_work_period_tracker.cc",
         "src/trace_processor/importers/ftrace/iostat_tracker.cc",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc",
         "src/trace_processor/importers/ftrace/rss_stat_tracker.cc",
         "src/trace_processor/importers/ftrace/thermal_tracker.cc",
@@ -12396,6 +12411,21 @@
     ],
 }
 
+// GN: //src/trace_processor/importers/instruments:instruments
+filegroup {
+    name: "perfetto_src_trace_processor_importers_instruments_instruments",
+    srcs: [
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_parser.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/instruments:row
+filegroup {
+    name: "perfetto_src_trace_processor_importers_instruments_row",
+}
+
 // GN: //src/trace_processor/importers/json:full
 filegroup {
     name: "perfetto_src_trace_processor_importers_json_full",
@@ -13319,6 +13349,7 @@
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_stats.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/general.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/process.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/utilization/system.sql",
@@ -14821,6 +14852,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -15297,6 +15329,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_unittests",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -15436,6 +15470,7 @@
     ],
     shared_libs: [
         "libbase",
+        "libexpat",
         "libicu",
         "liblog",
         "libprocinfo",
@@ -16138,6 +16173,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -16270,8 +16306,11 @@
         ":perfetto_include_perfetto_ext_traced_sys_stats_counters",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16345,6 +16384,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -16478,6 +16519,7 @@
     target: {
         android: {
             shared_libs: [
+                "libexpat",
                 "libicu",
                 "liblog",
                 "libprotobuf-cpp-full",
@@ -16491,6 +16533,7 @@
         },
         host: {
             static_libs: [
+                "libexpat",
                 "libprotobuf-cpp-full",
                 "libsqlite_static_noicu",
                 "libz",
@@ -16575,6 +16618,7 @@
         ":perfetto_src_trace_processor_importers_etw_minimal",
         ":perfetto_src_trace_processor_importers_ftrace_minimal",
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
         ":perfetto_src_trace_processor_importers_perf_record",
@@ -16675,8 +16719,11 @@
         ":perfetto_include_perfetto_profiling_pprof_builder",
         ":perfetto_include_perfetto_protozero_protozero",
         ":perfetto_include_perfetto_public_abi_base",
+        ":perfetto_include_perfetto_public_abi_public",
         ":perfetto_include_perfetto_public_base",
+        ":perfetto_include_perfetto_public_protos_protos",
         ":perfetto_include_perfetto_public_protozero",
+        ":perfetto_include_perfetto_public_public",
         ":perfetto_include_perfetto_trace_processor_basic_types",
         ":perfetto_include_perfetto_trace_processor_storage",
         ":perfetto_include_perfetto_trace_processor_trace_processor",
@@ -16748,6 +16795,8 @@
         ":perfetto_src_trace_processor_importers_fuchsia_minimal",
         ":perfetto_src_trace_processor_importers_gzip_full",
         ":perfetto_src_trace_processor_importers_i2c_full",
+        ":perfetto_src_trace_processor_importers_instruments_instruments",
+        ":perfetto_src_trace_processor_importers_instruments_row",
         ":perfetto_src_trace_processor_importers_json_full",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
@@ -16807,6 +16856,7 @@
         ":perfetto_src_traceconv_utils",
     ],
     static_libs: [
+        "libexpat",
         "libsqlite_static_noicu",
         "libz",
         "perfetto_src_trace_processor_demangle",
diff --git a/BUILD b/BUILD
index 2f616c6..2f6503b 100644
--- a/BUILD
+++ b/BUILD
@@ -236,6 +236,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -305,8 +307,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -367,7 +372,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -1530,6 +1536,8 @@
         "src/trace_processor/importers/common/global_args_tracker.h",
         "src/trace_processor/importers/common/jit_cache.cc",
         "src/trace_processor/importers/common/jit_cache.h",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc",
+        "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h",
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/machine_tracker.h",
         "src/trace_processor/importers/common/mapping_tracker.cc",
@@ -1634,6 +1642,8 @@
         "src/trace_processor/importers/ftrace/iostat_tracker.h",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.cc",
         "src/trace_processor/importers/ftrace/mali_gpu_event_tracker.h",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc",
+        "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.cc",
         "src/trace_processor/importers/ftrace/pkvm_hyp_cpu_tracker.h",
         "src/trace_processor/importers/ftrace/rss_stat_tracker.cc",
@@ -1705,6 +1715,27 @@
     ],
 )
 
+# GN target: //src/trace_processor/importers/instruments:instruments
+perfetto_filegroup(
+    name = "src_trace_processor_importers_instruments_instruments",
+    srcs = [
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc",
+        "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h",
+        "src/trace_processor/importers/instruments/row_data_tracker.cc",
+        "src/trace_processor/importers/instruments/row_data_tracker.h",
+        "src/trace_processor/importers/instruments/row_parser.cc",
+        "src/trace_processor/importers/instruments/row_parser.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/instruments:row
+perfetto_filegroup(
+    name = "src_trace_processor_importers_instruments_row",
+    srcs = [
+        "src/trace_processor/importers/instruments/row.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/json:full
 perfetto_filegroup(
     name = "src_trace_processor_importers_json_full",
@@ -2751,6 +2782,7 @@
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/frequency.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle.sql",
         "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_stats.sql",
+        "src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql",
     ],
 )
 
@@ -5240,6 +5272,7 @@
         "protos/perfetto/trace/ftrace/oom.proto",
         "protos/perfetto/trace/ftrace/panel.proto",
         "protos/perfetto/trace/ftrace/perf_trace_counters.proto",
+        "protos/perfetto/trace/ftrace/pixel_mm.proto",
         "protos/perfetto/trace/ftrace/power.proto",
         "protos/perfetto/trace/ftrace/printk.proto",
         "protos/perfetto/trace/ftrace/raw_syscalls.proto",
@@ -6228,6 +6261,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6294,8 +6329,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6358,7 +6396,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
@@ -6380,8 +6419,11 @@
         ":include_perfetto_ext_traced_sys_stats_counters",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6410,6 +6452,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6531,7 +6575,8 @@
                ":src_trace_processor_metrics_gen_cc_metrics_descriptor",
                ":src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.linenoise +
            PERFETTO_CONFIG.deps.protobuf_full +
            PERFETTO_CONFIG.deps.sqlite +
@@ -6622,8 +6667,11 @@
         ":include_perfetto_profiling_pprof_builder",
         ":include_perfetto_protozero_protozero",
         ":include_perfetto_public_abi_base",
+        ":include_perfetto_public_abi_public",
         ":include_perfetto_public_base",
+        ":include_perfetto_public_protos_protos",
         ":include_perfetto_public_protozero",
+        ":include_perfetto_public_public",
         ":include_perfetto_trace_processor_basic_types",
         ":include_perfetto_trace_processor_storage",
         ":include_perfetto_trace_processor_trace_processor",
@@ -6652,6 +6700,8 @@
         ":src_trace_processor_importers_fuchsia_minimal",
         ":src_trace_processor_importers_gzip_full",
         ":src_trace_processor_importers_i2c_full",
+        ":src_trace_processor_importers_instruments_instruments",
+        ":src_trace_processor_importers_instruments_row",
         ":src_trace_processor_importers_json_full",
         ":src_trace_processor_importers_json_minimal",
         ":src_trace_processor_importers_memory_tracker_graph_processor",
@@ -6774,7 +6824,8 @@
                ":src_trace_processor_perfetto_sql_stdlib_stdlib",
                ":src_traceconv_gen_cc_trace_descriptor",
                ":src_traceconv_gen_cc_winscope_descriptor",
-           ] + PERFETTO_CONFIG.deps.jsoncpp +
+           ] + PERFETTO_CONFIG.deps.expat +
+           PERFETTO_CONFIG.deps.jsoncpp +
            PERFETTO_CONFIG.deps.sqlite +
            PERFETTO_CONFIG.deps.sqlite_ext_percentile +
            PERFETTO_CONFIG.deps.zlib +
diff --git a/bazel/deps.bzl b/bazel/deps.bzl
index 69dbcbb..d97179a 100644
--- a/bazel/deps.bzl
+++ b/bazel/deps.bzl
@@ -67,6 +67,14 @@
     )
 
     _add_repo_if_not_existing(
+        new_git_repository,
+        name = "perfetto_dep_expat",
+        remote = "https://github.com/libexpat/libexpat",
+        commit = "fa75b96546c069d17b8f80d91e0f4ef0cde3790d",  # R_2_6_2
+        build_file = "//bazel:expat.BUILD",
+    )
+
+    _add_repo_if_not_existing(
         http_archive,
         name = "perfetto_dep_zlib",
         url = "https://storage.googleapis.com/perfetto/zlib-6d3f6aa0f87c9791ca7724c279ef61384f331dfd.tar.gz",
diff --git a/bazel/expat.BUILD b/bazel/expat.BUILD
new file mode 100644
index 0000000..10f88c7
--- /dev/null
+++ b/bazel/expat.BUILD
@@ -0,0 +1,64 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@perfetto_cfg//:perfetto_cfg.bzl", "PERFETTO_CONFIG")
+
+cc_library(
+    name = "expat",
+    hdrs = glob(["expat/lib/*.h"]),
+    deps = [
+        ":expat_impl",
+    ],
+    visibility = ["//visibility:public"],
+)
+
+cc_library(
+    name = "expat_impl",
+    srcs = [
+      "expat/lib/xmlparse.c",
+      "expat/lib/xmlrole.c",
+      "expat/lib/xmltok.c",
+    ],
+    hdrs = [
+      "expat/lib/ascii.h",
+      "expat/lib/asciitab.h",
+      "expat/lib/expat.h",
+      "expat/lib/expat_external.h",
+      "expat/lib/iasciitab.h",
+      "expat/lib/internal.h",
+      "expat/lib/latin1tab.h",
+      "expat/lib/nametab.h",
+      "expat/lib/siphash.h",
+      "expat/lib/utf8tab.h",
+      "expat/lib/winconfig.h",
+      "expat/lib/xmlrole.h",
+      "expat/lib/xmltok.h",
+      "expat/lib/xmltok_impl.c",
+      "expat/lib/xmltok_impl.h",
+      "expat/lib/xmltok_ns.c",
+    ],
+    deps = [
+        "@perfetto//buildtools/expat/include:expat_config",
+    ],
+    copts = [
+        "-DHAVE_EXPAT_CONFIG_H",
+    ] + PERFETTO_CONFIG.deps_copts.expat,
+    defines = [
+        "XML_STATIC"
+    ],
+    includes = [
+        "expat",
+        "expat/lib",
+    ],
+)
diff --git a/bazel/standalone/perfetto_cfg.bzl b/bazel/standalone/perfetto_cfg.bzl
index cbabb29..254bcb8 100644
--- a/bazel/standalone/perfetto_cfg.bzl
+++ b/bazel/standalone/perfetto_cfg.bzl
@@ -45,6 +45,7 @@
         base_platform = ["//:perfetto_base_default_platform"],
 
         zlib = ["@perfetto_dep_zlib//:zlib"],
+        expat = ["@perfetto_dep_expat//:expat"],
         jsoncpp = ["@perfetto_dep_jsoncpp//:jsoncpp"],
         linenoise = ["@perfetto_dep_linenoise//:linenoise"],
         sqlite = ["@perfetto_dep_sqlite//:sqlite"],
@@ -83,6 +84,7 @@
     # initialized with the Perfetto build files (i.e. via perfetto_deps()).
     deps_copts = struct(
         zlib = [],
+        expat = [],
         jsoncpp = [],
         linenoise = [],
         sqlite = [],
diff --git a/buildtools/.gitignore b/buildtools/.gitignore
index a3e6376..42137c3 100644
--- a/buildtools/.gitignore
+++ b/buildtools/.gitignore
@@ -11,6 +11,7 @@
 /catapult_trace_viewer/
 /clang_format/
 /clang/
+/expat/src/
 /d8/
 /debian_sid_arm-sysroot/
 /debian_sid_arm64-sysroot/
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index b58b678..b4af804 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -1402,6 +1402,41 @@
   deps = [ "//gn:default_deps" ]
 }
 
+config("expat_public_config") {
+  defines = [ "XML_STATIC" ]
+  cflags = [
+    # Using -isystem instead of include_dirs (-I), so we don't need to suppress
+    # warnings coming from third-party headers. Doing so would mask warnings in
+    # our own code.
+    perfetto_isystem_cflag,
+    rebase_path("expat/src/expat/lib", root_build_dir),
+    perfetto_isystem_cflag,
+    rebase_path("expat/include", root_build_dir),
+  ]
+}
+
+config("no_format_warning") {
+  cflags = [ "-Wno-format" ]
+}
+
+static_library("expat") {
+  sources = [
+    "expat/src/expat/lib/expat.h",
+    "expat/src/expat/lib/xmlparse.c",
+    "expat/src/expat/lib/xmlrole.c",
+    "expat/src/expat/lib/xmltok.c",
+  ]
+
+  public_configs = [ ":expat_public_config" ]
+  configs -= [ "//gn/standalone:extra_warnings" ]
+  configs += [ ":no_format_warning" ]
+
+  defines = [
+    "_LIB",
+    "HAVE_EXPAT_CONFIG_H",
+  ]
+}
+
 config("linenoise_config") {
   visibility = _buildtools_visibility
   cflags = [
diff --git a/buildtools/expat/include/BUILD b/buildtools/expat/include/BUILD
new file mode 100644
index 0000000..1c6cb8f
--- /dev/null
+++ b/buildtools/expat/include/BUILD
@@ -0,0 +1,24 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+cc_library(
+    name = "expat_config",
+    hdrs = [
+      "expat_config.h",
+    ],
+    includes = [
+        ".",
+    ],
+    visibility = ["//visibility:public"],
+)
diff --git a/buildtools/expat/include/expat_config.h b/buildtools/expat/include/expat_config.h
new file mode 100644
index 0000000..8ce2388
--- /dev/null
+++ b/buildtools/expat/include/expat_config.h
@@ -0,0 +1,146 @@
+/* expat_config.h.  Generated from expat_config.h.in by configure.  */
+/* expat_config.h.in.  Generated from configure.ac by autoheader.  */
+
+#ifndef EXPAT_CONFIG_H
+#define EXPAT_CONFIG_H 1
+
+/* Define if building universal (internal helper macro) */
+/* #undef AC_APPLE_UNIVERSAL_BUILD */
+
+/* 1234 = LILENDIAN, 4321 = BIGENDIAN */
+#define BYTEORDER 1234
+
+/* Define to 1 if you have the `arc4random' function. */
+/* #undef HAVE_ARC4RANDOM */
+
+/* Define to 1 if you have the `arc4random_buf' function. */
+/* #define HAVE_ARC4RANDOM_BUF 1 */
+
+/* define if the compiler supports basic C++11 syntax */
+#define HAVE_CXX11 1
+
+/* Define to 1 if you have the <dlfcn.h> header file. */
+#define HAVE_DLFCN_H 1
+
+/* Define to 1 if you have the <fcntl.h> header file. */
+#define HAVE_FCNTL_H 1
+
+/* Define to 1 if you have the `getpagesize' function. */
+#define HAVE_GETPAGESIZE 1
+
+/* Define to 1 if you have the `getrandom' function. */
+/* #define HAVE_GETRANDOM 1 */
+
+/* Define to 1 if you have the <inttypes.h> header file. */
+#define HAVE_INTTYPES_H 1
+
+/* Define to 1 if you have the `bsd' library (-lbsd). */
+/* #undef HAVE_LIBBSD */
+
+/* Define to 1 if you have a working `mmap' system call. */
+#define HAVE_MMAP 1
+
+/* Define to 1 if you have the <stdint.h> header file. */
+#define HAVE_STDINT_H 1
+
+/* Define to 1 if you have the <stdio.h> header file. */
+#define HAVE_STDIO_H 1
+
+/* Define to 1 if you have the <stdlib.h> header file. */
+#define HAVE_STDLIB_H 1
+
+/* Define to 1 if you have the <strings.h> header file. */
+#define HAVE_STRINGS_H 1
+
+/* Define to 1 if you have the <string.h> header file. */
+#define HAVE_STRING_H 1
+
+/* Define to 1 if you have `syscall' and `SYS_getrandom'. */
+/* #define HAVE_SYSCALL_GETRANDOM 1 */
+
+/* Define to 1 if you have the <sys/param.h> header file. */
+#define HAVE_SYS_PARAM_H 1
+
+/* Define to 1 if you have the <sys/stat.h> header file. */
+#define HAVE_SYS_STAT_H 1
+
+/* Define to 1 if you have the <sys/types.h> header file. */
+#define HAVE_SYS_TYPES_H 1
+
+/* Define to 1 if you have the <unistd.h> header file. */
+#define HAVE_UNISTD_H 1
+
+/* Define to the sub-directory where libtool stores uninstalled libraries. */
+#define LT_OBJDIR ".libs/"
+
+/* Name of package */
+#define PACKAGE "expat"
+
+/* Define to the address where bug reports for this package should be sent. */
+#define PACKAGE_BUGREPORT "https://github.com/libexpat/libexpat/issues"
+
+/* Define to the full name of this package. */
+#define PACKAGE_NAME "expat"
+
+/* Define to the full name and version of this package. */
+#define PACKAGE_STRING "expat 2.6.2"
+
+/* Define to the one symbol short name of this package. */
+#define PACKAGE_TARNAME "expat"
+
+/* Define to the home page for this package. */
+#define PACKAGE_URL ""
+
+/* Define to the version of this package. */
+#define PACKAGE_VERSION "2.6.2"
+
+/* Define to 1 if all of the C90 standard headers exist (not just the ones
+   required in a freestanding environment). This macro is provided for
+   backward compatibility; new code need not use it. */
+#define STDC_HEADERS 1
+
+/* Version number of package */
+#define VERSION "2.6.2"
+
+/* Define WORDS_BIGENDIAN to 1 if your processor stores words with the most
+   significant byte first (like Motorola and SPARC, unlike Intel). */
+#if defined AC_APPLE_UNIVERSAL_BUILD
+#if defined __BIG_ENDIAN__
+#define WORDS_BIGENDIAN 1
+#endif
+#else
+#ifndef WORDS_BIGENDIAN
+/* #  undef WORDS_BIGENDIAN */
+#endif
+#endif
+
+/* Define to allow retrieving the byte offsets for attribute names and values.
+ */
+/* #undef XML_ATTR_INFO */
+
+/* Define to specify how much context to retain around the current parse
+   point, 0 to disable. */
+#define XML_CONTEXT_BYTES 1024
+
+/* Define to include code reading entropy from `/dev/urandom'. */
+#define XML_DEV_URANDOM 1
+
+/* Define to make parameter entity parsing functionality available. */
+#define XML_DTD 1
+
+/* Define as 1/0 to enable/disable support for general entities. */
+#define XML_GE 1
+
+/* Define to make XML Namespaces functionality available. */
+#define XML_NS 1
+
+/* Define to empty if `const' does not conform to ANSI C. */
+/* #undef const */
+
+/* Define to `long int' if <sys/types.h> does not define. */
+/* #undef off_t */
+
+/* Define to `unsigned int' if <sys/types.h> does not define. */
+/* #undef size_t */
+
+#endif  // ndef EXPAT_CONFIG_H
\ No newline at end of file
diff --git a/gn/BUILD.gn b/gn/BUILD.gn
index e61fb9f..bfa7a8e 100644
--- a/gn/BUILD.gn
+++ b/gn/BUILD.gn
@@ -363,6 +363,14 @@
   }
 }
 
+group("expat") {
+  if (perfetto_root_path == "//") {
+    public_deps = [ "//buildtools:expat" ]
+  } else {
+    public_deps = [ "//third_party/expat:expat" ]
+  }
+}
+
 if (enable_perfetto_trace_processor_json) {
   group("jsoncpp") {
     if (perfetto_root_path == "//") {
diff --git a/protos/perfetto/metrics/android/wattson_in_time_period.proto b/protos/perfetto/metrics/android/wattson_in_time_period.proto
index 2f02e4d..9691b66 100644
--- a/protos/perfetto/metrics/android/wattson_in_time_period.proto
+++ b/protos/perfetto/metrics/android/wattson_in_time_period.proto
@@ -31,7 +31,7 @@
 
 message AndroidWattsonCpuSubsystemEstimate {
   // estimates and estimates of subrails
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   optional AndroidWattsonPolicyEstimate policy0 = 2;
   optional AndroidWattsonPolicyEstimate policy1 = 3;
   optional AndroidWattsonPolicyEstimate policy2 = 4;
@@ -44,7 +44,7 @@
 }
 
 message AndroidWattsonPolicyEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   optional AndroidWattsonCpuEstimate cpu0 = 2;
   optional AndroidWattsonCpuEstimate cpu1 = 3;
   optional AndroidWattsonCpuEstimate cpu2 = 4;
@@ -56,9 +56,9 @@
 }
 
 message AndroidWattsonCpuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
 }
 
 message AndroidWattsonDsuScuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
 }
diff --git a/protos/perfetto/metrics/android/wattson_tasks_attribution.proto b/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
index f505c04..43b79df 100644
--- a/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
+++ b/protos/perfetto/metrics/android/wattson_tasks_attribution.proto
@@ -26,9 +26,9 @@
 
 message AndroidWattsonTaskInfo {
   // Average estimated power for wall duration in mW
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   // Total energy over wall duration across CPUs in mWs
-  optional float estimate_mws = 2;
+  optional float estimated_mws = 2;
   optional string thread_name = 3;
   optional string process_name = 4;
   optional string package_name = 5;
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 7ef002f..621316c 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -2895,7 +2895,7 @@
 
 message AndroidWattsonCpuSubsystemEstimate {
   // estimates and estimates of subrails
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   optional AndroidWattsonPolicyEstimate policy0 = 2;
   optional AndroidWattsonPolicyEstimate policy1 = 3;
   optional AndroidWattsonPolicyEstimate policy2 = 4;
@@ -2908,7 +2908,7 @@
 }
 
 message AndroidWattsonPolicyEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   optional AndroidWattsonCpuEstimate cpu0 = 2;
   optional AndroidWattsonCpuEstimate cpu1 = 3;
   optional AndroidWattsonCpuEstimate cpu2 = 4;
@@ -2920,11 +2920,11 @@
 }
 
 message AndroidWattsonCpuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
 }
 
 message AndroidWattsonDsuScuEstimate {
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
 }
 
 // End of protos/perfetto/metrics/android/wattson_in_time_period.proto
@@ -2939,9 +2939,9 @@
 
 message AndroidWattsonTaskInfo {
   // Average estimated power for wall duration in mW
-  optional float estimate_mw = 1;
+  optional float estimated_mw = 1;
   // Total energy over wall duration across CPUs in mWs
-  optional float estimate_mws = 2;
+  optional float estimated_mws = 2;
   optional string thread_name = 3;
   optional string process_name = 4;
   optional string package_name = 5;
diff --git a/protos/perfetto/trace/ftrace/all_protos.gni b/protos/perfetto/trace/ftrace/all_protos.gni
index 1cb6085..5aae5d9 100644
--- a/protos/perfetto/trace/ftrace/all_protos.gni
+++ b/protos/perfetto/trace/ftrace/all_protos.gni
@@ -63,6 +63,7 @@
   "oom.proto",
   "panel.proto",
   "perf_trace_counters.proto",
+  "pixel_mm.proto",
   "power.proto",
   "printk.proto",
   "raw_syscalls.proto",
diff --git a/protos/perfetto/trace/ftrace/ftrace_event.proto b/protos/perfetto/trace/ftrace/ftrace_event.proto
index 4a0a589..877618a 100644
--- a/protos/perfetto/trace/ftrace/ftrace_event.proto
+++ b/protos/perfetto/trace/ftrace/ftrace_event.proto
@@ -63,6 +63,7 @@
 import "protos/perfetto/trace/ftrace/oom.proto";
 import "protos/perfetto/trace/ftrace/panel.proto";
 import "protos/perfetto/trace/ftrace/perf_trace_counters.proto";
+import "protos/perfetto/trace/ftrace/pixel_mm.proto";
 import "protos/perfetto/trace/ftrace/power.proto";
 import "protos/perfetto/trace/ftrace/printk.proto";
 import "protos/perfetto/trace/ftrace/raw_syscalls.proto";
@@ -674,5 +675,7 @@
     KgslAdrenoCmdbatchSubmittedFtraceEvent kgsl_adreno_cmdbatch_submitted = 535;
     KgslAdrenoCmdbatchSyncFtraceEvent kgsl_adreno_cmdbatch_sync = 536;
     KgslAdrenoCmdbatchRetiredFtraceEvent kgsl_adreno_cmdbatch_retired = 537;
+    PixelMmKswapdWakeFtraceEvent pixel_mm_kswapd_wake = 538;
+    PixelMmKswapdDoneFtraceEvent pixel_mm_kswapd_done = 539;
   }
 }
diff --git a/protos/perfetto/trace/ftrace/perf_trace_counters.proto b/protos/perfetto/trace/ftrace/perf_trace_counters.proto
index 0e3531c..337e492 100644
--- a/protos/perfetto/trace/ftrace/perf_trace_counters.proto
+++ b/protos/perfetto/trace/ftrace/perf_trace_counters.proto
@@ -8,19 +8,25 @@
 message SchedSwitchWithCtrsFtraceEvent {
   optional int32 old_pid = 1;
   optional int32 new_pid = 2;
-  optional uint32 cctr = 3;
-  optional uint32 ctr0 = 4;
-  optional uint32 ctr1 = 5;
-  optional uint32 ctr2 = 6;
-  optional uint32 ctr3 = 7;
+  optional uint64 cctr = 3;
+  optional uint64 ctr0 = 4;
+  optional uint64 ctr1 = 5;
+  optional uint64 ctr2 = 6;
+  optional uint64 ctr3 = 7;
   optional uint32 lctr0 = 8;
   optional uint32 lctr1 = 9;
-  optional uint32 ctr4 = 10;
-  optional uint32 ctr5 = 11;
+  optional uint64 ctr4 = 10;
+  optional uint64 ctr5 = 11;
   optional string prev_comm = 12;
   optional int32 prev_pid = 13;
   optional uint32 cyc = 14;
   optional uint32 inst = 15;
   optional uint32 stallbm = 16;
   optional uint32 l3dm = 17;
+  optional int32 next_pid = 18;
+  optional string next_comm = 19;
+  optional int64 prev_state = 20;
+  optional uint64 amu0 = 21;
+  optional uint64 amu1 = 22;
+  optional uint64 amu2 = 23;
 }
diff --git a/protos/perfetto/trace/ftrace/pixel_mm.proto b/protos/perfetto/trace/ftrace/pixel_mm.proto
new file mode 100644
index 0000000..5c323e4
--- /dev/null
+++ b/protos/perfetto/trace/ftrace/pixel_mm.proto
@@ -0,0 +1,14 @@
+// Autogenerated by:
+// ../../src/tools/ftrace_proto_gen/ftrace_proto_gen.cc
+// Do not edit.
+
+syntax = "proto2";
+package perfetto.protos;
+
+message PixelMmKswapdWakeFtraceEvent {
+  optional int32 whatever = 1;
+}
+message PixelMmKswapdDoneFtraceEvent {
+  optional uint64 delta_nr_scanned = 1;
+  optional uint64 delta_nr_reclaimed = 2;
+}
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index a3822b2..a13fdff 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -9662,25 +9662,43 @@
 message SchedSwitchWithCtrsFtraceEvent {
   optional int32 old_pid = 1;
   optional int32 new_pid = 2;
-  optional uint32 cctr = 3;
-  optional uint32 ctr0 = 4;
-  optional uint32 ctr1 = 5;
-  optional uint32 ctr2 = 6;
-  optional uint32 ctr3 = 7;
+  optional uint64 cctr = 3;
+  optional uint64 ctr0 = 4;
+  optional uint64 ctr1 = 5;
+  optional uint64 ctr2 = 6;
+  optional uint64 ctr3 = 7;
   optional uint32 lctr0 = 8;
   optional uint32 lctr1 = 9;
-  optional uint32 ctr4 = 10;
-  optional uint32 ctr5 = 11;
+  optional uint64 ctr4 = 10;
+  optional uint64 ctr5 = 11;
   optional string prev_comm = 12;
   optional int32 prev_pid = 13;
   optional uint32 cyc = 14;
   optional uint32 inst = 15;
   optional uint32 stallbm = 16;
   optional uint32 l3dm = 17;
+  optional int32 next_pid = 18;
+  optional string next_comm = 19;
+  optional int64 prev_state = 20;
+  optional uint64 amu0 = 21;
+  optional uint64 amu1 = 22;
+  optional uint64 amu2 = 23;
 }
 
 // End of protos/perfetto/trace/ftrace/perf_trace_counters.proto
 
+// Begin of protos/perfetto/trace/ftrace/pixel_mm.proto
+
+message PixelMmKswapdWakeFtraceEvent {
+  optional int32 whatever = 1;
+}
+message PixelMmKswapdDoneFtraceEvent {
+  optional uint64 delta_nr_scanned = 1;
+  optional uint64 delta_nr_reclaimed = 2;
+}
+
+// End of protos/perfetto/trace/ftrace/pixel_mm.proto
+
 // Begin of protos/perfetto/trace/ftrace/power.proto
 
 message CpuFrequencyFtraceEvent {
@@ -11092,6 +11110,8 @@
     KgslAdrenoCmdbatchSubmittedFtraceEvent kgsl_adreno_cmdbatch_submitted = 535;
     KgslAdrenoCmdbatchSyncFtraceEvent kgsl_adreno_cmdbatch_sync = 536;
     KgslAdrenoCmdbatchRetiredFtraceEvent kgsl_adreno_cmdbatch_retired = 537;
+    PixelMmKswapdWakeFtraceEvent pixel_mm_kswapd_wake = 538;
+    PixelMmKswapdDoneFtraceEvent pixel_mm_kswapd_done = 539;
   }
 }
 
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 7b9052d..7c2cd5c 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/python/tools/check_imports.py b/python/tools/check_imports.py
index 464683e..c61f3bb 100755
--- a/python/tools/check_imports.py
+++ b/python/tools/check_imports.py
@@ -242,11 +242,6 @@
         r"/core_plugins/.*",
         "core code should not depend on plugins.",
     ),
-    #NoDirectDep(
-    #    r'/tracks/.*',
-    #    r'/core/.*',
-    #    'instead tracks should depend on the API exposed at ui/src/public.',
-    #),
     NoDep(
         r'/core/.*',
         r'/plugins/.*',
diff --git a/src/profiling/symbolizer/local_symbolizer.cc b/src/profiling/symbolizer/local_symbolizer.cc
index c8ad70f..5271e82 100644
--- a/src/profiling/symbolizer/local_symbolizer.cc
+++ b/src/profiling/symbolizer/local_symbolizer.cc
@@ -116,7 +116,7 @@
 }
 
 template <typename E>
-std::optional<uint64_t> GetLoadBias(void* mem, size_t size) {
+std::optional<uint64_t> GetElfLoadBias(void* mem, size_t size) {
   const typename E::Ehdr* ehdr = static_cast<typename E::Ehdr*>(mem);
   if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
     PERFETTO_ELOG("Corrupted ELF.");
@@ -136,7 +136,7 @@
 }
 
 template <typename E>
-std::optional<std::string> GetBuildId(void* mem, size_t size) {
+std::optional<std::string> GetElfBuildId(void* mem, size_t size) {
   const typename E::Ehdr* ehdr = static_cast<typename E::Ehdr*>(mem);
   if (!InRange(mem, size, ehdr, sizeof(typename E::Ehdr))) {
     PERFETTO_ELOG("Corrupted ELF.");
@@ -202,13 +202,103 @@
           mem[EI_MAG2] == ELFMAG2 && mem[EI_MAG3] == ELFMAG3);
 }
 
-struct BuildIdAndLoadBias {
-  std::string build_id;
-  uint64_t load_bias;
+constexpr uint32_t kMachO64Magic = 0xfeedfacf;
+
+bool IsMachO64(const char* mem, size_t size) {
+  if (size < sizeof(kMachO64Magic))
+    return false;
+  return memcmp(mem, &kMachO64Magic, sizeof(kMachO64Magic)) == 0;
+}
+
+struct mach_header_64 {
+  uint32_t magic;      /* mach magic number identifier */
+  int32_t cputype;     /* cpu specifier */
+  int32_t cpusubtype;  /* machine specifier */
+  uint32_t filetype;   /* type of file */
+  uint32_t ncmds;      /* number of load commands */
+  uint32_t sizeofcmds; /* the size of all the load commands */
+  uint32_t flags;      /* flags */
+  uint32_t reserved;   /* reserved */
 };
 
-std::optional<BuildIdAndLoadBias> GetBuildIdAndLoadBias(const char* fname,
-                                                        size_t size) {
+struct load_command {
+  uint32_t cmd;     /* type of load command */
+  uint32_t cmdsize; /* total size of command in bytes */
+};
+
+struct segment_64_command {
+  uint32_t cmd;      /* LC_SEGMENT_64 */
+  uint32_t cmdsize;  /* includes sizeof section_64 structs */
+  char segname[16];  /* segment name */
+  uint64_t vmaddr;   /* memory address of this segment */
+  uint64_t vmsize;   /* memory size of this segment */
+  uint64_t fileoff;  /* file offset of this segment */
+  uint64_t filesize; /* amount to map from the file */
+  uint32_t maxprot;  /* maximum VM protection */
+  uint32_t initprot; /* initial VM protection */
+  uint32_t nsects;   /* number of sections in segment */
+  uint32_t flags;    /* flags */
+};
+
+struct BinaryInfo {
+  std::string build_id;
+  uint64_t load_bias;
+  BinaryType type;
+};
+
+std::optional<BinaryInfo> GetMachOBinaryInfo(char* mem, size_t size) {
+  if (size < sizeof(mach_header_64))
+    return {};
+
+  mach_header_64 header;
+  memcpy(&header, mem, sizeof(mach_header_64));
+
+  if (size < sizeof(mach_header_64) + header.sizeofcmds)
+    return {};
+
+  std::optional<std::string> build_id;
+  uint64_t load_bias = 0;
+
+  char* pcmd = mem + sizeof(mach_header_64);
+  char* pcmds_end = pcmd + header.sizeofcmds;
+  while (pcmd < pcmds_end) {
+    load_command cmd_header;
+    memcpy(&cmd_header, pcmd, sizeof(load_command));
+
+    constexpr uint32_t LC_SEGMENT_64 = 0x19;
+    constexpr uint32_t LC_UUID = 0x1b;
+
+    switch (cmd_header.cmd) {
+      case LC_UUID: {
+        build_id = std::string(pcmd + sizeof(load_command),
+                               cmd_header.cmdsize - sizeof(load_command));
+        break;
+      }
+      case LC_SEGMENT_64: {
+        segment_64_command seg_cmd;
+        memcpy(&seg_cmd, pcmd, sizeof(segment_64_command));
+        if (strcmp(seg_cmd.segname, "__TEXT") == 0) {
+          load_bias = seg_cmd.vmaddr;
+        }
+        break;
+      }
+      default:
+        break;
+    }
+
+    pcmd += cmd_header.cmdsize;
+  }
+
+  if (build_id) {
+    constexpr uint32_t MH_DSYM = 0xa;
+    BinaryType type = header.filetype == MH_DSYM ? BinaryType::kMachODsym
+                                                 : BinaryType::kMachO;
+    return BinaryInfo{*build_id, load_bias, type};
+  }
+  return {};
+}
+
+std::optional<BinaryInfo> GetBinaryInfo(const char* fname, size_t size) {
   static_assert(EI_CLASS > EI_MAG3, "mem[EI_MAG?] accesses are in range.");
   if (size <= EI_CLASS)
     return std::nullopt;
@@ -219,25 +309,26 @@
   }
   char* mem = static_cast<char*>(map.data());
 
-  if (!IsElf(mem, size))
-    return std::nullopt;
-
   std::optional<std::string> build_id;
   std::optional<uint64_t> load_bias;
-  switch (mem[EI_CLASS]) {
-    case ELFCLASS32:
-      build_id = GetBuildId<Elf32>(mem, size);
-      load_bias = GetLoadBias<Elf32>(mem, size);
-      break;
-    case ELFCLASS64:
-      build_id = GetBuildId<Elf64>(mem, size);
-      load_bias = GetLoadBias<Elf64>(mem, size);
-      break;
-    default:
-      return std::nullopt;
-  }
-  if (build_id && load_bias) {
-    return BuildIdAndLoadBias{*build_id, *load_bias};
+  if (IsElf(mem, size)) {
+    switch (mem[EI_CLASS]) {
+      case ELFCLASS32:
+        build_id = GetElfBuildId<Elf32>(mem, size);
+        load_bias = GetElfLoadBias<Elf32>(mem, size);
+        break;
+      case ELFCLASS64:
+        build_id = GetElfBuildId<Elf64>(mem, size);
+        load_bias = GetElfLoadBias<Elf64>(mem, size);
+        break;
+      default:
+        return std::nullopt;
+    }
+    if (build_id && load_bias) {
+      return BinaryInfo{*build_id, *load_bias, BinaryType::kElf};
+    }
+  } else if (IsMachO64(mem, size)) {
+    return GetMachOBinaryInfo(mem, size);
   }
   return std::nullopt;
 }
@@ -245,6 +336,7 @@
 std::map<std::string, FoundBinary> BuildIdIndex(std::vector<std::string> dirs) {
   std::map<std::string, FoundBinary> result;
   WalkDirectories(std::move(dirs), [&result](const char* fname, size_t size) {
+    static_assert(EI_MAG3 + 1 == sizeof(kMachO64Magic));
     char magic[EI_MAG3 + 1];
     // Scope file access. On windows OpenFile opens an exclusive lock.
     // This lock needs to be released before mapping the file.
@@ -259,16 +351,39 @@
         PERFETTO_PLOG("Failed to read %s", fname);
         return;
       }
-      if (!IsElf(magic, static_cast<size_t>(rd))) {
-        PERFETTO_DLOG("%s not an ELF.", fname);
+      if (!IsElf(magic, static_cast<size_t>(rd)) &&
+          !IsMachO64(magic, static_cast<size_t>(rd))) {
+        PERFETTO_DLOG("%s not an ELF or Mach-O 64.", fname);
         return;
       }
     }
-    std::optional<BuildIdAndLoadBias> build_id_and_load_bias =
-        GetBuildIdAndLoadBias(fname, size);
-    if (build_id_and_load_bias) {
-      result.emplace(build_id_and_load_bias->build_id,
-                     FoundBinary{fname, build_id_and_load_bias->load_bias});
+    std::optional<BinaryInfo> binary_info = GetBinaryInfo(fname, size);
+    if (!binary_info) {
+      PERFETTO_DLOG("Failed to extract build id from %s.", fname);
+      return;
+    }
+    auto it = result.emplace(
+        binary_info->build_id,
+        FoundBinary{fname, binary_info->load_bias, binary_info->type});
+
+    // If there was already an existing FoundBinary, the emplace wouldn't insert
+    // anything. But, for Mac binaries, we prefer dSYM files over the original
+    // binary, so make sure these overwrite the FoundBinary entry.
+    bool has_existing = it.second == false;
+    if (has_existing) {
+      if (it.first->second.type == BinaryType::kMachO &&
+          binary_info->type == BinaryType::kMachODsym) {
+        PERFETTO_LOG("Overwriting index entry for %s to %s.",
+                     base::ToHex(binary_info->build_id).c_str(), fname);
+        it.first->second =
+            FoundBinary{fname, binary_info->load_bias, binary_info->type};
+      } else {
+        PERFETTO_DLOG("Ignoring %s, index entry for %s already exists.", fname,
+                      base::ToHex(binary_info->build_id).c_str());
+      }
+    } else {
+      PERFETTO_LOG("Indexed: %s (%s)", fname,
+                   base::ToHex(binary_info->build_id).c_str());
     }
   });
   return result;
@@ -548,6 +663,13 @@
 
   std::optional<FoundBinary>& cache_entry = p.first->second;
 
+  // Try the absolute path first.
+  if (base::StartsWith(abspath, "/")) {
+    cache_entry = IsCorrectFile(abspath, build_id);
+    if (cache_entry)
+      return cache_entry;
+  }
+
   for (const std::string& root_str : roots_) {
     cache_entry = FindBinaryInRoot(root_str, abspath, build_id);
     if (cache_entry)
@@ -579,14 +701,14 @@
     return std::nullopt;
   }
 
-  std::optional<BuildIdAndLoadBias> build_id_and_load_bias =
-      GetBuildIdAndLoadBias(symbol_file.c_str(), size);
-  if (!build_id_and_load_bias)
+  std::optional<BinaryInfo> binary_info =
+      GetBinaryInfo(symbol_file.c_str(), size);
+  if (!binary_info)
     return std::nullopt;
-  if (build_id_and_load_bias->build_id != build_id) {
+  if (binary_info->build_id != build_id) {
     return std::nullopt;
   }
-  return FoundBinary{symbol_file, build_id_and_load_bias->load_bias};
+  return FoundBinary{symbol_file, binary_info->load_bias, binary_info->type};
 }
 
 std::optional<FoundBinary> LocalBinaryFinder::FindBinaryInRoot(
diff --git a/src/profiling/symbolizer/local_symbolizer.h b/src/profiling/symbolizer/local_symbolizer.h
index d7185ef..36a8718 100644
--- a/src/profiling/symbolizer/local_symbolizer.h
+++ b/src/profiling/symbolizer/local_symbolizer.h
@@ -33,10 +33,16 @@
 
 bool ParseLlvmSymbolizerJsonLine(const std::string& line,
                                  std::vector<SymbolizedFrame>* result);
+enum BinaryType {
+  kElf,
+  kMachO,
+  kMachODsym,
+};
 
 struct FoundBinary {
   std::string file_name;
   uint64_t load_bias;
+  BinaryType type;
 };
 
 class BinaryFinder {
diff --git a/src/tools/ftrace_proto_gen/event_list b/src/tools/ftrace_proto_gen/event_list
index 9d5553d..aecdad3 100644
--- a/src/tools/ftrace_proto_gen/event_list
+++ b/src/tools/ftrace_proto_gen/event_list
@@ -531,4 +531,6 @@
 kgsl/adreno_cmdbatch_queued
 kgsl/adreno_cmdbatch_submitted
 kgsl/adreno_cmdbatch_sync
-kgsl/adreno_cmdbatch_retired
\ No newline at end of file
+kgsl/adreno_cmdbatch_retired
+pixel_mm/pixel_mm_kswapd_wake
+pixel_mm/pixel_mm_kswapd_done
\ No newline at end of file
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index ec71357..e063ea6 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -174,6 +174,7 @@
       "importers/ftrace:full",
       "importers/fuchsia:full",
       "importers/gzip:full",
+      "importers/instruments",
       "importers/json:full",
       "importers/json:minimal",
       "importers/ninja",
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 70bffd5..b2250fd 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -57,6 +57,7 @@
       return std::nullopt;
 
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
       return TraceSorter::SortingMode::kDefault;
 
     case kUnknownTraceType:
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
index c2f61da..d128b43 100644
--- a/src/trace_processor/importers/common/BUILD.gn
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -41,6 +41,8 @@
     "global_args_tracker.h",
     "jit_cache.cc",
     "jit_cache.h",
+    "legacy_v8_cpu_profile_tracker.cc",
+    "legacy_v8_cpu_profile_tracker.h",
     "machine_tracker.cc",
     "machine_tracker.h",
     "mapping_tracker.cc",
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
new file mode 100644
index 0000000..9206d87
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.cc
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+LegacyV8CpuProfileTracker::LegacyV8CpuProfileTracker(
+    TraceProcessorContext* context)
+    : context_(context) {}
+
+void LegacyV8CpuProfileTracker::SetStartTsForSessionAndPid(uint64_t session_id,
+                                                           uint32_t pid,
+                                                           int64_t ts) {
+  auto [it, inserted] = state_by_session_and_pid_.Insert(
+      std::make_pair(session_id, pid),
+      State{ts, base::FlatHashMap<uint32_t, CallsiteId>(), nullptr});
+  it->ts = ts;
+  if (inserted) {
+    it->mapping = &context_->mapping_tracker->CreateDummyMapping("");
+  }
+}
+
+base::Status LegacyV8CpuProfileTracker::AddCallsite(
+    uint64_t session_id,
+    uint32_t pid,
+    uint32_t raw_callsite_id,
+    std::optional<uint32_t> parent_raw_callsite_id,
+    base::StringView script_url,
+    base::StringView function_name) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot insert callsite");
+  }
+  FrameId frame_id =
+      state->mapping->InternDummyFrame(function_name, script_url);
+  CallsiteId callsite_id;
+  if (parent_raw_callsite_id) {
+    auto* parent_id = state->callsites.Find(*parent_raw_callsite_id);
+    if (!parent_id) {
+      return base::ErrStatus(
+          "v8 profile parent id does not exist: cannot insert callsite");
+    }
+    auto row =
+        context_->storage->stack_profile_callsite_table().FindById(*parent_id);
+    callsite_id = context_->stack_profile_tracker->InternCallsite(
+        *parent_id, frame_id, row->depth() + 1);
+  } else {
+    callsite_id = context_->stack_profile_tracker->InternCallsite(std::nullopt,
+                                                                  frame_id, 0);
+  }
+  if (!state->callsites.Insert(raw_callsite_id, callsite_id).second) {
+    return base::ErrStatus("v8 profile: callsite with id already exists");
+  }
+  return base::OkStatus();
+}
+
+base::StatusOr<int64_t> LegacyV8CpuProfileTracker::AddDeltaAndGetTs(
+    uint64_t session_id,
+    uint32_t pid,
+    int64_t delta_ts) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus(
+        "v8 profile id does not exist: cannot compute timestamp from delta");
+  }
+  state->ts += delta_ts;
+  return state->ts;
+}
+
+base::Status LegacyV8CpuProfileTracker::AddSample(int64_t ts,
+                                                  uint64_t session_id,
+                                                  uint32_t pid,
+                                                  uint32_t tid,
+                                                  uint32_t raw_callsite_id) {
+  auto* state = state_by_session_and_pid_.Find(std::make_pair(session_id, pid));
+  if (!state) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  auto* id = state->callsites.Find(raw_callsite_id);
+  if (!id) {
+    return base::ErrStatus("v8 callsite id does not exist: cannot add sample");
+  }
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  auto* samples = context_->storage->mutable_cpu_profile_stack_sample_table();
+  samples->Insert({ts, *id, utid, 0});
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
new file mode 100644
index 0000000..bdb2e78
--- /dev/null
+++ b/src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
+
+#include <cstdint>
+#include <optional>
+#include <utility>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor {
+
+// Stores interned callsites for given pid for legacy v8 samples.
+class LegacyV8CpuProfileTracker {
+ public:
+  explicit LegacyV8CpuProfileTracker(TraceProcessorContext*);
+
+  // Sets the start timestamp for the given pid.
+  void SetStartTsForSessionAndPid(uint64_t session_id,
+                                  uint32_t pid,
+                                  int64_t ts);
+
+  // Adds the callsite with for the given session and pid and given raw callsite
+  // id.
+  base::Status AddCallsite(uint64_t session_id,
+                           uint32_t pid,
+                           uint32_t raw_callsite_id,
+                           std::optional<uint32_t> parent_raw_callsite_id,
+                           base::StringView script_url,
+                           base::StringView function_name);
+
+  // Increments the current timestamp for the given session and pid by
+  // |delta_ts| and returns the resulting full timestamp.
+  base::StatusOr<int64_t> AddDeltaAndGetTs(uint64_t session_id,
+                                           uint32_t pid,
+                                           int64_t delta_ts);
+
+  // Adds the sample with for the given session and pid/tid and given raw
+  // callsite id.
+  base::Status AddSample(int64_t ts,
+                         uint64_t session_id,
+                         uint32_t pid,
+                         uint32_t tid,
+                         uint32_t raw_callsite_id);
+
+ private:
+  struct State {
+    int64_t ts;
+    base::FlatHashMap<uint32_t, CallsiteId> callsites;
+    DummyMemoryMapping* mapping;
+  };
+  struct Hasher {
+    uint64_t operator()(const std::pair<uint64_t, uint32_t>& res) {
+      return base::Hasher::Combine(res.first, res.second);
+    }
+  };
+  base::FlatHashMap<std::pair<uint64_t, uint32_t>, State, Hasher>
+      state_by_session_and_pid_;
+
+  TraceProcessorContext* const context_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_LEGACY_V8_CPU_PROFILE_TRACKER_H_
diff --git a/src/trace_processor/importers/common/mapping_tracker.cc b/src/trace_processor/importers/common/mapping_tracker.cc
index 02976f9..6e6fbf3 100644
--- a/src/trace_processor/importers/common/mapping_tracker.cc
+++ b/src/trace_processor/importers/common/mapping_tracker.cc
@@ -25,6 +25,7 @@
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/build_id.h"
@@ -161,14 +162,15 @@
       });
 }
 
-VirtualMemoryMapping* MappingTracker::GetDummyMapping() {
-  if (!dummy_mapping_) {
-    CreateMappingParams params;
-    params.memory_range =
-        AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
-    dummy_mapping_ = &InternMemoryMapping(params);
-  }
-  return dummy_mapping_;
+DummyMemoryMapping& MappingTracker::CreateDummyMapping(std::string name) {
+  CreateMappingParams params;
+  params.name = std::move(name);
+  params.memory_range =
+      AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
+  std::unique_ptr<DummyMemoryMapping> mapping(
+      new DummyMemoryMapping(context_, std::move(params)));
+
+  return AddMapping(std::move(mapping));
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/mapping_tracker.h b/src/trace_processor/importers/common/mapping_tracker.h
index 4791dba..c655d57 100644
--- a/src/trace_processor/importers/common/mapping_tracker.h
+++ b/src/trace_processor/importers/common/mapping_tracker.h
@@ -68,6 +68,10 @@
   UserMemoryMapping& CreateUserMemoryMapping(UniquePid upid,
                                              CreateMappingParams params);
 
+  // Sometimes we just need a mapping and we are lacking trace data to create a
+  // proper one. Use this mapping in those cases.
+  DummyMemoryMapping& CreateDummyMapping(std::string name);
+
   // Create an "other" mapping. Returned reference will be valid for the
   // duration of this instance.
   VirtualMemoryMapping& InternMemoryMapping(CreateMappingParams params);
@@ -91,10 +95,6 @@
   // Jitted ranges will only be applied to UserMemoryMappings
   void AddJitRange(UniquePid upid, AddressRange range, JitCache* jit_cache);
 
-  // Sometimes we just need a mapping and we are lacking trace data to create a
-  // proper one. Use this mapping in those cases.
-  VirtualMemoryMapping* GetDummyMapping();
-
  private:
   template <typename MappingImpl>
   MappingImpl& AddMapping(std::unique_ptr<MappingImpl> mapping);
@@ -140,8 +140,6 @@
   KernelMemoryMapping* kernel_ = nullptr;
 
   base::FlatHashMap<UniquePid, AddressRangeMap<JitCache*>> jit_caches_;
-
-  VirtualMemoryMapping* dummy_mapping_ = nullptr;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/parser_types.h b/src/trace_processor/importers/common/parser_types.h
index 7290626..bd32bde 100644
--- a/src/trace_processor/importers/common/parser_types.h
+++ b/src/trace_processor/importers/common/parser_types.h
@@ -17,14 +17,19 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
 
-#include <stdint.h>
+#include <array>
+#include <cstdint>
+#include <functional>
+#include <optional>
+#include <string>
+#include <utility>
 
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 struct alignas(8) InlineSchedSwitch {
   int64_t prev_state;
@@ -32,6 +37,11 @@
   int32_t next_prio;
   StringPool::Id next_comm;
 };
+static_assert(sizeof(InlineSchedSwitch) == 24);
+
+// We enforce the exact size as it's critical for peak-memory use when sorting
+// data in trace processor that this struct is as small as possible.
+static_assert(sizeof(InlineSchedSwitch) == 24);
 
 struct alignas(8) InlineSchedWaking {
   int32_t pid;
@@ -40,18 +50,23 @@
   StringPool::Id comm;
   uint16_t common_flags;
 };
+
+// We enforce the exact size as it's critical for peak-memory use when sorting
+// data in trace processor that this struct is as small as possible.
 static_assert(sizeof(InlineSchedWaking) == 16);
 
 struct alignas(8) JsonEvent {
   std::string value;
 };
+static_assert(sizeof(JsonEvent) % 8 == 0);
 
-struct TracePacketData {
+struct alignas(8) TracePacketData {
   TraceBlobView packet;
   RefPtr<PacketSequenceStateGeneration> sequence_state;
 };
+static_assert(sizeof(TracePacketData) % 8 == 0);
 
-struct TrackEventData {
+struct alignas(8) TrackEventData {
   TrackEventData(TraceBlobView pv,
                  RefPtr<PacketSequenceStateGeneration> generation)
       : trace_packet_data{std::move(pv), std::move(generation)} {}
@@ -75,8 +90,16 @@
   double counter_value = 0;
   std::array<double, kMaxNumExtraCounters> extra_counter_values = {};
 };
+static_assert(sizeof(TracePacketData) % 8 == 0);
 
-}  // namespace trace_processor
-}  // namespace perfetto
+struct alignas(8) LegacyV8CpuProfileEvent {
+  uint64_t session_id;
+  uint32_t pid;
+  uint32_t tid;
+  uint32_t callsite_id;
+};
+static_assert(sizeof(LegacyV8CpuProfileEvent) % 8 == 0);
+
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PARSER_TYPES_H_
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index f497278..a667b75 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -23,6 +23,7 @@
 JsonTraceParser::~JsonTraceParser() = default;
 FuchsiaRecordParser::~FuchsiaRecordParser() = default;
 PerfRecordParser::~PerfRecordParser() = default;
+InstrumentsRowParser::~InstrumentsRowParser() = default;
 AndroidLogEventParser::~AndroidLogEventParser() = default;
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index 87fd8ff..cf7c84a 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -17,13 +17,16 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
 
-#include <stdint.h>
+#include <cstdint>
 #include <string>
 
 namespace perfetto::trace_processor {
 namespace perf_importer {
 struct Record;
 }
+namespace instruments_importer {
+struct Row;
+}
 
 struct AndroidLogEvent;
 class PacketSequenceStateGeneration;
@@ -34,6 +37,7 @@
 struct InlineSchedWaking;
 struct TracePacketData;
 struct TrackEventData;
+struct LegacyV8CpuProfileEvent;
 
 class ProtoTraceParser {
  public:
@@ -44,6 +48,7 @@
   virtual void ParseFtraceEvent(uint32_t, int64_t, TracePacketData) = 0;
   virtual void ParseInlineSchedSwitch(uint32_t, int64_t, InlineSchedSwitch) = 0;
   virtual void ParseInlineSchedWaking(uint32_t, int64_t, InlineSchedWaking) = 0;
+  virtual void ParseLegacyV8ProfileEvent(int64_t, LegacyV8CpuProfileEvent) = 0;
 };
 
 class JsonTraceParser {
@@ -65,6 +70,12 @@
   virtual void ParsePerfRecord(int64_t, perf_importer::Record) = 0;
 };
 
+class InstrumentsRowParser {
+ public:
+  virtual ~InstrumentsRowParser();
+  virtual void ParseInstrumentsRow(int64_t, instruments_importer::Row) = 0;
+};
+
 class AndroidLogEventParser {
  public:
   virtual ~AndroidLogEventParser();
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.cc b/src/trace_processor/importers/common/virtual_memory_mapping.cc
index 0485243..562049e 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.cc
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.cc
@@ -23,6 +23,7 @@
 #include <string>
 #include <utility>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/address_range.h"
 #include "src/trace_processor/importers/common/jit_cache.h"
@@ -119,5 +120,40 @@
   return {frame_id, true};
 }
 
+DummyMemoryMapping::~DummyMemoryMapping() = default;
+
+DummyMemoryMapping::DummyMemoryMapping(TraceProcessorContext* context,
+                                       CreateMappingParams params)
+    : VirtualMemoryMapping(context, std::move(params)) {}
+
+FrameId DummyMemoryMapping::InternDummyFrame(base::StringView function_name,
+                                             base::StringView source_file) {
+  DummyFrameKey key{context()->storage->InternString(function_name),
+                    context()->storage->InternString(source_file)};
+
+  if (FrameId* id = interned_dummy_frames_.Find(key); id) {
+    return *id;
+  }
+
+  uint32_t symbol_set_id = context()->storage->symbol_table().row_count();
+
+  tables::SymbolTable::Id symbol_id =
+      context()
+          ->storage->mutable_symbol_table()
+          ->Insert({symbol_set_id, key.function_name_id, key.source_file_id})
+          .id;
+
+  PERFETTO_CHECK(symbol_set_id == symbol_id.value);
+
+  const FrameId frame_id =
+      context()
+          ->storage->mutable_stack_profile_frame_table()
+          ->Insert({key.function_name_id, mapping_id(), 0, symbol_set_id})
+          .id;
+  interned_dummy_frames_.Insert(key, frame_id);
+
+  return frame_id;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/virtual_memory_mapping.h b/src/trace_processor/importers/common/virtual_memory_mapping.h
index 498a9ef..1676437 100644
--- a/src/trace_processor/importers/common/virtual_memory_mapping.h
+++ b/src/trace_processor/importers/common/virtual_memory_mapping.h
@@ -86,6 +86,8 @@
   VirtualMemoryMapping(TraceProcessorContext* context,
                        CreateMappingParams params);
 
+  TraceProcessorContext* context() const { return context_; }
+
  private:
   friend class MappingTracker;
 
@@ -149,6 +151,42 @@
   const UniquePid upid_;
 };
 
+// Dummy mapping to be able to create frames when we have no real pc addresses
+// or real mappings.
+class DummyMemoryMapping : public VirtualMemoryMapping {
+ public:
+  ~DummyMemoryMapping() override;
+
+  // Interns a frame based solely on function name and source file. This is
+  // useful for profilers that do not emit an address nor a mapping.
+  FrameId InternDummyFrame(base::StringView function_name,
+                           base::StringView source_file);
+
+ private:
+  friend class MappingTracker;
+  DummyMemoryMapping(TraceProcessorContext* context,
+                     CreateMappingParams params);
+
+  struct DummyFrameKey {
+    StringId function_name_id;
+    StringId source_file_id;
+
+    bool operator==(const DummyFrameKey& o) const {
+      return function_name_id == o.function_name_id &&
+             source_file_id == o.source_file_id;
+    }
+
+    struct Hasher {
+      size_t operator()(const DummyFrameKey& k) const {
+        return static_cast<size_t>(base::Hasher::Combine(
+            k.function_name_id.raw_id(), k.source_file_id.raw_id()));
+      }
+    };
+  };
+  base::FlatHashMap<DummyFrameKey, FrameId, DummyFrameKey::Hasher>
+      interned_dummy_frames_;
+};
+
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/importers/ftrace/BUILD.gn b/src/trace_processor/importers/ftrace/BUILD.gn
index 19354a7..1cc2b0d 100644
--- a/src/trace_processor/importers/ftrace/BUILD.gn
+++ b/src/trace_processor/importers/ftrace/BUILD.gn
@@ -47,6 +47,8 @@
     "iostat_tracker.h",
     "mali_gpu_event_tracker.cc",
     "mali_gpu_event_tracker.h",
+    "pixel_mm_kswapd_event_tracker.cc",
+    "pixel_mm_kswapd_event_tracker.h",
     "pkvm_hyp_cpu_tracker.cc",
     "pkvm_hyp_cpu_tracker.h",
     "rss_stat_tracker.cc",
diff --git a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
index 4e346e1..9e2adb2 100644
--- a/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_descriptors.cc
@@ -24,7 +24,7 @@
 namespace trace_processor {
 namespace {
 
-std::array<FtraceMessageDescriptor, 538> descriptors{{
+std::array<FtraceMessageDescriptor, 540> descriptors{{
     {nullptr, 0, {}},
     {nullptr, 0, {}},
     {nullptr, 0, {}},
@@ -5353,26 +5353,32 @@
     },
     {
         "sched_switch_with_ctrs",
-        17,
+        23,
         {
             {},
             {"old_pid", ProtoSchemaType::kInt32},
             {"new_pid", ProtoSchemaType::kInt32},
-            {"cctr", ProtoSchemaType::kUint32},
-            {"ctr0", ProtoSchemaType::kUint32},
-            {"ctr1", ProtoSchemaType::kUint32},
-            {"ctr2", ProtoSchemaType::kUint32},
-            {"ctr3", ProtoSchemaType::kUint32},
+            {"cctr", ProtoSchemaType::kUint64},
+            {"ctr0", ProtoSchemaType::kUint64},
+            {"ctr1", ProtoSchemaType::kUint64},
+            {"ctr2", ProtoSchemaType::kUint64},
+            {"ctr3", ProtoSchemaType::kUint64},
             {"lctr0", ProtoSchemaType::kUint32},
             {"lctr1", ProtoSchemaType::kUint32},
-            {"ctr4", ProtoSchemaType::kUint32},
-            {"ctr5", ProtoSchemaType::kUint32},
+            {"ctr4", ProtoSchemaType::kUint64},
+            {"ctr5", ProtoSchemaType::kUint64},
             {"prev_comm", ProtoSchemaType::kString},
             {"prev_pid", ProtoSchemaType::kInt32},
             {"cyc", ProtoSchemaType::kUint32},
             {"inst", ProtoSchemaType::kUint32},
             {"stallbm", ProtoSchemaType::kUint32},
             {"l3dm", ProtoSchemaType::kUint32},
+            {"next_pid", ProtoSchemaType::kInt32},
+            {"next_comm", ProtoSchemaType::kString},
+            {"prev_state", ProtoSchemaType::kInt64},
+            {"amu0", ProtoSchemaType::kUint64},
+            {"amu1", ProtoSchemaType::kUint64},
+            {"amu2", ProtoSchemaType::kUint64},
         },
     },
     {
@@ -5950,6 +5956,23 @@
             {"active", ProtoSchemaType::kUint64},
         },
     },
+    {
+        "pixel_mm_kswapd_wake",
+        1,
+        {
+            {},
+            {"whatever", ProtoSchemaType::kInt32},
+        },
+    },
+    {
+        "pixel_mm_kswapd_done",
+        2,
+        {
+            {},
+            {"delta_nr_scanned", ProtoSchemaType::kUint64},
+            {"delta_nr_reclaimed", ProtoSchemaType::kUint64},
+        },
+    },
 }};
 
 }  // namespace
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.cc b/src/trace_processor/importers/ftrace/ftrace_parser.cc
index 735b8de..73498c2 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.cc
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.cc
@@ -320,6 +320,7 @@
       pkvm_hyp_cpu_tracker_(context),
       gpu_work_period_tracker_(context),
       thermal_tracker_(context),
+      pixel_mm_kswapd_event_tracker_(context),
       sched_wakeup_name_id_(context->storage->InternString("sched_wakeup")),
       sched_waking_name_id_(context->storage->InternString("sched_waking")),
       cpu_id_(context->storage->InternString("cpu")),
@@ -1297,6 +1298,14 @@
         ParseBclIrq(ts, fld_bytes);
         break;
       }
+      case FtraceEvent::kPixelMmKswapdWakeFieldNumber: {
+        pixel_mm_kswapd_event_tracker_.ParsePixelMmKswapdWake(ts, pid);
+        break;
+      }
+      case FtraceEvent::kPixelMmKswapdDoneFieldNumber: {
+        pixel_mm_kswapd_event_tracker_.ParsePixelMmKswapdDone(ts, pid, fld_bytes);
+        break;
+      }
       default:
         break;
     }
diff --git a/src/trace_processor/importers/ftrace/ftrace_parser.h b/src/trace_processor/importers/ftrace/ftrace_parser.h
index f2c50ac..a9a7e5e 100644
--- a/src/trace_processor/importers/ftrace/ftrace_parser.h
+++ b/src/trace_processor/importers/ftrace/ftrace_parser.h
@@ -37,6 +37,7 @@
 #include "src/trace_processor/importers/ftrace/rss_stat_tracker.h"
 #include "src/trace_processor/importers/ftrace/thermal_tracker.h"
 #include "src/trace_processor/importers/ftrace/virtio_gpu_tracker.h"
+#include "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto {
@@ -317,6 +318,7 @@
   PkvmHypervisorCpuTracker pkvm_hyp_cpu_tracker_;
   GpuWorkPeriodTracker gpu_work_period_tracker_;
   ThermalTracker thermal_tracker_;
+  PixelMmKswapdEventTracker pixel_mm_kswapd_event_tracker_;
 
   const StringId sched_wakeup_name_id_;
   const StringId sched_waking_name_id_;
diff --git a/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc
new file mode 100644
index 0000000..a43e764
--- /dev/null
+++ b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.cc
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h"
+
+#include "perfetto/ext/base/string_utils.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
+#include "protos/perfetto/trace/ftrace/pixel_mm.pbzero.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/slice_tracker.h"
+#include "src/trace_processor/importers/common/track_tracker.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+PixelMmKswapdEventTracker::PixelMmKswapdEventTracker(
+    TraceProcessorContext* context)
+    : context_(context),
+      kswapd_efficiency_name_(
+          context->storage->InternString("kswapd_efficiency")),
+      efficiency_pct_name_(context->storage->InternString("efficiency %")),
+      pages_scanned_name_(context->storage->InternString("pages scanned")),
+      pages_reclaimed_name_(context->storage->InternString("pages reclaimed")) {
+}
+
+void PixelMmKswapdEventTracker::ParsePixelMmKswapdWake(int64_t timestamp,
+                                                       uint32_t pid) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  TrackId details_track = context_->track_tracker->InternThreadTrack(utid);
+
+  context_->slice_tracker->Begin(timestamp, details_track, kNullStringId,
+                                 kswapd_efficiency_name_);
+}
+
+void PixelMmKswapdEventTracker::ParsePixelMmKswapdDone(
+    int64_t timestamp,
+    uint32_t pid,
+    protozero::ConstBytes blob) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(pid);
+  TrackId details_track = context_->track_tracker->InternThreadTrack(utid);
+
+  protos::pbzero::PixelMmKswapdDoneFtraceEvent::Decoder decoder(blob.data,
+                                                                blob.size);
+
+  context_->slice_tracker->End(
+      timestamp, details_track, kNullStringId, kswapd_efficiency_name_,
+      [this, &decoder](ArgsTracker::BoundInserter* inserter) {
+        if (decoder.has_delta_nr_scanned()) {
+          inserter->AddArg(
+              pages_scanned_name_,
+              Variadic::UnsignedInteger(decoder.delta_nr_scanned()));
+        }
+        if (decoder.has_delta_nr_reclaimed()) {
+          inserter->AddArg(
+              pages_reclaimed_name_,
+              Variadic::UnsignedInteger(decoder.delta_nr_reclaimed()));
+        }
+
+        if (decoder.has_delta_nr_reclaimed() &&
+            decoder.has_delta_nr_scanned()) {
+          double efficiency =
+              static_cast<double>(decoder.delta_nr_reclaimed()) * 100 /
+              static_cast<double>(decoder.delta_nr_scanned());
+
+          inserter->AddArg(efficiency_pct_name_,
+                           Variadic::UnsignedInteger(
+                               static_cast<uint64_t>(std::round(efficiency))));
+        }
+      });
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h
new file mode 100644
index 0000000..dfdd6a0
--- /dev/null
+++ b/src/trace_processor/importers/ftrace/pixel_mm_kswapd_event_tracker.h
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
+
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/util/descriptors.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+class PixelMmKswapdEventTracker {
+ public:
+  explicit PixelMmKswapdEventTracker(TraceProcessorContext*);
+
+  void ParsePixelMmKswapdWake(int64_t timestamp, uint32_t pid);
+  void ParsePixelMmKswapdDone(int64_t timestamp,
+                              uint32_t pid,
+                              protozero::ConstBytes);
+
+ private:
+  TraceProcessorContext* context_;
+  const StringId kswapd_efficiency_name_;
+  const StringId efficiency_pct_name_;
+  const StringId pages_scanned_name_;
+  const StringId pages_reclaimed_name_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_FTRACE_PIXEL_MM_KSWAPD_EVENT_TRACKER_H_
diff --git a/src/trace_processor/importers/fuchsia/BUILD.gn b/src/trace_processor/importers/fuchsia/BUILD.gn
index 47d430b..8aa0826 100644
--- a/src/trace_processor/importers/fuchsia/BUILD.gn
+++ b/src/trace_processor/importers/fuchsia/BUILD.gn
@@ -39,7 +39,9 @@
     "../../../../gn:default_deps",
     "../../sorter",
     "../../storage",
+    "../../tables",
     "../../types",
+    "../../util:trace_type",
     "../common",
     "../proto:minimal",
   ]
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
index f15a725..fb669a9 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
+++ b/src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h
@@ -17,14 +17,22 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
 
+#include <cstdint>
+#include <memory>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "perfetto/base/status.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
-#include "src/trace_processor/importers/fuchsia/fuchsia_trace_utils.h"
+#include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/types/task_state.h"
+#include "src/trace_processor/tables/sched_tables_py.h"
+#include "src/trace_processor/util/trace_type.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -37,7 +45,7 @@
   ~FuchsiaTraceTokenizer() override;
 
   // ChunkedTraceReader implementation
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   base::Status NotifyEndOfFile() override;
 
  private:
@@ -130,7 +138,6 @@
   std::unordered_map<uint64_t, Thread> threads_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_FUCHSIA_FUCHSIA_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/instruments/BUILD.gn b/src/trace_processor/importers/instruments/BUILD.gn
new file mode 100644
index 0000000..3ca66d7
--- /dev/null
+++ b/src/trace_processor/importers/instruments/BUILD.gn
@@ -0,0 +1,48 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/test.gni")
+
+source_set("row") {
+  sources = [ "row.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+    "../../util:build_id",
+  ]
+}
+
+source_set("instruments") {
+  sources = [
+    "instruments_xml_tokenizer.cc",
+    "instruments_xml_tokenizer.h",
+    "row_data_tracker.cc",
+    "row_data_tracker.h",
+    "row_parser.cc",
+    "row_parser.h",
+  ]
+  public_deps = [ ":row" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../../../gn:expat",
+    "../../../../include/perfetto/ext/base:base",
+    "../../../../include/perfetto/public",
+    "../../../../include/perfetto/trace_processor:trace_processor",
+    "../../../../protos/perfetto/trace:zero",
+    "../../sorter",
+    "../../storage",
+    "../../types",
+    "../common:common",
+  ]
+}
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
new file mode 100644
index 0000000..51cee76
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.cc
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
+
+#include <map>
+
+#include <expat.h>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/public/fnv1a.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+namespace {
+
+std::string MakeTrimmed(const char* chars, int len) {
+  while (len > 0 && std::isspace(*chars)) {
+    chars++;
+    len--;
+  }
+  while (len > 0 && std::isspace(chars[len - 1])) {
+    len--;
+  }
+  return std::string(chars, static_cast<size_t>(len));
+}
+
+}  // namespace
+
+// The Instruments XML tokenizer reads instruments traces exported with:
+//
+//   xctrace export --input /path/to/profile.trace --xpath
+//     '//trace-toc/run/data/table[@schema="os-signpost and
+//        @category="PointsOfInterest"] |
+//      //trace-toc/run/data/table[@schema="time-sample"]'
+//
+// This exports two tables:
+//   1. Points of interest signposts
+//   2. Time samples
+//
+// The first is used for clock synchronization -- perfetto emits signpost events
+// during tracing which allow synchronization of the xctrace clock (relative to
+// start of profiling) with the perfetto boottime clock. The second contains
+// the samples themselves.
+//
+// The expected format of the rows in the clock sync table is:
+//
+//     <row>
+//       <event-time>1234</event-time>
+//       <subsystem>dev.perfetto.clock_sync</subsystem>
+//       <os-log-metadata>
+//         <uint64>5678</uint64>
+//       </os-log-metadata>
+//     </row>
+//
+// There may be other rows with other data (from other subsystems), and
+// additional data in the row (such as thread data and other metadata) -- this
+// can be safely ignored.
+//
+// The expected format of the rows in the time sample table is:
+//
+//     <row>
+//       <sample-time>1234</sample-time>
+//       <thread fmt="Thread name">
+//         <tid>1</tid>
+//         <process fmt="Process name">
+//           <pid>1<pid>
+//         </process>
+//       </thread>
+//       <core>0</core>
+//       <backtrace>
+//         <frame addr="0x120001234">
+//           <binary
+//             name="MyBinary" UUID="01234567-89ABC-CDEF-0123-456789ABCDEF"
+//             load-addr="0x120000000" path="/path/to/MyBinary.app/MyBinary" />
+//         </frame>
+//         ... more frames ...
+//     </row>
+//
+// Here we do not expect other rows with other data -- every row should have a
+// backtrace, and we use the presence of a backtrace to distinguish time samples
+// and clock sync eventst. However, there can be additional data in the row
+// (such as other metadata) -- this can be safely ignored.
+//
+// In addition, the XML format annotates elements with ids, to later reuse the
+// same data by id without needing to repeat its contents. For example, you
+// might have thread data for a sample:
+//
+//     <thread id="11" fmt="My Thread"><tid id="12">10</tid>...</thread>
+//
+// and subsequent samples on that thread will simply have
+//
+//     <thread ref="11" />
+//
+// This means that most elements have to have their pertinent data cached by id,
+// including any data store in child elements (which themselves also have to
+// be cached by id, like the <tid> in the example above).
+//
+// This importer reads the XML data using a streaming XML parser, which means
+// it has to maintain some parsing state (such as the current stack of tags, or
+// the current element for which we are reading data).
+class InstrumentsXmlTokenizer::Impl {
+ public:
+  explicit Impl(TraceProcessorContext* context)
+      : context_(context), data_(RowDataTracker::GetOrCreate(context_)) {
+    parser_ = XML_ParserCreate(nullptr);
+    XML_SetElementHandler(parser_, ElementStart, ElementEnd);
+    XML_SetCharacterDataHandler(parser_, CharacterData);
+    XML_SetUserData(parser_, this);
+
+    const char* subsystem = "dev.perfetto.instruments_clock";
+    clock_ = static_cast<ClockTracker::ClockId>(
+        PerfettoFnv1a(subsystem, strlen(subsystem)) | 0x80000000);
+
+    // Use the above clock if we can, in case there is no other trace and
+    // no clock sync events.
+    context_->clock_tracker->SetTraceTimeClock(clock_);
+  }
+  ~Impl() { XML_ParserFree(parser_); }
+
+  base::Status Parse(TraceBlobView view) {
+    if (!XML_Parse(parser_, reinterpret_cast<const char*>(view.data()),
+                   static_cast<int>(view.length()), false)) {
+      return base::ErrStatus("XML parse error at line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+  base::Status End() {
+    if (!XML_Parse(parser_, nullptr, 0, true)) {
+      return base::ErrStatus("XML parse error at end, line %lu: %s\n",
+                             XML_GetCurrentLineNumber(parser_),
+                             XML_ErrorString(XML_GetErrorCode(parser_)));
+    }
+    return base::OkStatus();
+  }
+
+ private:
+  static void ElementStart(void* data, const char* el, const char** attr) {
+    reinterpret_cast<Impl*>(data)->ElementStart(el, attr);
+  }
+  static void ElementEnd(void* data, const char* el) {
+    reinterpret_cast<Impl*>(data)->ElementEnd(el);
+  }
+  static void CharacterData(void* data, const char* chars, int len) {
+    reinterpret_cast<Impl*>(data)->CharacterData(chars, len);
+  }
+
+  void ElementStart(const char* el, const char** attrs) {
+    tag_stack_.emplace_back(el);
+    std::string_view tag_name = tag_stack_.back();
+
+    if (tag_name == "row") {
+      current_row_ = Row{};
+    } else if (tag_name == "thread") {
+      MaybeCachedRef<ThreadId> thread_lookup =
+          GetOrInsertByRef(attrs, thread_ref_to_thread_);
+      if (thread_lookup.is_new) {
+        auto new_thread = data_.NewThread();
+        thread_lookup.ref = new_thread.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_thread.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_thread_ = new_thread.id;
+      }
+      current_row_.thread = thread_lookup.ref;
+    } else if (tag_name == "process") {
+      MaybeCachedRef<ProcessId> process_lookup =
+          GetOrInsertByRef(attrs, process_ref_to_process_);
+      if (process_lookup.is_new) {
+        // Can only be processing a new process when processing a new thread.
+        PERFETTO_DCHECK(current_new_thread_ != kNullId);
+        auto new_process = data_.NewProcess();
+        process_lookup.ref = new_process.id;
+
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "fmt") {
+            new_process.ptr->fmt = InternString(attrs[i + 1]);
+          }
+        }
+
+        current_new_process_ = new_process.id;
+      }
+      if (current_new_thread_) {
+        data_.GetThread(current_new_thread_)->process = process_lookup.ref;
+      }
+    } else if (tag_name == "core") {
+      MaybeCachedRef<uint32_t> core_id_lookup =
+          GetOrInsertByRef(attrs, core_ref_to_core_);
+      if (core_id_lookup.is_new) {
+        current_new_core_id_ = &core_id_lookup.ref;
+      } else {
+        current_row_.core_id = core_id_lookup.ref;
+      }
+    } else if (tag_name == "sample-time" || tag_name == "event-time") {
+      // Share time lookup logic between sample times and event times, including
+      // updating the current row's sample time for both.
+      MaybeCachedRef<int64_t> time_lookup =
+          GetOrInsertByRef(attrs, sample_time_ref_to_time_);
+      if (time_lookup.is_new) {
+        current_time_ref_ = &time_lookup.ref;
+      } else {
+        current_row_.timestamp_ = time_lookup.ref;
+      }
+    } else if (tag_name == "subsystem") {
+      MaybeCachedRef<std::string> subsystem_lookup =
+          GetOrInsertByRef(attrs, subsystem_ref_to_subsystem_);
+      current_subsystem_ref_ = &subsystem_lookup.ref;
+    } else if (tag_name == "uint64") {
+      // The only uint64 we care about is the one for the clock sync, which is
+      // expected to contain exactly one uint64 value -- we'll
+      // map all uint64 to a single value and check against the subsystem
+      // when the row is closed.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      if (uint64_lookup.is_new) {
+        current_uint64_ref_ = &uint64_lookup.ref;
+      } else {
+        if (current_os_log_metadata_uint64_ref_) {
+          // Update the os-log-metadata's uint64 value with this uint64 value.
+          *current_os_log_metadata_uint64_ref_ = uint64_lookup.ref;
+        }
+      }
+    } else if (tag_name == "os-log-metadata") {
+      // The only os-log-metadata we care about is the one with the single
+      // uint64 clock sync value, so also map this to uint64 values with its own
+      // id.
+      MaybeCachedRef<uint64_t> uint64_lookup =
+          GetOrInsertByRef(attrs, os_log_metadata_or_uint64_ref_to_uint64_);
+      current_os_log_metadata_uint64_ref_ = &uint64_lookup.ref;
+    } else if (tag_name == "backtrace") {
+      MaybeCachedRef<BacktraceId> backtrace_lookup =
+          GetOrInsertByRef(attrs, backtrace_ref_to_backtrace_);
+      if (backtrace_lookup.is_new) {
+        backtrace_lookup.ref = data_.NewBacktrace().id;
+      }
+      current_row_.backtrace = backtrace_lookup.ref;
+    } else if (tag_name == "frame") {
+      MaybeCachedRef<BacktraceFrameId> frame_lookup =
+          GetOrInsertByRef(attrs, frame_ref_to_frame_);
+      if (frame_lookup.is_new) {
+        IdPtr<Frame> new_frame = data_.NewFrame();
+        frame_lookup.ref = new_frame.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "addr") {
+            new_frame.ptr->addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        current_new_frame_ = new_frame.id;
+      }
+      data_.GetBacktrace(current_row_.backtrace)
+          ->frames.push_back(frame_lookup.ref);
+    } else if (tag_name == "binary") {
+      // Can only be processing a binary when processing a new frame.
+      PERFETTO_DCHECK(current_new_frame_ != kNullId);
+
+      MaybeCachedRef<BinaryId> binary_lookup =
+          GetOrInsertByRef(attrs, binary_ref_to_binary_);
+      if (binary_lookup.is_new) {
+        auto new_binary = data_.NewBinary();
+        binary_lookup.ref = new_binary.id;
+        for (int i = 2; attrs[i]; i += 2) {
+          std::string key(attrs[i]);
+          if (key == "path") {
+            new_binary.ptr->path = std::string(attrs[i + 1]);
+          } else if (key == "UUID") {
+            new_binary.ptr->uuid =
+                BuildId::FromHex(base::StringView(attrs[i + 1]));
+          } else if (key == "load-addr") {
+            new_binary.ptr->load_addr = strtoll(attrs[i + 1], nullptr, 16);
+          }
+        }
+        new_binary.ptr->max_addr = new_binary.ptr->load_addr;
+      }
+      PERFETTO_DCHECK(data_.GetFrame(current_new_frame_)->binary == kNullId);
+      data_.GetFrame(current_new_frame_)->binary = binary_lookup.ref;
+    }
+  }
+
+  void ElementEnd(const char* el) {
+    PERFETTO_DCHECK(el == tag_stack_.back());
+    std::string tag_name = std::move(tag_stack_.back());
+    tag_stack_.pop_back();
+
+    if (tag_name == "row") {
+      if (current_row_.backtrace) {
+        // Rows with backtraces are assumed to be time samples.
+        base::StatusOr<int64_t> trace_ts =
+            ToTraceTimestamp(current_row_.timestamp_);
+        if (!trace_ts.ok()) {
+          PERFETTO_DLOG("Skipping timestamp %ld, no clock snapshot yet",
+                        current_row_.timestamp_);
+        } else {
+          context_->sorter->PushInstrumentsRow(*trace_ts,
+                                               std::move(current_row_));
+        }
+      } else if (current_subsystem_ref_ != nullptr) {
+        // Rows without backtraces are assumed to be signpost events -- filter
+        // these for `dev.perfetto.clock_sync` events.
+        if (*current_subsystem_ref_ == "dev.perfetto.clock_sync") {
+          PERFETTO_DCHECK(current_os_log_metadata_uint64_ref_ != nullptr);
+          uint64_t clock_sync_timestamp = *current_os_log_metadata_uint64_ref_;
+          if (latest_clock_sync_timestamp_ > clock_sync_timestamp) {
+            PERFETTO_DLOG("Skipping timestamp %ld, non-monotonic sync deteced",
+                          current_row_.timestamp_);
+          } else {
+            latest_clock_sync_timestamp_ = clock_sync_timestamp;
+            auto status = context_->clock_tracker->AddSnapshot(
+                {{clock_, current_row_.timestamp_},
+                 {protos::pbzero::ClockSnapshot::Clock::BOOTTIME,
+                  static_cast<int64_t>(latest_clock_sync_timestamp_)}});
+            if (!status.ok()) {
+              PERFETTO_FATAL("Error adding clock snapshot: %s",
+                             status.status().c_message());
+            }
+          }
+        }
+        current_subsystem_ref_ = nullptr;
+        current_os_log_metadata_uint64_ref_ = nullptr;
+        current_uint64_ref_ = nullptr;
+      }
+    } else if (current_new_frame_ != kNullId && tag_name == "frame") {
+      Frame* frame = data_.GetFrame(current_new_frame_);
+      if (frame->binary) {
+        Binary* binary = data_.GetBinary(frame->binary);
+        // We don't know what the binary's mapping end is, but we know that the
+        // current frame is inside of it, so use that.
+        PERFETTO_DCHECK(frame->addr > binary->load_addr);
+        if (frame->addr > binary->max_addr) {
+          binary->max_addr = frame->addr;
+        }
+      }
+      current_new_frame_ = kNullId;
+    } else if (current_new_thread_ != kNullId && tag_name == "thread") {
+      current_new_thread_ = kNullId;
+    } else if (current_new_process_ != kNullId && tag_name == "process") {
+      current_new_process_ = kNullId;
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      current_new_core_id_ = nullptr;
+    }
+  }
+
+  void CharacterData(const char* chars, int len) {
+    std::string_view tag_name = tag_stack_.back();
+    if (current_time_ref_ != nullptr &&
+        (tag_name == "sample-time" || tag_name == "event-time")) {
+      std::string s = MakeTrimmed(chars, len);
+      current_row_.timestamp_ = *current_time_ref_ = stoll(s);
+      current_time_ref_ = nullptr;
+    } else if (current_new_thread_ != kNullId && tag_name == "tid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetThread(current_new_thread_)->tid = stoi(s);
+    } else if (current_new_process_ != kNullId && tag_name == "pid") {
+      std::string s = MakeTrimmed(chars, len);
+      data_.GetProcess(current_new_process_)->pid = stoi(s);
+    } else if (current_new_core_id_ != nullptr && tag_name == "core") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_new_core_id_ = static_cast<uint32_t>(stoul(s));
+    } else if (current_subsystem_ref_ != nullptr && tag_name == "subsystem") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_subsystem_ref_ = s;
+    } else if (current_uint64_ref_ != nullptr &&
+               current_os_log_metadata_uint64_ref_ != nullptr &&
+               tag_name == "uint64") {
+      std::string s = MakeTrimmed(chars, len);
+      *current_os_log_metadata_uint64_ref_ = *current_uint64_ref_ = stoull(s);
+    }
+  }
+
+  base::StatusOr<int64_t> ToTraceTimestamp(int64_t time) {
+    base::StatusOr<int64_t> trace_ts =
+        context_->clock_tracker->ToTraceTime(clock_, time);
+
+    if (PERFETTO_LIKELY(trace_ts.ok())) {
+      latest_timestamp_ = std::max(latest_timestamp_, *trace_ts);
+    }
+
+    return trace_ts;
+  }
+
+  StringId InternString(base::StringView string_view) {
+    return context_->storage->InternString(string_view);
+  }
+  StringId InternString(const char* string) {
+    return InternString(base::StringView(string));
+  }
+  StringId InternString(const char* data, size_t len) {
+    return InternString(base::StringView(data, len));
+  }
+
+  template <typename Value>
+  struct MaybeCachedRef {
+    Value& ref;
+    bool is_new;
+  };
+  // Implement the element caching mechanism. Either insert an element by its
+  // id attribute into the given map, or look up the element in the cache by its
+  // ref attribute. The returned value is a reference into the map, to allow
+  // in-place modification.
+  template <typename Value>
+  MaybeCachedRef<Value> GetOrInsertByRef(const char** attrs,
+                                         std::map<unsigned long, Value>& map) {
+    PERFETTO_DCHECK(attrs[0] != nullptr);
+    PERFETTO_DCHECK(attrs[1] != nullptr);
+    const char* key = attrs[0];
+    // The id or ref attribute has to be the first attribute on the element.
+    PERFETTO_DCHECK(strcmp(key, "ref") == 0 || strcmp(key, "id") == 0);
+    unsigned long id = strtoul(attrs[1], nullptr, 10);
+    // If the first attribute key is `id`, then this is a new entry in the
+    // cache -- otherwise, for lookup by ref, it should already exist.
+    bool is_new = strcmp(key, "id") == 0;
+    PERFETTO_DCHECK(is_new == (map.find(id) == map.end()));
+    return {map[id], is_new};
+  }
+
+  TraceProcessorContext* context_;
+  RowDataTracker& data_;
+
+  XML_Parser parser_;
+  std::vector<std::string> tag_stack_;
+  int64_t latest_timestamp_;
+
+  // These maps store the cached element data. These currently have to be
+  // std::map, because they require pointer stability under insertion,
+  // as the various `current_foo_` pointers below point directly into the map
+  // data.
+  //
+  // TODO(leszeks): Relax this pointer stability requirement, and use
+  // base::FlatHashMap.
+  // TODO(leszeks): Consider merging these into a single map from ID to
+  // a variant (or similar).
+  std::map<unsigned long, ThreadId> thread_ref_to_thread_;
+  std::map<unsigned long, ProcessId> process_ref_to_process_;
+  std::map<unsigned long, uint32_t> core_ref_to_core_;
+  std::map<unsigned long, int64_t> sample_time_ref_to_time_;
+  std::map<unsigned long, BinaryId> binary_ref_to_binary_;
+  std::map<unsigned long, BacktraceFrameId> frame_ref_to_frame_;
+  std::map<unsigned long, BacktraceId> backtrace_ref_to_backtrace_;
+  std::map<unsigned long, std::string> subsystem_ref_to_subsystem_;
+  std::map<unsigned long, uint64_t> os_log_metadata_or_uint64_ref_to_uint64_;
+
+  Row current_row_;
+  int64_t* current_time_ref_ = nullptr;
+  ThreadId current_new_thread_ = kNullId;
+  ProcessId current_new_process_ = kNullId;
+  uint32_t* current_new_core_id_ = nullptr;
+  BacktraceFrameId current_new_frame_ = kNullId;
+
+  ClockTracker::ClockId clock_;
+  std::string* current_subsystem_ref_ = nullptr;
+  uint64_t* current_os_log_metadata_uint64_ref_ = nullptr;
+  uint64_t* current_uint64_ref_ = nullptr;
+  uint64_t latest_clock_sync_timestamp_ = 0;
+};
+
+InstrumentsXmlTokenizer::InstrumentsXmlTokenizer(TraceProcessorContext* context)
+    : impl_(new Impl(context)) {}
+InstrumentsXmlTokenizer::~InstrumentsXmlTokenizer() {
+  delete impl_;
+}
+
+base::Status InstrumentsXmlTokenizer::Parse(TraceBlobView view) {
+  return impl_->Parse(std::move(view));
+}
+
+[[nodiscard]] base::Status InstrumentsXmlTokenizer::NotifyEndOfFile() {
+  return impl_->End();
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
new file mode 100644
index 0000000..be044dd
--- /dev/null
+++ b/src/trace_processor/importers/instruments/instruments_xml_tokenizer.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class InstrumentsXmlTokenizer : public ChunkedTraceReader {
+ public:
+  explicit InstrumentsXmlTokenizer(TraceProcessorContext*);
+  ~InstrumentsXmlTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+
+  [[nodiscard]] base::Status NotifyEndOfFile() override;
+
+ private:
+  class Impl;
+
+  class Impl* impl_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_INSTRUMENTS_XML_TOKENIZER_H_
diff --git a/src/trace_processor/importers/instruments/row.h b/src/trace_processor/importers/instruments/row.h
new file mode 100644
index 0000000..531a36d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/util/build_id.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+// TODO(leszeks): Would be nice if these were strong type aliases, to be
+// type safe.
+using ThreadId = uint32_t;
+using ProcessId = uint32_t;
+using BacktraceId = uint32_t;
+using BacktraceFrameId = uint32_t;
+using BinaryId = uint32_t;
+
+constexpr uint32_t kNullId = 0u;
+
+struct Binary {
+  std::string path;
+  BuildId uuid = BuildId::FromRaw(std::string(""));
+  long long load_addr = 0;
+  long long max_addr = 0;
+};
+
+struct Frame {
+  long long addr = 0;
+  BinaryId binary = kNullId;
+};
+
+struct Process {
+  int pid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+};
+
+struct Thread {
+  int tid = 0;
+  StringPool::Id fmt = StringPool::Id::Null();
+  ProcessId process = kNullId;
+};
+
+struct Backtrace {
+  std::vector<BacktraceFrameId> frames;
+};
+
+struct alignas(8) Row {
+  int64_t timestamp_;
+  uint32_t core_id;
+  ThreadId thread = kNullId;
+  BacktraceId backtrace = kNullId;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_H_
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.cc b/src/trace_processor/importers/instruments/row_data_tracker.cc
new file mode 100644
index 0000000..bd27629
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.cc
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+#include "perfetto/base/status.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowDataTracker::RowDataTracker() {}
+RowDataTracker::~RowDataTracker() = default;
+
+IdPtr<Thread> RowDataTracker::NewThread() {
+  ThreadId id = static_cast<ThreadId>(threads_.size());
+  Thread* ptr = &threads_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Thread* RowDataTracker::GetThread(ThreadId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &threads_[id - 1];
+}
+
+IdPtr<Process> RowDataTracker::NewProcess() {
+  ProcessId id = static_cast<ProcessId>(processes_.size());
+  Process* ptr = &processes_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Process* RowDataTracker::GetProcess(ProcessId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &processes_[id - 1];
+}
+
+IdPtr<Frame> RowDataTracker::NewFrame() {
+  BacktraceFrameId id = static_cast<BacktraceFrameId>(frames_.size());
+  Frame* ptr = &frames_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Frame* RowDataTracker::GetFrame(BacktraceFrameId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &frames_[id - 1];
+}
+
+IdPtr<Backtrace> RowDataTracker::NewBacktrace() {
+  BacktraceId id = static_cast<BacktraceId>(backtraces_.size());
+  Backtrace* ptr = &backtraces_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Backtrace* RowDataTracker::GetBacktrace(BacktraceId id) {
+  PERFETTO_DCHECK(id != kNullId);
+  return &backtraces_[id - 1];
+}
+
+IdPtr<Binary> RowDataTracker::NewBinary() {
+  BinaryId id = static_cast<BinaryId>(binaries_.size());
+  Binary* ptr = &binaries_.emplace_back();
+  // Always add 1 to ids, so that they're non-zero.
+  return {id + 1, ptr};
+}
+Binary* RowDataTracker::GetBinary(BinaryId id) {
+  // Frames are allowed to have null binaries.
+  if (id == kNullId)
+    return nullptr;
+  return &binaries_[id - 1];
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_data_tracker.h b/src/trace_processor/importers/instruments/row_data_tracker.h
new file mode 100644
index 0000000..247326e
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_data_tracker.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
+
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/types/destructible.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+template <typename T>
+struct IdPtr {
+  uint32_t id;
+  T* ptr;
+};
+
+// Keeps track of row data.
+class RowDataTracker : public Destructible {
+ public:
+  static RowDataTracker& GetOrCreate(TraceProcessorContext* context) {
+    if (!context->instruments_row_data_tracker) {
+      context->instruments_row_data_tracker.reset(new RowDataTracker());
+    }
+    return static_cast<RowDataTracker&>(*context->instruments_row_data_tracker);
+  }
+  ~RowDataTracker() override;
+
+  IdPtr<Thread> NewThread();
+  Thread* GetThread(ThreadId id);
+
+  IdPtr<Process> NewProcess();
+  Process* GetProcess(ProcessId id);
+
+  IdPtr<Frame> NewFrame();
+  Frame* GetFrame(BacktraceFrameId id);
+
+  IdPtr<Backtrace> NewBacktrace();
+  Backtrace* GetBacktrace(BacktraceId id);
+
+  IdPtr<Binary> NewBinary();
+  Binary* GetBinary(BinaryId id);
+
+ private:
+  explicit RowDataTracker();
+
+  std::vector<Thread> threads_;
+  std::vector<Process> processes_;
+  std::vector<Frame> frames_;
+  std::vector<Backtrace> backtraces_;
+  std::vector<Binary> binaries_;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_DATA_TRACKER_H_
diff --git a/src/trace_processor/importers/instruments/row_parser.cc b/src/trace_processor/importers/instruments/row_parser.cc
new file mode 100644
index 0000000..184d1f1
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.cc
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/instruments/row_parser.h"
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/importers/instruments/row_data_tracker.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+RowParser::RowParser(TraceProcessorContext* context)
+    : context_(context), data_(RowDataTracker::GetOrCreate(context)) {}
+
+void RowParser::ParseInstrumentsRow(int64_t ts, instruments_importer::Row row) {
+  if (!row.backtrace) {
+    return;
+  }
+
+  Thread* thread = data_.GetThread(row.thread);
+  Process* process = data_.GetProcess(thread->process);
+  uint32_t tid = static_cast<uint32_t>(thread->tid);
+  uint32_t pid = static_cast<uint32_t>(process->pid);
+
+  UniqueTid utid = context_->process_tracker->UpdateThread(tid, pid);
+  UniquePid upid = context_->process_tracker->GetOrCreateProcess(pid);
+
+  // TODO(leszeks): Avoid setting thread/process name if we've already seen this
+  // Thread* / Process*.
+  context_->process_tracker->UpdateThreadNameByUtid(utid, thread->fmt,
+                                                    ThreadNamePriority::kOther);
+  context_->process_tracker->SetProcessNameIfUnset(upid, process->fmt);
+
+  Backtrace* backtrace = data_.GetBacktrace(row.backtrace);
+  std::optional<CallsiteId> parent;
+  uint32_t depth = 0;
+  base::FlatHashMap<FrameId, CallsiteTreeNode>* frame_to_callsite = &top_frames;
+  auto leaf = backtrace->frames.rend() - 1;
+  for (auto it = backtrace->frames.rbegin(); it != backtrace->frames.rend();
+       ++it) {
+    Frame* frame = data_.GetFrame(*it);
+    Binary* binary = data_.GetBinary(frame->binary);
+
+    uint64_t rel_pc = static_cast<uint64_t>(frame->addr);
+    if (frame->binary) {
+      rel_pc -= static_cast<uint64_t>(binary->load_addr);
+    }
+
+    // For non-leaf functions, the pc will be after the end of the call. Adjust
+    // it to be within the call instruction.
+    if (rel_pc != 0 && it != leaf) {
+      --rel_pc;
+    }
+
+    auto frame_inserted = frame_to_frame_id_.Insert(*it, FrameId{0});
+    if (frame_inserted.second) {
+      auto mapping_inserted = binary_to_mapping_.Insert(frame->binary, nullptr);
+      if (mapping_inserted.second) {
+        if (binary == nullptr) {
+          *mapping_inserted.first = GetDummyMapping(upid);
+        } else {
+          BuildId build_id = binary->uuid;
+          *mapping_inserted.first =
+              &context_->mapping_tracker->CreateUserMemoryMapping(
+                  upid, {AddressRange(static_cast<uint64_t>(binary->load_addr),
+                                      static_cast<uint64_t>(binary->max_addr)),
+                         0, 0, 0, binary->path, build_id});
+        }
+      }
+      VirtualMemoryMapping* mapping = *mapping_inserted.first;
+
+      // Intern the frame with no function name -- the symbolizer will annotate
+      // frames later.
+      *frame_inserted.first =
+          mapping->InternFrame(rel_pc, base::StringView(""));
+    }
+    FrameId frame_id = *frame_inserted.first;
+
+    // Lookup the frame id in the current callsite prefix tree node.
+    auto callsite_node_inserted =
+        frame_to_callsite->Insert(frame_id, CallsiteTreeNode{});
+    if (callsite_node_inserted.second) {
+      callsite_node_inserted.first->callsite_id =
+          context_->storage->mutable_stack_profile_callsite_table()
+              ->Insert({depth, parent, frame_id})
+              .id;
+    }
+    parent = callsite_node_inserted.first->callsite_id;
+    frame_to_callsite = &callsite_node_inserted.first->next_frames;
+    depth++;
+  }
+
+  context_->storage->mutable_instruments_sample_table()->Insert(
+      {ts, utid, row.core_id, parent});
+}
+
+DummyMemoryMapping* RowParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping =
+      &context_->mapping_tracker->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
+}  // namespace perfetto::trace_processor::instruments_importer
diff --git a/src/trace_processor/importers/instruments/row_parser.h b/src/trace_processor/importers/instruments/row_parser.h
new file mode 100644
index 0000000..a5e915d
--- /dev/null
+++ b/src/trace_processor/importers/instruments/row_parser.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
+#include "src/trace_processor/importers/instruments/row.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::instruments_importer {
+
+class RowDataTracker;
+
+class RowParser : public InstrumentsRowParser {
+ public:
+  explicit RowParser(TraceProcessorContext*);
+  ~RowParser() override = default;
+
+  void ParseInstrumentsRow(int64_t, instruments_importer::Row) override;
+
+ private:
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  TraceProcessorContext* context_;
+  RowDataTracker& data_;
+
+  // Cache FrameId and binary mappings by instruments frame and binary
+  // pointers, respectively. These are already de-duplicated in the
+  // instruments XML parsing.
+  base::FlatHashMap<BacktraceFrameId, FrameId> frame_to_frame_id_;
+  base::FlatHashMap<BinaryId, VirtualMemoryMapping*> binary_to_mapping_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
+
+  // Cache callsites by FrameId in a prefix tree, where children in the
+  // prefix tree are child frames at the callsite. This should be more
+  // efficient than looking up frame+parent pairs in a hashmap.
+  // TODO(leszeks): Verify that this is more efficient and share the code
+  // with other importers.
+  struct CallsiteTreeNode {
+    CallsiteId callsite_id{0};
+    base::FlatHashMap<FrameId, CallsiteTreeNode> next_frames{};
+  };
+  base::FlatHashMap<FrameId, CallsiteTreeNode> top_frames;
+};
+
+}  // namespace perfetto::trace_processor::instruments_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_INSTRUMENTS_ROW_PARSER_H_
diff --git a/src/trace_processor/importers/json/json_trace_parser_impl.h b/src/trace_processor/importers/json/json_trace_parser_impl.h
index 4c0e269..c675f23 100644
--- a/src/trace_processor/importers/json/json_trace_parser_impl.h
+++ b/src/trace_processor/importers/json/json_trace_parser_impl.h
@@ -17,21 +17,19 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
 
-#include <stdint.h>
-
-#include <memory>
-#include <tuple>
+#include <cstdint>
+#include <string>
 
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/importers/systrace/systrace_line_parser.h"
+#include "src/trace_processor/storage/trace_storage.h"
 
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -43,8 +41,8 @@
   ~JsonTraceParserImpl() override;
 
   // TraceParser implementation.
-  void ParseJsonPacket(int64_t timestamp, std::string string_value) override;
-  void ParseSystraceLine(int64_t timestamp, SystraceLine line) override;
+  void ParseJsonPacket(int64_t, std::string) override;
+  void ParseSystraceLine(int64_t, SystraceLine) override;
 
  private:
   TraceProcessorContext* const context_;
@@ -53,7 +51,6 @@
   void MaybeAddFlow(TrackId track_id, const Json::Value& event);
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.cc b/src/trace_processor/importers/json/json_trace_tokenizer.cc
index 8345a9e..5a35f3c 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.cc
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.cc
@@ -16,23 +16,32 @@
 
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
 #include <memory>
+#include <optional>
+#include <string>
+#include <utility>
 
-#include "perfetto/base/build_config.h"
+#include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/string_utils.h"
-
+#include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/json/json_utils.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/importers/systrace/systrace_line.h"
+#include "src/trace_processor/sorter/trace_sorter.h"  // IWYU pragma: keep
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/util/status_macros.h"
 
-namespace perfetto {
-namespace trace_processor {
-
+namespace perfetto::trace_processor {
 namespace {
 
+std::string FormatErrorContext(const char* s, const char* e) {
+  return {s, static_cast<size_t>(e - s)};
+}
+
 base::Status AppendUnescapedCharacter(char c,
                                       bool is_escaping,
                                       std::string* key) {
@@ -64,7 +73,7 @@
         key->append("\\u");
         break;
       default:
-        return base::ErrStatus("Illegal character in JSON");
+        return base::ErrStatus("Illegal character in JSON %c", c);
     }
   } else if (c != '\\') {
     key->push_back(c);
@@ -213,7 +222,7 @@
         return ReadDictRes::kEndOfTrace;
       if (--braces > 0)
         continue;
-      size_t len = static_cast<size_t>((s + 1) - dict_begin);
+      auto len = static_cast<size_t>((s + 1) - dict_begin);
       *value = base::StringView(dict_begin, len);
       *next = s + 1;
       return ReadDictRes::kFoundDict;
@@ -324,11 +333,12 @@
         state = kInsideDict;
         continue;
       }
-      return base::ErrStatus("Unexpected character before JSON dict");
+      return base::ErrStatus("Unexpected character before JSON dict: '%c'", *s);
     }
 
-    if (state == kAfterDict)
-      return base::ErrStatus("Unexpected character after JSON dict");
+    if (state == kAfterDict) {
+      return base::ErrStatus("Unexpected character after JSON dict: '%c'", *s);
+    }
 
     PERFETTO_DCHECK(state == kInsideDict);
     PERFETTO_DCHECK(s < end);
@@ -347,18 +357,22 @@
     if (res == ReadKeyRes::kFatalError) {
       return base::ErrStatus(
           "Failure parsing JSON: encountered fatal error while parsing key for "
-          "value");
+          "value: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     if (res == ReadKeyRes::kNeedsMoreData) {
-      return base::ErrStatus("Failure parsing JSON: partial JSON dictionary");
+      return base::ErrStatus(
+          "Failure parsing JSON: partial JSON dictionary: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     PERFETTO_DCHECK(res == ReadKeyRes::kFoundKey);
 
     if (*s == '[') {
       return base::ErrStatus(
-          "Failure parsing JSON: unsupported JSON dictionary with array");
+          "Failure parsing JSON: unsupported JSON dictionary with array: '%s'",
+          FormatErrorContext(s, end).c_str());
     }
 
     std::string value_str;
@@ -369,14 +383,17 @@
           dict_res == ReadDictRes::kEndOfArray ||
           dict_res == ReadDictRes::kEndOfTrace) {
         return base::ErrStatus(
-            "Failure parsing JSON: unable to parse dictionary");
+            "Failure parsing JSON: unable to parse dictionary: '%s'",
+            FormatErrorContext(s, end).c_str());
       }
       value_str = dict_str.ToStdString();
     } else if (*s == '"') {
       auto str_res = ReadOneJsonString(s, end, &value_str, &s);
       if (str_res == ReadStringRes::kNeedsMoreData ||
           str_res == ReadStringRes::kFatalError) {
-        return base::ErrStatus("Failure parsing JSON: unable to parse string");
+        return base::ErrStatus(
+            "Failure parsing JSON: unable to parse string: '%s",
+            FormatErrorContext(s, end).c_str());
       }
     } else {
       const char* value_start = s;
@@ -396,8 +413,10 @@
     }
   }
 
-  if (state != kAfterDict)
-    return base::ErrStatus("Failure parsing JSON: malformed dictionary");
+  if (state != kAfterDict) {
+    return base::ErrStatus("Failure parsing JSON: malformed dictionary: '%s'",
+                           FormatErrorContext(start, end).c_str());
+  }
 
   *value = std::nullopt;
   return base::OkStatus();
@@ -673,5 +692,4 @@
              : base::ErrStatus("JSON trace file is incomplete");
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/json/json_trace_tokenizer.h b/src/trace_processor/importers/json/json_trace_tokenizer.h
index 8672def..00af4ca 100644
--- a/src/trace_processor/importers/json/json_trace_tokenizer.h
+++ b/src/trace_processor/importers/json/json_trace_tokenizer.h
@@ -17,18 +17,21 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
 
-#include <stdint.h>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
 
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/systrace/systrace_line_tokenizer.h"
-#include "src/trace_processor/storage/trace_storage.h"
 
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TraceProcessorContext;
 
@@ -162,7 +165,6 @@
   std::vector<char> buffer_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_JSON_JSON_TRACE_TOKENIZER_H_
diff --git a/src/trace_processor/importers/json/json_utils.cc b/src/trace_processor/importers/json/json_utils.cc
index d2e1c18..d4157a1 100644
--- a/src/trace_processor/importers/json/json_utils.cc
+++ b/src/trace_processor/importers/json/json_utils.cc
@@ -29,14 +29,6 @@
 namespace trace_processor {
 namespace json {
 
-bool IsJsonSupported() {
-#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
-  return true;
-#else
-  return false;
-#endif
-}
-
 std::optional<int64_t> CoerceToTs(const Json::Value& value) {
   PERFETTO_DCHECK(IsJsonSupported());
 
diff --git a/src/trace_processor/importers/json/json_utils.h b/src/trace_processor/importers/json/json_utils.h
index b9d0762..83b79ed 100644
--- a/src/trace_processor/importers/json/json_utils.h
+++ b/src/trace_processor/importers/json/json_utils.h
@@ -38,7 +38,13 @@
 
 // Returns whether JSON related functioanlity is supported with the current
 // build flags.
-bool IsJsonSupported();
+constexpr bool IsJsonSupported() {
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  return true;
+#else
+  return false;
+#endif
+}
 
 std::optional<int64_t> CoerceToTs(const Json::Value& value);
 std::optional<int64_t> CoerceToTs(const std::string& value);
diff --git a/src/trace_processor/importers/json/json_utils_unittest.cc b/src/trace_processor/importers/json/json_utils_unittest.cc
index 5b17cc3..3d9be27 100644
--- a/src/trace_processor/importers/json/json_utils_unittest.cc
+++ b/src/trace_processor/importers/json/json_utils_unittest.cc
@@ -16,13 +16,12 @@
 
 #include "src/trace_processor/importers/json/json_utils.h"
 
+#include <json/config.h>
 #include <json/value.h>
 
 #include "test/gtest_and_gmock.h"
 
-namespace perfetto {
-namespace trace_processor {
-namespace json {
+namespace perfetto::trace_processor::json {
 namespace {
 
 TEST(JsonTraceUtilsTest, CoerceToUint32) {
@@ -78,6 +77,4 @@
 }
 
 }  // namespace
-}  // namespace json
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::json
diff --git a/src/trace_processor/importers/perf/record_parser.cc b/src/trace_processor/importers/perf/record_parser.cc
index a63a5bd..0b0d169 100644
--- a/src/trace_processor/importers/perf/record_parser.cc
+++ b/src/trace_processor/importers/perf/record_parser.cc
@@ -225,7 +225,7 @@
       context_->storage->IncrementStats(stats::perf_dummy_mapping_used);
       // Simpleperf will not create mappings for anonymous executable mappings
       // which are used by JITted code (e.g. V8 JavaScript).
-      mapping = mapping_tracker_->GetDummyMapping();
+      mapping = GetDummyMapping(upid);
     }
 
     const FrameId frame_id =
@@ -346,4 +346,14 @@
   return base::OkStatus();
 }
 
+DummyMemoryMapping* RecordParser::GetDummyMapping(UniquePid upid) {
+  if (auto it = dummy_mappings_.Find(upid); it) {
+    return *it;
+  }
+
+  DummyMemoryMapping* mapping = &mapping_tracker_->CreateDummyMapping("");
+  dummy_mappings_.Insert(upid, mapping);
+  return mapping;
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/record_parser.h b/src/trace_processor/importers/perf/record_parser.h
index 76926d1..6230845 100644
--- a/src/trace_processor/importers/perf/record_parser.h
+++ b/src/trace_processor/importers/perf/record_parser.h
@@ -22,6 +22,7 @@
 #include <optional>
 
 #include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/perf/mmap_record.h"
 #include "src/trace_processor/importers/perf/record.h"
@@ -31,6 +32,7 @@
 namespace perfetto {
 namespace trace_processor {
 
+class DummyMemoryMapping;
 class MappingTracker;
 class TraceProcessorContext;
 
@@ -66,8 +68,11 @@
 
   UniquePid GetUpid(const CommonMmapRecordFields& fields) const;
 
-  TraceProcessorContext* const context_ = nullptr;
+  DummyMemoryMapping* GetDummyMapping(UniquePid upid);
+
+  TraceProcessorContext* const context_;
   MappingTracker* const mapping_tracker_;
+  base::FlatHashMap<UniquePid, DummyMemoryMapping*> dummy_mappings_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/proto/android_probes_parser.cc b/src/trace_processor/importers/proto/android_probes_parser.cc
index a6cd9b6..e059404 100644
--- a/src/trace_processor/importers/proto/android_probes_parser.cc
+++ b/src/trace_processor/importers/proto/android_probes_parser.cc
@@ -438,7 +438,8 @@
           context_->async_track_set_tracker->Scoped(track_set_id, ts, 0);
       context_->slice_tracker->Scoped(ts, track_id, kNullStringId, state_id, 0);
     } else if (name.StartsWith("debug.tracing.battery_stats.") ||
-               name == "debug.tracing.mcc" || name == "debug.tracing.mnc") {
+               name == "debug.tracing.mcc" || name == "debug.tracing.mnc" ||
+               name == "debug.tracing.desktop_mode_visible_tasks") {
       StringId name_id = context_->storage->InternString(
           name.substr(strlen("debug.tracing.")));
       std::optional<int32_t> state =
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
index 8a11d5d..393bd79 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -16,21 +16,24 @@
 
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 
-#include <string.h>
-
-#include <cinttypes>
+#include <cstdint>
+#include <cstring>
 #include <string>
+#include <utility>
+#include <vector>
 
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/metatrace_events.h"
 #include "perfetto/ext/base/string_utils.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/string_writer.h"
-#include "perfetto/ext/base/uuid.h"
-
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -39,8 +42,8 @@
 #include "src/trace_processor/importers/etw/etw_module.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/track_event_module.h"
-#include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
@@ -49,8 +52,7 @@
 #include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 ProtoTraceParserImpl::ProtoTraceParserImpl(TraceProcessorContext* context)
     : context_(context),
@@ -109,8 +111,8 @@
 }
 
 void ProtoTraceParserImpl::ParseEtwEvent(uint32_t cpu,
-                                     int64_t ts,
-                                     TracePacketData data) {
+                                         int64_t ts,
+                                         TracePacketData data) {
   PERFETTO_DCHECK(context_->etw_module);
   context_->etw_module->ParseEtwEventData(cpu, ts, data);
 
@@ -121,8 +123,8 @@
 }
 
 void ProtoTraceParserImpl::ParseFtraceEvent(uint32_t cpu,
-                                        int64_t ts,
-                                        TracePacketData data) {
+                                            int64_t ts,
+                                            TracePacketData data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseFtraceEventData(cpu, ts, data);
 
@@ -133,8 +135,8 @@
 }
 
 void ProtoTraceParserImpl::ParseInlineSchedSwitch(uint32_t cpu,
-                                              int64_t ts,
-                                              InlineSchedSwitch data) {
+                                                  int64_t ts,
+                                                  InlineSchedSwitch data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseInlineSchedSwitch(cpu, ts, data);
 
@@ -145,8 +147,8 @@
 }
 
 void ProtoTraceParserImpl::ParseInlineSchedWaking(uint32_t cpu,
-                                              int64_t ts,
-                                              InlineSchedWaking data) {
+                                                  int64_t ts,
+                                                  InlineSchedWaking data) {
   PERFETTO_DCHECK(context_->ftrace_module);
   context_->ftrace_module->ParseInlineSchedWaking(cpu, ts, data);
 
@@ -156,6 +158,18 @@
   context_->args_tracker->Flush();
 }
 
+void ProtoTraceParserImpl::ParseLegacyV8ProfileEvent(
+    int64_t ts,
+    LegacyV8CpuProfileEvent event) {
+  base::Status status = context_->legacy_v8_cpu_profile_tracker->AddSample(
+      ts, event.session_id, event.pid, event.tid, event.callsite_id);
+  if (!status.ok()) {
+    context_->storage->IncrementStats(
+        stats::legacy_v8_cpu_profile_invalid_sample);
+  }
+  context_->args_tracker->Flush();
+}
+
 void ProtoTraceParserImpl::ParseChromeEvents(int64_t ts, ConstBytes blob) {
   TraceStorage* storage = context_->storage.get();
   protos::pbzero::ChromeEventBundle::Decoder bundle(blob.data, blob.size);
@@ -373,5 +387,4 @@
   return *maybe_id;
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.h b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
index 2c4dc07..f6b2304 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
@@ -17,11 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_PARSER_IMPL_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_PARSER_IMPL_H_
 
-#include <stdint.h>
+#include <cstdint>
 
-#include <array>
-#include <memory>
-
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
@@ -29,11 +27,9 @@
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class TracePacket_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -65,12 +61,14 @@
                               int64_t /*ts*/,
                               InlineSchedWaking data) override;
 
-  void ParseChromeEvents(int64_t ts, ConstBytes);
-  void ParseMetatraceEvent(int64_t ts, ConstBytes);
+  void ParseLegacyV8ProfileEvent(int64_t ts, LegacyV8CpuProfileEvent) override;
 
  private:
   StringId GetMetatraceInternedString(uint64_t iid);
 
+  void ParseChromeEvents(int64_t ts, ConstBytes);
+  void ParseMetatraceEvent(int64_t ts, ConstBytes);
+
   TraceProcessorContext* context_;
 
   const StringId metatrace_id_;
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 54bfdba..1e72da0 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -16,33 +16,37 @@
 
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 
+#include <algorithm>
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <map>
 #include <numeric>
 #include <optional>
-#include <string>
+#include <tuple>
+#include <utility>
 #include <vector>
 
-#include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
 #include "perfetto/ext/base/string_view.h"
-#include "perfetto/ext/base/utils.h"
+#include "perfetto/protozero/field.h"
 #include "perfetto/protozero/proto_decoder.h"
-#include "perfetto/protozero/proto_utils.h"
 #include "perfetto/public/compiler.h"
-#include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
-#include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/metadata_tables_py.h"
+#include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/descriptors.h"
-#include "src/trace_processor/util/gzip_utils.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/common/trace_stats.pbzero.h"
@@ -50,13 +54,11 @@
 #include "protos/perfetto/trace/clock_snapshot.pbzero.h"
 #include "protos/perfetto/trace/extension_descriptor.pbzero.h"
 #include "protos/perfetto/trace/perfetto/tracing_service_event.pbzero.h"
-#include "protos/perfetto/trace/profiling/profile_common.pbzero.h"
 #include "protos/perfetto/trace/remote_clock_sync.pbzero.h"
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 ProtoTraceReader::ProtoTraceReader(TraceProcessorContext* ctx)
     : context_(ctx),
@@ -65,13 +67,13 @@
           ctx->storage->InternString("invalid_incremental_state")) {}
 ProtoTraceReader::~ProtoTraceReader() = default;
 
-util::Status ProtoTraceReader::Parse(TraceBlobView blob) {
+base::Status ProtoTraceReader::Parse(TraceBlobView blob) {
   return tokenizer_.Tokenize(std::move(blob), [this](TraceBlobView packet) {
     return ParsePacket(std::move(packet));
   });
 }
 
-util::Status ProtoTraceReader::ParseExtensionDescriptor(ConstBytes descriptor) {
+base::Status ProtoTraceReader::ParseExtensionDescriptor(ConstBytes descriptor) {
   protos::pbzero::ExtensionDescriptor::Decoder decoder(descriptor.data,
                                                        descriptor.size);
 
@@ -82,10 +84,10 @@
       /*merge_existing_messages=*/true);
 }
 
-util::Status ProtoTraceReader::ParsePacket(TraceBlobView packet) {
+base::Status ProtoTraceReader::ParsePacket(TraceBlobView packet) {
   protos::pbzero::TracePacket::Decoder decoder(packet.data(), packet.length());
   if (PERFETTO_UNLIKELY(decoder.bytes_left())) {
-    return util::ErrStatus(
+    return base::ErrStatus(
         "Failed to parse proto packet fully; the trace is probably corrupt.");
   }
 
@@ -172,7 +174,7 @@
   if (decoder.sequence_flags() &
       protos::pbzero::TracePacket::SEQ_NEEDS_INCREMENTAL_STATE) {
     if (!seq_id) {
-      return util::ErrStatus(
+      return base::ErrStatus(
           "TracePacket specified SEQ_NEEDS_INCREMENTAL_STATE but the "
           "TraceWriter's sequence_id is zero (the service is "
           "probably too old)");
@@ -183,12 +185,12 @@
         // Account for the skipped packet for trace proto content analysis,
         // with a special annotation.
         PacketAnalyzer::SampleAnnotation annotation;
-        annotation.push_back(
-            {skipped_packet_key_id_, invalid_incremental_state_key_id_});
+        annotation.emplace_back(skipped_packet_key_id_,
+                                invalid_incremental_state_key_id_);
         PacketAnalyzer::Get(context_)->ProcessPacket(packet, annotation);
       }
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return util::OkStatus();
+      return base::OkStatus();
     }
   }
 
@@ -223,7 +225,7 @@
       ClockTracker::ClockId converted_clock_id = timestamp_clock_id;
       if (ClockTracker::IsSequenceClock(converted_clock_id)) {
         if (!seq_id) {
-          return util::ErrStatus(
+          return base::ErrStatus(
               "TracePacket specified a sequence-local clock id (%" PRIu32
               ") but the TraceWriter's sequence_id is zero (the service is "
               "probably too old)",
@@ -239,7 +241,7 @@
         // We don't return an error here as it will cause the trace to stop
         // parsing. Instead, we rely on the stat increment in ToTraceTime() to
         // inform the user about the error.
-        return util::OkStatus();
+        return base::OkStatus();
       }
       timestamp = trace_ts.value();
     }
@@ -280,7 +282,7 @@
   context_->sorter->PushTracePacket(timestamp, state->current_generation(),
                                     std::move(packet), context_->machine_id());
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 void ProtoTraceReader::ParseTraceConfig(protozero::ConstBytes blob) {
@@ -373,7 +375,7 @@
   }
 }
 
-util::Status ProtoTraceReader::ParseClockSnapshot(ConstBytes blob,
+base::Status ProtoTraceReader::ParseClockSnapshot(ConstBytes blob,
                                                   uint32_t seq_id) {
   std::vector<ClockTracker::ClockTimestamp> clock_timestamps;
   protos::pbzero::ClockSnapshot::Decoder evt(blob.data, blob.size);
@@ -386,7 +388,7 @@
     ClockTracker::ClockId clock_id = clk.clock_id();
     if (ClockTracker::IsSequenceClock(clk.clock_id())) {
       if (!seq_id) {
-        return util::ErrStatus(
+        return base::ErrStatus(
             "ClockSnapshot packet is specifying a sequence-scoped clock id "
             "(%" PRIu64 ") but the TracePacket sequence_id is zero",
             clock_id);
@@ -450,10 +452,10 @@
 
     context_->storage->mutable_clock_snapshot_table()->Insert(row);
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
-util::Status ProtoTraceReader::ParseRemoteClockSync(ConstBytes blob) {
+base::Status ProtoTraceReader::ParseRemoteClockSync(ConstBytes blob) {
   protos::pbzero::RemoteClockSync::Decoder evt(blob.data, blob.size);
 
   std::vector<SyncClockSnapshots> sync_clock_snapshots;
@@ -496,7 +498,7 @@
     context_->clock_tracker->SetClockOffset(it.key(), it.value());
   }
 
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
@@ -593,7 +595,7 @@
   }
 }
 
-util::Status ProtoTraceReader::ParseServiceEvent(int64_t ts, ConstBytes blob) {
+base::Status ProtoTraceReader::ParseServiceEvent(int64_t ts, ConstBytes blob) {
   protos::pbzero::TracingServiceEvent::Decoder tse(blob);
   if (tse.tracing_started()) {
     context_->metadata_tracker->SetMetadata(metadata::tracing_started_ns,
@@ -615,7 +617,7 @@
   if (tse.read_tracing_buffers_completed()) {
     context_->sorter->NotifyReadBufferEvent();
   }
-  return util::OkStatus();
+  return base::OkStatus();
 }
 
 void ProtoTraceReader::ParseTraceStats(ConstBytes blob) {
@@ -741,5 +743,4 @@
   return base::OkStatus();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index 45c0c9f..9864156 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -17,11 +17,13 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_READER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_TRACE_READER_H_
 
-#include <stdint.h>
-
-#include <tuple>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
 #include <utility>
+#include <vector>
 
+#include "perfetto/base/status.h"
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
@@ -35,12 +37,10 @@
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class TracePacket_Decoder;
 class TraceConfig_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -61,13 +61,13 @@
   ~ProtoTraceReader() override;
 
   // ChunkedTraceReader implementation.
-  util::Status Parse(TraceBlobView) override;
+  base::Status Parse(TraceBlobView) override;
   base::Status NotifyEndOfFile() override;
 
   using SyncClockSnapshots = base::FlatHashMap<
       int64_t,
       std::pair</*host ts*/ uint64_t, /*client ts*/ uint64_t>>;
-  base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
+  static base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
   CalculateClockOffsetsForTesting(
       std::vector<SyncClockSnapshots>& sync_clock_snapshots) {
     return CalculateClockOffsets(sync_clock_snapshots);
@@ -77,10 +77,10 @@
 
  private:
   using ConstBytes = protozero::ConstBytes;
-  util::Status ParsePacket(TraceBlobView);
-  util::Status ParseServiceEvent(int64_t ts, ConstBytes);
-  util::Status ParseClockSnapshot(ConstBytes blob, uint32_t seq_id);
-  util::Status ParseRemoteClockSync(ConstBytes blob);
+  base::Status ParsePacket(TraceBlobView);
+  base::Status ParseServiceEvent(int64_t ts, ConstBytes);
+  base::Status ParseClockSnapshot(ConstBytes blob, uint32_t seq_id);
+  base::Status ParseRemoteClockSync(ConstBytes blob);
   void HandleIncrementalStateCleared(
       const protos::pbzero::TracePacket_Decoder&);
   void HandleFirstPacketOnSequence(uint32_t packet_sequence_id);
@@ -89,10 +89,10 @@
                                 TraceBlobView trace_packet_defaults);
   void ParseInternedData(const protos::pbzero::TracePacket_Decoder&,
                          TraceBlobView interned_data);
-  void ParseTraceConfig(ConstBytes);
+  static void ParseTraceConfig(ConstBytes);
   void ParseTraceStats(ConstBytes);
 
-  base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
+  static base::FlatHashMap<int64_t /*Clock Id*/, int64_t /*Offset*/>
   CalculateClockOffsets(std::vector<SyncClockSnapshots>&);
 
   PacketSequenceStateBuilder* GetIncrementalStateForPacketSequence(
@@ -105,7 +105,7 @@
     }
     return builder;
   }
-  util::Status ParseExtensionDescriptor(ConstBytes descriptor);
+  base::Status ParseExtensionDescriptor(ConstBytes descriptor);
 
   TraceProcessorContext* context_;
 
diff --git a/src/trace_processor/importers/proto/track_event_module.cc b/src/trace_processor/importers/proto/track_event_module.cc
index aee720c..0ae00f2 100644
--- a/src/trace_processor/importers/proto/track_event_module.cc
+++ b/src/trace_processor/importers/proto/track_event_module.cc
@@ -15,20 +15,21 @@
  */
 #include "src/trace_processor/importers/proto/track_event_module.h"
 
-#include "perfetto/base/build_config.h"
+#include <cstdint>
+#include <utility>
+
 #include "perfetto/base/logging.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-#include "protos/perfetto/config/data_source_config.pbzero.h"
-#include "protos/perfetto/config/trace_config.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 using perfetto::protos::pbzero::TracePacket;
 
@@ -59,9 +60,8 @@
       return tokenizer_.TokenizeTrackDescriptorPacket(std::move(state), decoder,
                                                       packet_timestamp);
     case TracePacket::kTrackEventFieldNumber:
-      tokenizer_.TokenizeTrackEventPacket(std::move(state), decoder, packet,
-                                          packet_timestamp);
-      return ModuleResult::Handled();
+      return tokenizer_.TokenizeTrackEventPacket(std::move(state), decoder,
+                                                 packet, packet_timestamp);
     case TracePacket::kThreadDescriptorFieldNumber:
       // TODO(eseckler): Remove once Chrome has switched to TrackDescriptors.
       return tokenizer_.TokenizeThreadDescriptorPacket(std::move(state),
@@ -70,13 +70,6 @@
   return ModuleResult::Ignored();
 }
 
-void TrackEventModule::ParseTrackEventData(const TracePacket::Decoder& decoder,
-                                           int64_t ts,
-                                           const TrackEventData& data) {
-  parser_.ParseTrackEvent(ts, &data, decoder.track_event(),
-                          decoder.trusted_packet_sequence_id());
-}
-
 void TrackEventModule::ParseTracePacketData(const TracePacket::Decoder& decoder,
                                             int64_t ts,
                                             const TracePacketData&,
@@ -107,9 +100,15 @@
   track_event_tracker_->OnFirstPacketOnSequence(packet_sequence_id);
 }
 
+void TrackEventModule::ParseTrackEventData(const TracePacket::Decoder& decoder,
+                                           int64_t ts,
+                                           const TrackEventData& data) {
+  parser_.ParseTrackEvent(ts, &data, decoder.track_event(),
+                          decoder.trusted_packet_sequence_id());
+}
+
 void TrackEventModule::NotifyEndOfFile() {
   parser_.NotifyEndOfFile();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_module.h b/src/trace_processor/importers/proto/track_event_module.h
index 4a7ae69..756a1fc 100644
--- a/src/trace_processor/importers/proto/track_event_module.h
+++ b/src/trace_processor/importers/proto/track_event_module.h
@@ -17,7 +17,11 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
 
+#include <cstdint>
+#include <memory>
+
 #include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/track_event_parser.h"
@@ -25,8 +29,7 @@
 
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class TrackEventModule : public ProtoImporterModule {
  public:
@@ -41,6 +44,11 @@
       RefPtr<PacketSequenceStateGeneration> state,
       uint32_t field_id) override;
 
+  void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder& decoder,
+                            int64_t ts,
+                            const TracePacketData& data,
+                            uint32_t field_id) override;
+
   void OnIncrementalStateCleared(uint32_t) override;
 
   void OnFirstPacketOnSequence(uint32_t) override;
@@ -49,11 +57,6 @@
                            int64_t ts,
                            const TrackEventData& data);
 
-  void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder& decoder,
-                            int64_t ts,
-                            const TracePacketData& data,
-                            uint32_t field_id) override;
-
   void NotifyEndOfFile() override;
 
  private:
@@ -62,7 +65,6 @@
   TrackEventParser parser_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_MODULE_H_
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 6ca338e..360126c 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -31,7 +31,6 @@
 #include "perfetto/protozero/proto_decoder.h"
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
-#include "perfetto/trace_processor/status.h"
 #include "src/trace_processor/containers/null_term_string_view.h"
 #include "src/trace_processor/containers/string_pool.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
@@ -39,6 +38,8 @@
 #include "src/trace_processor/importers/common/cpu_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
+#include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
@@ -209,8 +210,8 @@
 
     if (context_->content_analyzer) {
       PacketAnalyzer::SampleAnnotation annotation;
-      annotation.push_back({parser_->event_category_key_id_, category_id_});
-      annotation.push_back({parser_->event_name_key_id_, name_id_});
+      annotation.emplace_back(parser_->event_category_key_id_, category_id_);
+      annotation.emplace_back(parser_->event_name_key_id_, name_id_);
       PacketAnalyzer::Get(context_)->ProcessPacket(
           event_data_->trace_packet_data.packet, annotation);
     }
@@ -1341,6 +1342,7 @@
   std::optional<UniqueTid> upid_;
   std::optional<int64_t> thread_timestamp_;
   std::optional<int64_t> thread_instruction_count_;
+
   // All events in legacy JSON require a thread ID, but for some types of
   // events (e.g. async events or process/global-scoped instants), we don't
   // store it in the slice/track model. To pass the utid through to the json
diff --git a/src/trace_processor/importers/proto/track_event_parser.h b/src/trace_processor/importers/proto/track_event_parser.h
index 6198dd5..7843a53 100644
--- a/src/trace_processor/importers/proto/track_event_parser.h
+++ b/src/trace_processor/importers/proto/track_event_parser.h
@@ -18,11 +18,11 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_PARSER_H_
 
 #include <array>
-#include <map>
+#include <cstdint>
+#include <optional>
+#include <vector>
 
-#include "perfetto/base/build_config.h"
 #include "perfetto/protozero/field.h"
-#include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
@@ -31,14 +31,11 @@
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/util/proto_to_args_parser.h"
 
-#include "protos/perfetto/trace/track_event/track_event.pbzero.h"
-
 namespace Json {
 class Value;
 }
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Field numbers to be added to args table automatically via reflection
 //
@@ -135,7 +132,6 @@
   ActiveChromeProcessesTracker active_chrome_processes_tracker_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_PARSER_H_
diff --git a/src/trace_processor/importers/proto/track_event_sequence_state.cc b/src/trace_processor/importers/proto/track_event_sequence_state.cc
index 99bc93e..ee138b5 100644
--- a/src/trace_processor/importers/proto/track_event_sequence_state.cc
+++ b/src/trace_processor/importers/proto/track_event_sequence_state.cc
@@ -18,8 +18,7 @@
 
 #include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 void TrackEventSequenceState::SetThreadDescriptor(
     const protos::pbzero::ThreadDescriptor::Decoder& decoder) {
@@ -33,5 +32,4 @@
   thread_instruction_count_ = decoder.reference_thread_instruction_count();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index 238e247..268f45f 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -16,34 +16,51 @@
 
 #include "src/trace_processor/importers/proto/track_event_tokenizer.h"
 
+#include <cinttypes>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <utility>
+
+#include "perfetto/base/compiler.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
+#include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/metadata.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
-#include "protos/perfetto/trace/track_event/chrome_process_descriptor.pbzero.h"
-#include "protos/perfetto/trace/track_event/chrome_thread_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/process_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/range_of_interest.pbzero.h"
 #include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/util/status_macros.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 namespace {
 using protos::pbzero::CounterDescriptor;
@@ -217,7 +234,7 @@
   state.SetThreadDescriptor(thread);
 }
 
-void TrackEventTokenizer::TokenizeTrackEventPacket(
+ModuleResult TrackEventTokenizer::TokenizeTrackEventPacket(
     RefPtr<PacketSequenceStateGeneration> state,
     const protos::pbzero::TracePacket::Decoder& packet,
     TraceBlobView* packet_blob,
@@ -225,12 +242,10 @@
   if (PERFETTO_UNLIKELY(!packet.has_trusted_packet_sequence_id())) {
     PERFETTO_ELOG("TrackEvent packet without trusted_packet_sequence_id");
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
 
-  auto field = packet.track_event();
-  protos::pbzero::TrackEvent::Decoder event(field.data, field.size);
-
+  protos::pbzero::TrackEvent::Decoder event(packet.track_event());
   protos::pbzero::TrackEventDefaults::Decoder* defaults =
       state->GetTrackEventDefaults();
 
@@ -246,7 +261,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     timestamp = state->IncrementAndGetTrackEventTimeNs(
         event.timestamp_delta_us() * 1000);
@@ -272,7 +287,16 @@
   } else {
     PERFETTO_ELOG("TrackEvent without valid timestamp");
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
+  }
+
+  // Handle legacy sample events which might have timestamps embedded inside.
+  if (PERFETTO_UNLIKELY(event.has_legacy_event())) {
+    protos::pbzero::TrackEvent::LegacyEvent::Decoder leg(event.legacy_event());
+    if (PERFETTO_UNLIKELY(leg.phase() == 'P')) {
+      RETURN_IF_ERROR(TokenizeLegacySampleEvent(
+          event, leg, *data.trace_packet_data.sequence_state));
+    }
   }
 
   if (event.has_thread_time_delta_us()) {
@@ -280,7 +304,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     data.thread_timestamp = state->IncrementAndGetTrackEventThreadTimeNs(
         event.thread_time_delta_us() * 1000);
@@ -294,7 +318,7 @@
     // packet loss.
     if (!state->track_event_timestamps_valid()) {
       context_->storage->IncrementStats(stats::tokenizer_skipped_packets);
-      return;
+      return ModuleResult::Handled();
     }
     data.thread_instruction_count =
         state->IncrementAndGetTrackEventThreadInstructionCount(
@@ -315,7 +339,7 @@
       PERFETTO_DLOG(
           "Ignoring TrackEvent with counter_value but without track_uuid");
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     if (!event.has_counter_value() && !event.has_double_counter_value()) {
@@ -325,7 +349,7 @@
           "track_uuid %" PRIu64,
           track_uuid);
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     std::optional<double> value;
@@ -343,7 +367,7 @@
       PERFETTO_DLOG("Ignoring TrackEvent with invalid track_uuid %" PRIu64,
                     track_uuid);
       context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-      return;
+      return ModuleResult::Handled();
     }
 
     data.counter_value = *value;
@@ -358,7 +382,7 @@
   if (!result.ok()) {
     PERFETTO_DLOG("%s", result.c_message());
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
   result = AddExtraCounterValues(
       data, index, packet.trusted_packet_sequence_id(),
@@ -368,11 +392,12 @@
   if (!result.ok()) {
     PERFETTO_DLOG("%s", result.c_message());
     context_->storage->IncrementStats(stats::track_event_tokenizer_errors);
-    return;
+    return ModuleResult::Handled();
   }
 
   context_->sorter->PushTrackEventPacket(timestamp, std::move(data),
                                          context_->machine_id());
+  return ModuleResult::Handled();
 }
 
 template <typename T>
@@ -394,19 +419,19 @@
   } else if (default_track_uuid_it) {
     track_uuid_it = default_track_uuid_it;
   } else {
-    return base::Status(
+    return base::ErrStatus(
         "Ignoring TrackEvent with extra_{double_,}counter_values but without "
         "extra_{double_,}counter_track_uuids");
   }
 
   for (; value_it; ++value_it, ++track_uuid_it, ++index) {
     if (!*track_uuid_it) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with more extra_{double_,}counter_values than "
           "extra_{double_,}counter_track_uuids");
     }
     if (index >= TrackEventData::kMaxNumExtraCounters) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with more extra_{double_,}counter_values than "
           "TrackEventData::kMaxNumExtraCounters");
     }
@@ -415,7 +440,7 @@
             *track_uuid_it, trusted_packet_sequence_id,
             static_cast<double>(*value_it));
     if (!abs_value) {
-      return base::Status(
+      return base::ErrStatus(
           "Ignoring TrackEvent with invalid extra counter track");
     }
     data.extra_counter_values[index] = *abs_value;
@@ -423,5 +448,76 @@
   return base::OkStatus();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+base::Status TrackEventTokenizer::TokenizeLegacySampleEvent(
+    const protos::pbzero::TrackEvent::Decoder& event,
+    const protos::pbzero::TrackEvent::LegacyEvent::Decoder& legacy,
+    PacketSequenceStateGeneration& state) {
+  // We are just trying to parse out the V8 profiling events into the cpu
+  // sampling tables: if we don't have JSON enabled, just don't do this.
+#if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
+  for (auto it = event.debug_annotations(); it; ++it) {
+    protos::pbzero::DebugAnnotation::Decoder da(*it);
+    auto* interned_name = state.LookupInternedMessage<
+        protos::pbzero::InternedData::kDebugAnnotationNamesFieldNumber,
+        protos::pbzero::DebugAnnotationName>(da.name_iid());
+    base::StringView name(interned_name->name());
+    if (name != "data" || !da.has_legacy_json_value()) {
+      continue;
+    }
+    auto opt_val = json::ParseJsonString(da.legacy_json_value());
+    if (!opt_val) {
+      continue;
+    }
+    const auto& val = *opt_val;
+    if (val.isMember("startTime")) {
+      ASSIGN_OR_RETURN(int64_t ts, context_->clock_tracker->ToTraceTime(
+                                       protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+                                       val["startTime"].asInt64() * 1000));
+      context_->legacy_v8_cpu_profile_tracker->SetStartTsForSessionAndPid(
+          legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), ts);
+      continue;
+    }
+    const auto& profile = val["cpuProfile"];
+    for (const auto& n : profile["nodes"]) {
+      uint32_t node_id = n["id"].asUInt();
+      std::optional<uint32_t> parent_node_id =
+          n.isMember("parent") ? std::make_optional(n["parent"].asUInt())
+                               : std::nullopt;
+      const auto& frame = n["callFrame"];
+      base::StringView url =
+          frame.isMember("url") ? frame["url"].asCString() : base::StringView();
+      base::StringView function_name = frame["functionName"].asCString();
+      base::Status status =
+          context_->legacy_v8_cpu_profile_tracker->AddCallsite(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()), node_id,
+              parent_node_id, url, function_name);
+      if (!status.ok()) {
+        context_->storage->IncrementStats(
+            stats::legacy_v8_cpu_profile_invalid_callsite);
+        continue;
+      }
+    }
+    const auto& samples = profile["samples"];
+    const auto& deltas = val["timeDeltas"];
+    if (samples.size() != deltas.size()) {
+      return base::ErrStatus(
+          "v8 legacy profile: samples and timestamps do not have same size");
+    }
+    for (uint32_t i = 0; i < samples.size(); ++i) {
+      ASSIGN_OR_RETURN(
+          int64_t ts,
+          context_->legacy_v8_cpu_profile_tracker->AddDeltaAndGetTs(
+              legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+              deltas[i].asInt64() * 1000));
+      context_->sorter->PushLegacyV8CpuProfileEvent(
+          ts, legacy.unscoped_id(), static_cast<uint32_t>(state.pid()),
+          static_cast<uint32_t>(state.tid()), samples[i].asUInt());
+    }
+  }
+#else
+  base::ignore_result(event, legacy, state);
+#endif
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.h b/src/trace_processor/importers/proto/track_event_tokenizer.h
index 6ce3347..0627562 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.h
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.h
@@ -17,23 +17,26 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TOKENIZER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TOKENIZER_H_
 
-#include <stdint.h>
+#include <cstddef>
+#include <cstdint>
 
+#include "perfetto/base/status.h"
 #include "perfetto/protozero/proto_decoder.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 namespace perfetto {
 
-namespace protos {
-namespace pbzero {
+namespace protos::pbzero {
 class ChromeThreadDescriptor_Decoder;
 class ProcessDescriptor_Decoder;
 class ThreadDescriptor_Decoder;
 class TracePacket_Decoder;
-}  // namespace pbzero
-}  // namespace protos
+class TrackEvent_Decoder;
+class TrackEvent_LegacyEvent_Decoder;
+}  // namespace protos::pbzero
 
 namespace trace_processor {
 
@@ -57,10 +60,11 @@
   ModuleResult TokenizeThreadDescriptorPacket(
       RefPtr<PacketSequenceStateGeneration> state,
       const protos::pbzero::TracePacket_Decoder&);
-  void TokenizeTrackEventPacket(RefPtr<PacketSequenceStateGeneration> state,
-                                const protos::pbzero::TracePacket_Decoder&,
-                                TraceBlobView* packet,
-                                int64_t packet_timestamp);
+  ModuleResult TokenizeTrackEventPacket(
+      RefPtr<PacketSequenceStateGeneration> state,
+      const protos::pbzero::TracePacket_Decoder&,
+      TraceBlobView* packet,
+      int64_t packet_timestamp);
 
  private:
   void TokenizeThreadDescriptor(
@@ -74,6 +78,10 @@
       protozero::RepeatedFieldIterator<T> value_it,
       protozero::RepeatedFieldIterator<uint64_t> packet_track_uuid_it,
       protozero::RepeatedFieldIterator<uint64_t> default_track_uuid_it);
+  base::Status TokenizeLegacySampleEvent(
+      const protos::pbzero::TrackEvent_Decoder&,
+      const protos::pbzero::TrackEvent_LegacyEvent_Decoder&,
+      PacketSequenceStateGeneration& state);
 
   TraceProcessorContext* context_;
   TrackEventTracker* track_event_tracker_;
diff --git a/src/trace_processor/importers/proto/track_event_tracker.h b/src/trace_processor/importers/proto/track_event_tracker.h
index 2eb7092..e32138d 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.h
+++ b/src/trace_processor/importers/proto/track_event_tracker.h
@@ -17,14 +17,21 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
 
+#include <cstdint>
+#include <map>
+#include <optional>
+#include <tuple>
 #include <unordered_set>
+#include <vector>
 
-#include "src/trace_processor/importers/common/args_tracker.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // Tracks and stores tracks based on track types, ids and scopes.
 class TrackEventTracker {
@@ -266,7 +273,6 @@
   TraceProcessorContext* const context_;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_TRACKER_H_
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.cc b/src/trace_processor/importers/proto/winscope/winscope_module.cc
index 9946a23..317374e 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.cc
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.cc
@@ -49,6 +49,23 @@
                                  kWinscopeDescriptor.size());
 }
 
+ModuleResult WinscopeModule::TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* /*packet*/,
+    int64_t /*packet_timestamp*/,
+    RefPtr<PacketSequenceStateGeneration> /*state*/,
+    uint32_t field_id) {
+
+  switch (field_id) {
+    case TracePacket::kProtologViewerConfigFieldNumber:
+      protolog_parser_.ParseProtoLogViewerConfig(
+          decoder.protolog_viewer_config());
+      return ModuleResult::Handled();
+  }
+
+  return ModuleResult::Ignored();
+}
+
 void WinscopeModule::ParseTracePacketData(const TracePacket::Decoder& decoder,
                                           int64_t timestamp,
                                           const TracePacketData& data,
@@ -73,10 +90,6 @@
       protolog_parser_.ParseProtoLogMessage(
           data.sequence_state.get(), decoder.protolog_message(), timestamp);
       return;
-    case TracePacket::kProtologViewerConfigFieldNumber:
-      protolog_parser_.ParseProtoLogViewerConfig(
-          decoder.protolog_viewer_config());
-      return;
     case TracePacket::kWinscopeExtensionsFieldNumber:
       ParseWinscopeExtensionsData(decoder.winscope_extensions(), timestamp,
                                   data);
diff --git a/src/trace_processor/importers/proto/winscope/winscope_module.h b/src/trace_processor/importers/proto/winscope/winscope_module.h
index 649e329..e14be59 100644
--- a/src/trace_processor/importers/proto/winscope/winscope_module.h
+++ b/src/trace_processor/importers/proto/winscope/winscope_module.h
@@ -36,6 +36,13 @@
  public:
   explicit WinscopeModule(TraceProcessorContext* context);
 
+  ModuleResult TokenizePacket(
+    const protos::pbzero::TracePacket::Decoder& decoder,
+    TraceBlobView* packet,
+    int64_t packet_timestamp,
+    RefPtr<PacketSequenceStateGeneration> state,
+    uint32_t field_id) override;
+
   void ParseTracePacketData(const protos::pbzero::TracePacket::Decoder&,
                             int64_t ts,
                             const TracePacketData&,
diff --git a/src/trace_processor/importers/systrace/systrace_line.h b/src/trace_processor/importers/systrace/systrace_line.h
index d125d0e..a5aa427 100644
--- a/src/trace_processor/importers/systrace/systrace_line.h
+++ b/src/trace_processor/importers/systrace/systrace_line.h
@@ -17,11 +17,10 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
 
-#include <cinttypes>
+#include <cstdint>
 #include <string>
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 struct alignas(8) SystraceLine {
   int64_t ts;
@@ -35,7 +34,6 @@
   std::string args_str;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_SYSTRACE_SYSTRACE_LINE_H_
diff --git a/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql b/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql
index 3011c54..1ed0a3d 100644
--- a/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_app_startup_rails.sql
@@ -31,7 +31,7 @@
 DROP VIEW IF EXISTS wattson_app_startup_rails_output;
 CREATE PERFETTO VIEW wattson_app_startup_rails_output AS
 SELECT AndroidWattsonTimePeriodMetric(
-  'metric_version', 2,
+  'metric_version', 3,
   'period_info', (
     SELECT RepeatedField(
       AndroidWattsonEstimateInfo(
diff --git a/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql b/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql
index e96ad8d..73e8e0d 100644
--- a/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_markers_rails.sql
@@ -33,7 +33,7 @@
 DROP VIEW IF EXISTS wattson_markers_rails_output;
 CREATE PERFETTO VIEW wattson_markers_rails_output AS
 SELECT AndroidWattsonTimePeriodMetric(
-  'metric_version', 2,
+  'metric_version', 3,
   'period_info', (
     SELECT RepeatedField(
       AndroidWattsonEstimateInfo(
diff --git a/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql b/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql
index 761de55..ce8666f 100644
--- a/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_markers_threads.sql
@@ -37,27 +37,27 @@
 CREATE PERFETTO VIEW _wattson_thread_attribution AS
 SELECT
   -- active time of thread divided by total time where Wattson is defined
-  SUM(estimate_mw * dur) / 1000000000 as estimate_mws,
+  SUM(estimated_mw * dur) / 1000000000 as estimated_mws,
   (
-    SUM(estimate_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
-  ) as estimate_mw,
+    SUM(estimated_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
+  ) as estimated_mw,
   thread_name,
   process_name,
   tid,
   pid
 FROM _windowed_threads_system_state
 GROUP BY utid
-ORDER BY estimate_mw DESC;
+ORDER BY estimated_mw DESC;
 
 DROP VIEW IF EXISTS wattson_markers_threads_output;
 CREATE PERFETTO VIEW wattson_markers_threads_output AS
 SELECT AndroidWattsonTasksAttributionMetric(
-  'metric_version', 1,
+  'metric_version', 2,
   'task_info', (
     SELECT RepeatedField(
       AndroidWattsonTaskInfo(
-        'estimate_mws', ROUND(estimate_mws, 6),
-        'estimate_mw', ROUND(estimate_mw, 6),
+        'estimated_mws', ROUND(estimated_mws, 6),
+        'estimated_mw', ROUND(estimated_mw, 6),
         'thread_name', thread_name,
         'process_name', process_name,
         'thread_id', tid,
diff --git a/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql b/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
index 57b805b..4a2c1b0 100644
--- a/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_rail_relations.sql
@@ -76,47 +76,47 @@
     is_defined,
     period_id,
     period_dur,
-    cast_double!(IIF(is_defined, sum_mw, NULL)) as estimate_mw,
+    cast_double!(IIF(is_defined, sum_mw, NULL)) as estimated_mw,
     AndroidWattsonPolicyEstimate(
-      'estimate_mw', cast_double!(IIF(is_defined, sum_mw, NULL)),
+      'estimated_mw', cast_double!(IIF(is_defined, sum_mw, NULL)),
       'cpu0', IIF(
         cpu0_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu0_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu0_mw),
         NULL
       ),
       'cpu1', IIF(
         cpu1_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu1_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu1_mw),
         NULL
       ),
       'cpu2', IIF(
         cpu2_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu2_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu2_mw),
         NULL
       ),
       'cpu3', IIF(
         cpu3_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu3_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu3_mw),
         NULL
       ),
       'cpu4', IIF(
         cpu4_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu4_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu4_mw),
         NULL
       ),
       'cpu5', IIF(
         cpu5_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu5_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu5_mw),
         NULL
       ),
       'cpu6', IIF(
         cpu6_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu6_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu6_mw),
         NULL
       ),
       'cpu7', IIF(
         cpu7_mw,
-        AndroidWattsonCpuEstimate('estimate_mw', cpu7_mw),
+        AndroidWattsonCpuEstimate('estimated_mw', cpu7_mw),
         NULL
       )
     ) AS proto
@@ -304,21 +304,21 @@
     period_id,
     period_dur,
     dsu_scu.dsu_scu_mw,
-    IIF(p0.is_defined, p0.estimate_mw, NULL) as p0_mw,
+    IIF(p0.is_defined, p0.estimated_mw, NULL) as p0_mw,
     IIF(p0.is_defined, p0.proto, NULL) as p0_proto,
-    IIF(p1.is_defined, p1.estimate_mw, NULL) as p1_mw,
+    IIF(p1.is_defined, p1.estimated_mw, NULL) as p1_mw,
     IIF(p1.is_defined, p1.proto, NULL) as p1_proto,
-    IIF(p2.is_defined, p2.estimate_mw, NULL) as p2_mw,
+    IIF(p2.is_defined, p2.estimated_mw, NULL) as p2_mw,
     IIF(p2.is_defined, p2.proto, NULL) as p2_proto,
-    IIF(p3.is_defined, p3.estimate_mw, NULL) as p3_mw,
+    IIF(p3.is_defined, p3.estimated_mw, NULL) as p3_mw,
     IIF(p3.is_defined, p3.proto, NULL) as p3_proto,
-    IIF(p4.is_defined, p4.estimate_mw, NULL) as p4_mw,
+    IIF(p4.is_defined, p4.estimated_mw, NULL) as p4_mw,
     IIF(p4.is_defined, p4.proto, NULL) as p4_proto,
-    IIF(p5.is_defined, p5.estimate_mw, NULL) as p5_mw,
+    IIF(p5.is_defined, p5.estimated_mw, NULL) as p5_mw,
     IIF(p5.is_defined, p5.proto, NULL) as p5_proto,
-    IIF(p6.is_defined, p6.estimate_mw, NULL) as p6_mw,
+    IIF(p6.is_defined, p6.estimated_mw, NULL) as p6_mw,
     IIF(p6.is_defined, p6.proto, NULL) as p6_proto,
-    IIF(p7.is_defined, p7.estimate_mw, NULL) as p7_mw,
+    IIF(p7.is_defined, p7.estimated_mw, NULL) as p7_mw,
     IIF(p7.is_defined, p7.proto, NULL) as p7_proto
   FROM _estimate_policy0_proto AS p0
   JOIN _estimate_policy1_proto AS p1 USING (period_id, period_dur)
@@ -344,7 +344,7 @@
   period_id,
   period_dur,
   AndroidWattsonCpuSubsystemEstimate(
-    'estimate_mw', sum_mw,
+    'estimated_mw', sum_mw,
     'policy0', p0_proto,
     'policy1', p1_proto,
     'policy2', p2_proto,
@@ -353,7 +353,7 @@
     'policy5', p5_proto,
     'policy6', p6_proto,
     'policy7', p7_proto,
-    'dsu_scu', AndroidWattsonDsuScuEstimate('estimate_mw', dsu_scu_mw)
+    'dsu_scu', AndroidWattsonDsuScuEstimate('estimated_mw', dsu_scu_mw)
   ) as proto
 FROM components_w_sum;
 
diff --git a/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql b/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
index 8f98714..61353c0 100644
--- a/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_tasks_attribution.sql
@@ -25,39 +25,39 @@
 -- "Unpivot" the table so that table can by PARTITIONED BY cpu
 DROP TABLE IF EXISTS _unioned_windowed_wattson;
 CREATE PERFETTO TABLE _unioned_windowed_wattson AS
-  SELECT ts, dur, 0 as cpu, cpu0_mw as estimate_mw
+  SELECT ts, dur, 0 as cpu, cpu0_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 0 = cpu)
   UNION ALL
-  SELECT ts, dur, 1 as cpu, cpu1_mw as estimate_mw
+  SELECT ts, dur, 1 as cpu, cpu1_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 1 = cpu)
   UNION ALL
-  SELECT ts, dur, 2 as cpu, cpu2_mw as estimate_mw
+  SELECT ts, dur, 2 as cpu, cpu2_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 2 = cpu)
   UNION ALL
-  SELECT ts, dur, 3 as cpu, cpu3_mw as estimate_mw
+  SELECT ts, dur, 3 as cpu, cpu3_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 3 = cpu)
   UNION ALL
-  SELECT ts, dur, 4 as cpu, cpu4_mw as estimate_mw
+  SELECT ts, dur, 4 as cpu, cpu4_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 4 = cpu)
   UNION ALL
-  SELECT ts, dur, 5 as cpu, cpu5_mw as estimate_mw
+  SELECT ts, dur, 5 as cpu, cpu5_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 5 = cpu)
   UNION ALL
-  SELECT ts, dur, 6 as cpu, cpu6_mw as estimate_mw
+  SELECT ts, dur, 6 as cpu, cpu6_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 6 = cpu)
   UNION ALL
-  SELECT ts, dur, 7 as cpu, cpu7_mw as estimate_mw
+  SELECT ts, dur, 7 as cpu, cpu7_mw as estimated_mw
   FROM _windowed_wattson
   WHERE EXISTS (SELECT cpu FROM _dev_cpu_policy_map WHERE 7 = cpu)
   UNION ALL
-  SELECT ts, dur, -1 as cpu, dsu_scu_mw as estimate_mw
+  SELECT ts, dur, -1 as cpu, dsu_scu_mw as estimated_mw
   FROM _windowed_wattson;
 
 DROP TABLE IF EXISTS _windowed_threads_system_state;
diff --git a/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql b/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
index ab035c0..e5489c4 100644
--- a/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_trace_rails.sql
@@ -33,7 +33,7 @@
 DROP VIEW IF EXISTS wattson_trace_rails_output;
 CREATE PERFETTO VIEW wattson_trace_rails_output AS
 SELECT AndroidWattsonTimePeriodMetric(
-  'metric_version', 2,
+  'metric_version', 3,
   'period_info', (
     SELECT RepeatedField(
       AndroidWattsonEstimateInfo(
diff --git a/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql b/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
index 455b876..bb83cab 100644
--- a/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
+++ b/src/trace_processor/metrics/sql/android/wattson_trace_threads.sql
@@ -36,27 +36,27 @@
 CREATE PERFETTO VIEW _wattson_thread_attribution AS
 SELECT
   -- active time of thread divided by total time of trace
-  SUM(estimate_mw * dur) / 1000000000 as estimate_mws,
+  SUM(estimated_mw * dur) / 1000000000 as estimated_mws,
   (
-    SUM(estimate_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
-  ) as estimate_mw,
+    SUM(estimated_mw * dur) / (SELECT SUM(dur) from _windowed_wattson)
+  ) as estimated_mw,
   thread_name,
   process_name,
   tid,
   pid
 FROM _windowed_threads_system_state
 GROUP BY utid
-ORDER BY estimate_mw DESC;
+ORDER BY estimated_mw DESC;
 
 DROP VIEW IF EXISTS wattson_trace_threads_output;
 CREATE PERFETTO VIEW wattson_trace_threads_output AS
 SELECT AndroidWattsonTasksAttributionMetric(
-  'metric_version', 1,
+  'metric_version', 2,
   'task_info', (
     SELECT RepeatedField(
       AndroidWattsonTaskInfo(
-        'estimate_mws', ROUND(estimate_mws, 6),
-        'estimate_mw', ROUND(estimate_mw, 6),
+        'estimated_mws', ROUND(estimated_mws, 6),
+        'estimated_mw', ROUND(estimated_mw, 6),
         'thread_name', thread_name,
         'process_name', process_name,
         'thread_id', tid,
diff --git a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
index 702151b..276f7d7 100644
--- a/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/callstacks/stack_profile.sql
@@ -68,7 +68,7 @@
   -- significant fraction of the runtime on big traces.
   IFNULL(
     DEMANGLE(COALESCE(s.name, f.deobfuscated_name, f.name)),
-    COALESCE(s.name, f.deobfuscated_name, f.name)
+    COALESCE(s.name, f.deobfuscated_name, f.name, '[Unknown]')
   ) AS name,
   f.mapping AS mapping_id,
   s.source_file,
@@ -112,3 +112,28 @@
   JOIN _callstack_spc_forest f USING (id)
   JOIN stack_profile_mapping m ON f.mapping_id = m.id
 );
+
+CREATE PERFETTO MACRO _callstacks_for_cpu_profile_stack_samples(
+  samples TableOrSubquery
+)
+RETURNS TableOrSubquery
+AS
+(
+  WITH metrics AS MATERIALIZED (
+    SELECT
+      callsite_id,
+      COUNT() AS self_count
+    FROM $samples
+    GROUP BY callsite_id
+  )
+  SELECT
+    c.id,
+    c.parent_id,
+    c.name,
+    c.mapping_name,
+    c.source_file,
+    c.line_number,
+    IFNULL(m.self_count, 0) AS self_count
+  FROM _callstacks_for_stack_profile_samples!(metrics) c
+  LEFT JOIN metrics m USING (callsite_id)
+);
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
index 42654eb..60d462d 100644
--- a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/BUILD.gn
@@ -19,6 +19,7 @@
     "frequency.sql",
     "idle.sql",
     "idle_stats.sql",
+    "idle_time_in_state.sql",
   ]
   deps = [ "utilization" ]
 }
diff --git a/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql
new file mode 100644
index 0000000..8bff4b3
--- /dev/null
+++ b/src/trace_processor/perfetto_sql/stdlib/linux/cpu/idle_time_in_state.sql
@@ -0,0 +1,74 @@
+--
+-- Copyright 2024 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+INCLUDE PERFETTO MODULE time.conversion;
+
+-- Counter information for sysfs cpuidle states.
+-- Tracks the percentage of time spent in each state between two timestamps, by
+-- dividing the incremental time spent in one state, by time all CPUS spent in
+-- any state.
+CREATE PERFETTO TABLE cpu_idle_time_in_state_counters(
+  -- Timestamp.
+  ts LONG,
+  -- State name.
+  state_name STRING,
+  -- Percentage of time all CPUS spent in this state.
+  idle_percentage DOUBLE,
+  -- Incremental time spent in this state (residency), in microseconds.
+  total_residency DOUBLE,
+  -- Time all CPUS spent in any state, in microseconds.
+  time_slice INT
+) AS
+WITH residency_deltas AS (
+  SELECT
+  ts,
+  c.name as state_name,
+  value - (LAG(value) OVER (PARTITION BY c.name, cct.cpu ORDER BY ts)) as delta
+  FROM counters c
+  JOIN cpu_counter_track cct on c.track_id=cct.id
+  WHERE c.name GLOB 'cpuidle.*'
+),
+total_residency_calc AS (
+SELECT
+  ts,
+  state_name,
+  sum(delta) as total_residency,
+  -- Perfetto timestamp is in nanoseconds whereas sysfs cpuidle time
+  -- is in microseconds.
+  (
+    (SELECT count(distinct cpu) from cpu_counter_track) *
+    (time_to_us(ts - LAG(ts,1) over (partition by state_name order by ts)))
+  )  as time_slice
+  FROM residency_deltas
+GROUP BY ts, state_name
+)
+SELECT
+  ts,
+  state_name,
+  MIN(100, (total_residency / time_slice) * 100) as idle_percentage,
+  total_residency,
+  time_slice
+FROM total_residency_calc
+WHERE time_slice IS NOT NULL
+UNION ALL
+-- Calculate c0 state by subtracting all other states from total time.
+SELECT
+  ts,
+  'cpuidle.C0' as state_name,
+  (MAX(0,time_slice - SUM(total_residency)) / time_slice) * 100 AS idle_percentage,
+  time_slice - SUM(total_residency),
+  time_slice
+FROM total_residency_calc
+WHERE time_slice IS NOT NULL
+GROUP BY ts;
diff --git a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
index 7a1d5d0..c950484 100644
--- a/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/viz/summary/tracks.sql
@@ -15,6 +15,20 @@
 
 INCLUDE PERFETTO MODULE viz.summary.slices;
 
+CREATE PERFETTO TABLE _thread_track_summary_by_utid_and_name AS
+SELECT
+  utid,
+  name,
+  -- Only meaningful when track_count == 1.
+  id as track_id,
+  -- Only meaningful when track_count == 1.
+  max_depth as max_depth,
+  GROUP_CONCAT(id) AS track_ids,
+  COUNT() AS track_count
+FROM thread_track
+JOIN _slice_track_summary USING (id)
+GROUP BY utid, name;
+
 CREATE PERFETTO TABLE _process_track_summary_by_upid_and_name AS
 SELECT
   upid,
diff --git a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql
index 6188c01..abb146f 100644
--- a/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/wattson/curves/idle_attribution.sql
@@ -117,7 +117,7 @@
     WHEN 6 THEN power.cpu6_mw
     WHEN 7 THEN power.cpu7_mw
     ELSE 0
-  END estimate_mw
+  END estimated_mw
 FROM _interval_intersect!(
   (
     _ii_table!(_idle_w_threads),
@@ -134,7 +134,7 @@
 CREATE PERFETTO FUNCTION _filter_idle_attribution(ts LONG, dur LONG)
 RETURNS Table(idle_cost_mws LONG, utid INT, upid INT, cpu INT) AS
 SELECT
-  cost.estimate_mw * cost.dur / 1e9 as idle_cost_mws,
+  cost.estimated_mw * cost.dur / 1e9 as idle_cost_mws,
   cost.utid,
   cost.upid,
   cost.cpu
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index a429c30..22400e4 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -33,7 +33,9 @@
     "../importers/common:parser_types",
     "../importers/common:trace_parser_hdr",
     "../importers/fuchsia:fuchsia_record",
+    "../importers/instruments:row",
     "../importers/perf:record",
+    "../importers/proto:packet_sequence_state_generation_hdr",
     "../importers/systrace:systrace_line",
     "../storage",
     "../types",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index b684e1a..924660a 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -202,6 +202,10 @@
       context.perf_record_parser->ParsePerfRecord(
           event.ts, token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      context.instruments_row_parser->ParseInstrumentsRow(
+          event.ts, token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kTracePacket:
       context.proto_trace_parser->ParseTracePacket(
           event.ts, token_buffer_.Extract<TracePacketData>(id));
@@ -226,6 +230,10 @@
       context.android_log_event_parser->ParseAndroidLogEvent(
           event.ts, token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      context.proto_trace_parser->ParseLegacyV8ProfileEvent(
+          event.ts, token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
     case TimestampedEvent::Type::kEtwEvent:
@@ -251,9 +259,11 @@
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -281,9 +291,11 @@
     case TimestampedEvent::Type::kSystraceLine:
     case TimestampedEvent::Type::kTracePacket:
     case TimestampedEvent::Type::kPerfRecord:
+    case TimestampedEvent::Type::kInstrumentsRow:
     case TimestampedEvent::Type::kJsonValue:
     case TimestampedEvent::Type::kFuchsiaRecord:
     case TimestampedEvent::Type::kAndroidLogEvent:
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -319,9 +331,15 @@
     case TimestampedEvent::Type::kPerfRecord:
       base::ignore_result(token_buffer_.Extract<perf_importer::Record>(id));
       return;
+    case TimestampedEvent::Type::kInstrumentsRow:
+      base::ignore_result(token_buffer_.Extract<instruments_importer::Row>(id));
+      return;
     case TimestampedEvent::Type::kAndroidLogEvent:
       base::ignore_result(token_buffer_.Extract<AndroidLogEvent>(id));
       return;
+    case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
+      base::ignore_result(token_buffer_.Extract<LegacyV8CpuProfileEvent>(id));
+      return;
   }
   PERFETTO_FATAL("For GCC");
 }
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 656b283..183f376 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -38,6 +38,7 @@
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/instruments/row.h"
 #include "src/trace_processor/importers/perf/record.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/sorter/trace_token_buffer.h"
@@ -128,6 +129,15 @@
                          machine_id);
   }
 
+  inline void PushInstrumentsRow(
+      int64_t timestamp,
+      instruments_importer::Row row,
+      std::optional<MachineId> machine_id = std::nullopt) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(row));
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kInstrumentsRow, id,
+                         machine_id);
+  }
+
   inline void PushTracePacket(
       int64_t timestamp,
       TracePacketData data,
@@ -199,6 +209,21 @@
     UpdateAppendMaxTs(queue);
   }
 
+  inline void PushLegacyV8CpuProfileEvent(
+      int64_t timestamp,
+      uint64_t session_id,
+      uint32_t pid,
+      uint32_t tid,
+      uint32_t callsite_id,
+      std::optional<MachineId> machine_id = std::nullopt) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(
+        LegacyV8CpuProfileEvent{session_id, pid, tid, callsite_id});
+    auto* queue = GetQueue(0, machine_id);
+    queue->Append(timestamp, TimestampedEvent::Type::kLegacyV8CpuProfileEvent,
+                  id);
+    UpdateAppendMaxTs(queue);
+  }
+
   inline void PushInlineFtraceEvent(
       uint32_t cpu,
       int64_t timestamp,
@@ -264,6 +289,7 @@
     enum class Type : uint8_t {
       kFtraceEvent,
       kPerfRecord,
+      kInstrumentsRow,
       kTracePacket,
       kInlineSchedSwitch,
       kInlineSchedWaking,
@@ -273,7 +299,8 @@
       kSystraceLine,
       kEtwEvent,
       kAndroidLogEvent,
-      kMax = kAndroidLogEvent,
+      kLegacyV8CpuProfileEvent,
+      kMax = kLegacyV8CpuProfileEvent,
     };
 
     // Number of bits required to store the max element in |Type|.
diff --git a/src/trace_processor/sorter/trace_token_buffer.cc b/src/trace_processor/sorter/trace_token_buffer.cc
index 7541a4d..ab8b15d 100644
--- a/src/trace_processor/sorter/trace_token_buffer.cc
+++ b/src/trace_processor/sorter/trace_token_buffer.cc
@@ -16,24 +16,24 @@
 
 #include "src/trace_processor/sorter/trace_token_buffer.h"
 
-#include <stdint.h>
 #include <algorithm>
 #include <cstdint>
 #include <cstring>
 #include <functional>
 #include <limits>
 #include <optional>
-#include <type_traits>
 #include <utility>
 
 #include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/util/bump_allocator.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 namespace {
 
 struct alignas(8) TrackEventDataDescriptor {
@@ -111,7 +111,7 @@
   InternedIndex interned_index = GetInternedIndex(alloc_id);
 
   // Compute the interning information for the TrackBlob and the SequenceState.
-  const TracePacketData& tpd = ted.trace_packet_data;
+  TracePacketData& tpd = ted.trace_packet_data;
   desc.intern_blob_offset = InternTraceBlob(interned_index, tpd.packet);
   desc.intern_blob_index =
       static_cast<uint16_t>(interned_blobs_.at(interned_index).size() - 1);
@@ -280,5 +280,4 @@
   return static_cast<size_t>(interned_index);
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index bddd78d..8838b4f 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -17,11 +17,9 @@
 #ifndef SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
 #define SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
 
-#include <stddef.h>
+#include <cstddef>
 
-namespace perfetto {
-namespace trace_processor {
-namespace stats {
+namespace perfetto::trace_processor::stats {
 
 // Compile time list of parsing and processing stats.
 // clang-format off
@@ -381,7 +379,12 @@
   F(mali_unknown_mcu_state_id,            kSingle,  kError,   kAnalysis,       \
       "An invalid Mali GPU MCU state ID was detected."),                       \
   F(pixel_modem_negative_timestamp,       kSingle,  kError,   kAnalysis,       \
-      "A negative timestamp was received from a Pixel modem event.")
+      "A negative timestamp was received from a Pixel modem event."),          \
+  F(legacy_v8_cpu_profile_invalid_callsite, kSingle,  kInfo,  kAnalysis,       \
+      "Indicates a callsite in legacy v8 CPU profiling is invalid."),          \
+  F(legacy_v8_cpu_profile_invalid_sample, kSingle,  kError,  kAnalysis,        \
+      "Indicates a sample in legacy v8 CPU profile is invalid. This will "     \
+      "cause CPU samples to be missing in the UI.")
 // clang-format on
 
 enum Type {
@@ -435,8 +438,6 @@
 constexpr char const* kDescriptions[] = {
     PERFETTO_TP_STATS(PERFETTO_TP_STATS_DESCRIPTION)};
 
-}  // namespace stats
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor::stats
 
 #endif  // SRC_TRACE_PROCESSOR_STORAGE_STATS_H_
diff --git a/src/trace_processor/storage/trace_storage.h b/src/trace_processor/storage/trace_storage.h
index b005f44..2a07c33 100644
--- a/src/trace_processor/storage/trace_storage.h
+++ b/src/trace_processor/storage/trace_storage.h
@@ -637,13 +637,6 @@
     return &trace_file_table_;
   }
 
-  const tables::StackSampleTable& stack_sample_table() const {
-    return stack_sample_table_;
-  }
-  tables::StackSampleTable* mutable_stack_sample_table() {
-    return &stack_sample_table_;
-  }
-
   const tables::CpuProfileStackSampleTable& cpu_profile_stack_sample_table()
       const {
     return cpu_profile_stack_sample_table_;
@@ -666,6 +659,13 @@
     return &perf_sample_table_;
   }
 
+  const tables::InstrumentsSampleTable& instruments_sample_table() const {
+    return instruments_sample_table_;
+  }
+  tables::InstrumentsSampleTable* mutable_instruments_sample_table() {
+    return &instruments_sample_table_;
+  }
+
   const tables::SymbolTable& symbol_table() const { return symbol_table_; }
 
   tables::SymbolTable* mutable_symbol_table() { return &symbol_table_; }
@@ -1152,13 +1152,13 @@
   tables::StackProfileFrameTable stack_profile_frame_table_{&string_pool_};
   tables::StackProfileCallsiteTable stack_profile_callsite_table_{
       &string_pool_};
-  tables::StackSampleTable stack_sample_table_{&string_pool_};
   tables::HeapProfileAllocationTable heap_profile_allocation_table_{
       &string_pool_};
   tables::CpuProfileStackSampleTable cpu_profile_stack_sample_table_{
-      &string_pool_, &stack_sample_table_};
+      &string_pool_};
   tables::PerfSessionTable perf_session_table_{&string_pool_};
   tables::PerfSampleTable perf_sample_table_{&string_pool_};
+  tables::InstrumentsSampleTable instruments_sample_table_{&string_pool_};
   tables::PackageListTable package_list_table_{&string_pool_};
   tables::AndroidGameInterventionListTable
       android_game_intervention_list_table_{&string_pool_};
diff --git a/src/trace_processor/tables/profiler_tables.py b/src/trace_processor/tables/profiler_tables.py
index 2c55640..60cb1fc 100644
--- a/src/trace_processor/tables/profiler_tables.py
+++ b/src/trace_processor/tables/profiler_tables.py
@@ -209,39 +209,22 @@
                 '''Frame at this position in the callstack.'''
         }))
 
-STACK_SAMPLE_TABLE = Table(
-    python_module=__file__,
-    class_name='StackSampleTable',
-    sql_name='stack_sample',
-    columns=[
-        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
-        C('callsite_id', CppTableId(STACK_PROFILE_CALLSITE_TABLE)),
-    ],
-    tabledoc=TableDoc(
-        doc='''
-          Root table for timestamped stack samples.
-        ''',
-        group='Callstack profilers',
-        columns={
-            'ts': '''timestamp of the sample.''',
-            'callsite_id': '''unwound callstack.'''
-        }))
-
 CPU_PROFILE_STACK_SAMPLE_TABLE = Table(
     python_module=__file__,
     class_name='CpuProfileStackSampleTable',
     sql_name='cpu_profile_stack_sample',
     columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('callsite_id', CppTableId(STACK_PROFILE_CALLSITE_TABLE)),
         C('utid', CppUint32()),
         C('process_priority', CppInt32()),
     ],
-    parent=STACK_SAMPLE_TABLE,
     tabledoc=TableDoc(
-        doc='''
-          Samples from the Chromium stack sampler.
-        ''',
+        doc='Table containing stack samples from CPU profiling.',
         group='Callstack profilers',
         columns={
+            'ts': '''timestamp of the sample.''',
+            'callsite_id': '''unwound callstack.''',
             'utid': '''thread that was active when the sample was taken.''',
             'process_priority': ''''''
         }))
@@ -254,9 +237,7 @@
         C('cmdline', CppOptional(CppString())),
     ],
     tabledoc=TableDoc(
-        doc='''
-          Perf sessions.
-        ''',
+        doc='''Perf sessions.''',
         group='Callstack profilers',
         columns={
             'cmdline': '''Command line used to collect the data.''',
@@ -276,9 +257,7 @@
         C('perf_session_id', CppTableId(PERF_SESSION_TABLE)),
     ],
     tabledoc=TableDoc(
-        doc='''
-          Samples from the traced_perf profiler.
-        ''',
+        doc='''Samples from the traced_perf profiler.''',
         group='Callstack profilers',
         columns={
             'ts':
@@ -302,6 +281,32 @@
                 streams (i.e. multiple data sources).'''
         }))
 
+INSTRUMENTS_SAMPLE_TABLE = Table(
+    python_module=__file__,
+    class_name='InstrumentsSampleTable',
+    sql_name='instruments_sample',
+    columns=[
+        C('ts', CppInt64(), flags=ColumnFlag.SORTED),
+        C('utid', CppUint32()),
+        C('cpu', CppOptional(CppUint32())),
+        C('callsite_id', CppOptional(CppTableId(STACK_PROFILE_CALLSITE_TABLE))),
+    ],
+    tabledoc=TableDoc(
+        doc='''
+          Samples from MacOS Instruments.
+        ''',
+        group='Callstack profilers',
+        columns={
+            'ts':
+                '''Timestamp of the sample.''',
+            'utid':
+                '''Sampled thread.''',
+            'cpu':
+                '''Core the sampled thread was running on.''',
+            'callsite_id':
+                '''If set, unwound callstack of the sampled thread.''',
+        }))
+
 SYMBOL_TABLE = Table(
     python_module=__file__,
     class_name='SymbolTable',
@@ -659,6 +664,7 @@
     HEAP_GRAPH_CLASS_TABLE,
     HEAP_GRAPH_OBJECT_TABLE,
     HEAP_GRAPH_REFERENCE_TABLE,
+    INSTRUMENTS_SAMPLE_TABLE,
     HEAP_PROFILE_ALLOCATION_TABLE,
     PACKAGE_LIST_TABLE,
     PERF_SAMPLE_TABLE,
@@ -667,7 +673,6 @@
     STACK_PROFILE_CALLSITE_TABLE,
     STACK_PROFILE_FRAME_TABLE,
     STACK_PROFILE_MAPPING_TABLE,
-    STACK_SAMPLE_TABLE,
     SYMBOL_TABLE,
     VULKAN_MEMORY_ALLOCATIONS_TABLE,
     PERF_COUNTER_TRACK_TABLE,
diff --git a/src/trace_processor/tables/table_destructors.cc b/src/trace_processor/tables/table_destructors.cc
index 229c724..f993a51 100644
--- a/src/trace_processor/tables/table_destructors.cc
+++ b/src/trace_processor/tables/table_destructors.cc
@@ -70,10 +70,10 @@
 StackProfileMappingTable::~StackProfileMappingTable() = default;
 StackProfileFrameTable::~StackProfileFrameTable() = default;
 StackProfileCallsiteTable::~StackProfileCallsiteTable() = default;
-StackSampleTable::~StackSampleTable() = default;
 CpuProfileStackSampleTable::~CpuProfileStackSampleTable() = default;
 PerfSessionTable::~PerfSessionTable() = default;
 PerfSampleTable::~PerfSampleTable() = default;
+InstrumentsSampleTable::~InstrumentsSampleTable() = default;
 SymbolTable::~SymbolTable() = default;
 HeapProfileAllocationTable::~HeapProfileAllocationTable() = default;
 ExperimentalFlamegraphTable::~ExperimentalFlamegraphTable() = default;
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 0da4693..53876b0 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -15,21 +15,22 @@
  */
 
 #include "src/trace_processor/types/trace_processor_context.h"
+
 #include <memory>
 #include <optional>
 
+#include "perfetto/base/logging.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
-#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/common/clock_converter.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/cpu_tracker.h"
-#include "src/trace_processor/importers/common/deobfuscation_mapping_table.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/legacy_v8_cpu_profile_tracker.h"
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
@@ -41,20 +42,16 @@
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
-#include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/android_track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/track_event.descriptor.h"
-#include "src/trace_processor/importers/proto/track_event_module.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_reader_registry.h"
-#include "src/trace_processor/types/destructible.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 TraceProcessorContext::TraceProcessorContext(const InitArgs& args)
     : config(args.config), storage(args.storage) {
@@ -109,11 +106,17 @@
       });
 
   trace_file_tracker = std::make_unique<TraceFileTracker>(this);
+  legacy_v8_cpu_profile_tracker =
+      std::make_unique<LegacyV8CpuProfileTracker>(this);
 }
 
 TraceProcessorContext::TraceProcessorContext() = default;
 TraceProcessorContext::~TraceProcessorContext() = default;
 
+TraceProcessorContext::TraceProcessorContext(TraceProcessorContext&&) = default;
+TraceProcessorContext& TraceProcessorContext::operator=(
+    TraceProcessorContext&&) = default;
+
 std::optional<MachineId> TraceProcessorContext::machine_id() const {
   if (!machine_tracker) {
     // Doesn't require that |machine_tracker| is initialzed, e.g. in unit tests.
@@ -122,5 +125,4 @@
   return machine_tracker->machine_id();
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 9f101bd..3a8dac5 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -51,6 +51,8 @@
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 #include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
+#include "src/trace_processor/importers/instruments/instruments_xml_tokenizer.h"
+#include "src/trace_processor/importers/instruments/row_parser.h"
 #include "src/trace_processor/importers/json/json_trace_parser_impl.h"
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 #include "src/trace_processor/importers/json/json_utils.h"
@@ -355,6 +357,10 @@
     start_ns = std::min(it.ts(), start_ns);
     end_ns = std::max(it.ts(), end_ns);
   }
+  for (auto it = storage.instruments_sample_table().IterateRows(); it; ++it) {
+    start_ns = std::min(it.ts(), start_ns);
+    end_ns = std::max(it.ts(), end_ns);
+  }
   for (auto it = storage.cpu_profile_stack_sample_table().IterateRows(); it;
        ++it) {
     start_ns = std::min(it.ts(), start_ns);
@@ -394,6 +400,12 @@
   context_.perf_record_parser =
       std::make_unique<perf_importer::RecordParser>(&context_);
 
+  context_.reader_registry
+      ->RegisterTraceReader<instruments_importer::InstrumentsXmlTokenizer>(
+          kInstrumentsXmlTraceType);
+  context_.instruments_row_parser =
+      std::make_unique<instruments_importer::RowParser>(&context_);
+
   if (util::IsGzipSupported()) {
     context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
         kGzipTraceType);
@@ -907,6 +919,7 @@
   RegisterStaticTable(storage->mutable_cpu_profile_stack_sample_table());
   RegisterStaticTable(storage->mutable_perf_session_table());
   RegisterStaticTable(storage->mutable_perf_sample_table());
+  RegisterStaticTable(storage->mutable_instruments_sample_table());
   RegisterStaticTable(storage->mutable_stack_profile_callsite_table());
   RegisterStaticTable(storage->mutable_stack_profile_mapping_table());
   RegisterStaticTable(storage->mutable_stack_profile_frame_table());
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index 7478dd9..8325d87 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -22,38 +22,35 @@
 #include <memory>
 #include <utility>
 
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/uuid.h"
+#include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
-#include "src/trace_processor/importers/common/clock_converter.h"
+#include "src/trace_processor/importers/common/clock_converter.h"  // IWYU pragma: keep
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
-#include "src/trace_processor/importers/common/flow_tracker.h"
-#include "src/trace_processor/importers/common/machine_tracker.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
-#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/common/sched_event_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/common/slice_translation_table.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
-#include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/perf/dso_tracker.h"
-#include "src/trace_processor/importers/proto/chrome_track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/default_modules.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
 #include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/importers/proto/proto_importer_module.h"
 #include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
-#include "src/trace_processor/importers/proto/track_event.descriptor.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/metadata.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/trace_reader_registry.h"
-#include "src/trace_processor/util/descriptors.h"
+#include "src/trace_processor/types/variadic.h"
+#include "src/trace_processor/util/status_macros.h"
 #include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto::trace_processor {
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
index b071295..dcecaf9 100644
--- a/src/trace_processor/trace_reader_registry.cc
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -37,6 +37,7 @@
     case kNinjaLogTraceType:
     case kSystraceTraceType:
     case kPerfDataTraceType:
+    case kInstrumentsXmlTraceType:
     case kUnknownTraceType:
     case kJsonTraceType:
     case kFuchsiaTraceType:
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 909959c..3e7ba55 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -17,6 +17,7 @@
 #ifndef SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
 #define SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
 
+#include <cstdint>
 #include <memory>
 #include <optional>
 #include <vector>
@@ -24,10 +25,8 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/util/trace_type.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 class AndroidLogEventParser;
 class ArgsTracker;
@@ -47,7 +46,9 @@
 class FuchsiaRecordParser;
 class GlobalArgsTracker;
 class HeapGraphTracker;
+class InstrumentsRowParser;
 class JsonTraceParser;
+class LegacyV8CpuProfileTracker;
 class MachineTracker;
 class MappingTracker;
 class MetadataTracker;
@@ -79,13 +80,15 @@
     std::shared_ptr<TraceStorage> storage;
     uint32_t raw_machine_id = 0;
   };
+
   explicit TraceProcessorContext(const InitArgs&);
+
   // The default constructor is used in testing.
   TraceProcessorContext();
   ~TraceProcessorContext();
 
-  TraceProcessorContext(TraceProcessorContext&&) = default;
-  TraceProcessorContext& operator=(TraceProcessorContext&&) = default;
+  TraceProcessorContext(TraceProcessorContext&&);
+  TraceProcessorContext& operator=(TraceProcessorContext&&);
 
   Config config;
 
@@ -128,6 +131,7 @@
   std::unique_ptr<MetadataTracker> metadata_tracker;
   std::unique_ptr<CpuTracker> cpu_tracker;
   std::unique_ptr<TraceFileTracker> trace_file_tracker;
+  std::unique_ptr<LegacyV8CpuProfileTracker> legacy_v8_cpu_profile_tracker;
 
   // These fields are stored as pointers to Destructible objects rather than
   // their actual type (a subclass of Destructible), as the concrete subclass
@@ -135,25 +139,26 @@
   // the GetOrCreate() method on their subclass type, e.g.
   // SyscallTracker::GetOrCreate(context)
   // clang-format off
-  std::unique_ptr<Destructible> android_probes_tracker;    // AndroidProbesTracker
-  std::unique_ptr<Destructible> binder_tracker;            // BinderTracker
-  std::unique_ptr<Destructible> heap_graph_tracker;        // HeapGraphTracker
-  std::unique_ptr<Destructible> syscall_tracker;           // SyscallTracker
-  std::unique_ptr<Destructible> system_info_tracker;       // SystemInfoTracker
-  std::unique_ptr<Destructible> v4l2_tracker;              // V4l2Tracker
-  std::unique_ptr<Destructible> virtio_video_tracker;      // VirtioVideoTracker
-  std::unique_ptr<Destructible> systrace_parser;           // SystraceParser
-  std::unique_ptr<Destructible> thread_state_tracker;      // ThreadStateTracker
-  std::unique_ptr<Destructible> i2c_tracker;               // I2CTracker
-  std::unique_ptr<Destructible> perf_data_tracker;         // PerfDataTracker
-  std::unique_ptr<Destructible> content_analyzer;          // ProtoContentAnalyzer
-  std::unique_ptr<Destructible> shell_transitions_tracker; // ShellTransitionsTracker
-  std::unique_ptr<Destructible> protolog_messages_tracker; // ProtoLogMessagesTracker
-  std::unique_ptr<Destructible> ftrace_sched_tracker;      // FtraceSchedEventTracker
-  std::unique_ptr<Destructible> v8_tracker;                // V8Tracker
-  std::unique_ptr<Destructible> jit_tracker;               // JitTracker
-  std::unique_ptr<Destructible> perf_dso_tracker;          // DsoTracker
-  std::unique_ptr<Destructible> protolog_message_decoder;  // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> android_probes_tracker;       // AndroidProbesTracker
+  std::unique_ptr<Destructible> binder_tracker;               // BinderTracker
+  std::unique_ptr<Destructible> heap_graph_tracker;           // HeapGraphTracker
+  std::unique_ptr<Destructible> syscall_tracker;              // SyscallTracker
+  std::unique_ptr<Destructible> system_info_tracker;          // SystemInfoTracker
+  std::unique_ptr<Destructible> v4l2_tracker;                 // V4l2Tracker
+  std::unique_ptr<Destructible> virtio_video_tracker;         // VirtioVideoTracker
+  std::unique_ptr<Destructible> systrace_parser;              // SystraceParser
+  std::unique_ptr<Destructible> thread_state_tracker;         // ThreadStateTracker
+  std::unique_ptr<Destructible> i2c_tracker;                  // I2CTracker
+  std::unique_ptr<Destructible> perf_data_tracker;            // PerfDataTracker
+  std::unique_ptr<Destructible> content_analyzer;             // ProtoContentAnalyzer
+  std::unique_ptr<Destructible> shell_transitions_tracker;    // ShellTransitionsTracker
+  std::unique_ptr<Destructible> protolog_messages_tracker;    // ProtoLogMessagesTracker
+  std::unique_ptr<Destructible> ftrace_sched_tracker;         // FtraceSchedEventTracker
+  std::unique_ptr<Destructible> v8_tracker;                   // V8Tracker
+  std::unique_ptr<Destructible> jit_tracker;                  // JitTracker
+  std::unique_ptr<Destructible> perf_dso_tracker;             // DsoTracker
+  std::unique_ptr<Destructible> protolog_message_decoder;     // ProtoLogMessageDecoder
+  std::unique_ptr<Destructible> instruments_row_data_tracker; // RowDataTracker
   // clang-format on
 
   std::unique_ptr<ProtoTraceParser> proto_trace_parser;
@@ -164,6 +169,7 @@
   std::unique_ptr<JsonTraceParser> json_trace_parser;
   std::unique_ptr<FuchsiaRecordParser> fuchsia_record_parser;
   std::unique_ptr<PerfRecordParser> perf_record_parser;
+  std::unique_ptr<InstrumentsRowParser> instruments_row_parser;
   std::unique_ptr<AndroidLogEventParser> android_log_event_parser;
 
   // This field contains the list of proto descriptors that can be used by
@@ -192,7 +198,6 @@
   std::unique_ptr<MultiMachineTraceManager> multi_machine_trace_manager;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_TYPES_TRACE_PROCESSOR_CONTEXT_H_
diff --git a/src/trace_processor/util/build_id.cc b/src/trace_processor/util/build_id.cc
index 20d76dd..d037e0e 100644
--- a/src/trace_processor/util/build_id.cc
+++ b/src/trace_processor/util/build_id.cc
@@ -83,6 +83,10 @@
   }
 
   while (it != hex.end()) {
+    if (*it == '-') {
+      ++it;
+      continue;
+    }
     int v = (HexToBinary(*it++) << 4);
     v += HexToBinary(*it++);
     res.push_back(static_cast<char>(v));
diff --git a/src/trace_processor/util/debug_annotation_parser.cc b/src/trace_processor/util/debug_annotation_parser.cc
index 0c2792e..6669b4d 100644
--- a/src/trace_processor/util/debug_annotation_parser.cc
+++ b/src/trace_processor/util/debug_annotation_parser.cc
@@ -36,7 +36,7 @@
   return result;
 }
 
-bool IsJsonSupported() {
+constexpr bool IsJsonSupported() {
 #if PERFETTO_BUILDFLAG(PERFETTO_TP_JSON)
   return true;
 #else
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
index 4c4b042..3c64c3f 100644
--- a/src/trace_processor/util/trace_type.cc
+++ b/src/trace_processor/util/trace_type.cc
@@ -119,6 +119,8 @@
       return "zip";
     case kPerfDataTraceType:
       return "perf";
+    case kInstrumentsXmlTraceType:
+      return "instruments_xml";
     case kAndroidLogcatTraceType:
       return "android_logcat";
     case kAndroidDumpstateTraceType:
@@ -172,6 +174,10 @@
       base::StartsWith(lower_start, "<html>"))
     return kSystraceTraceType;
 
+  // MacOS Instruments XML export.
+  if (base::StartsWith(start, "<?xml version=\"1.0\"?>\n<trace-query-result>"))
+    return kInstrumentsXmlTraceType;
+
   // Traces obtained from atrace -z (compress).
   // They all have the string "TRACE:" followed by 78 9C which is a zlib header
   // for "deflate, default compression, window size=32K" (see b/208691037)
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
index 7e730fc..dbf72ea 100644
--- a/src/trace_processor/util/trace_type.h
+++ b/src/trace_processor/util/trace_type.h
@@ -37,6 +37,7 @@
   kSystraceTraceType,
   kUnknownTraceType,
   kZipFile,
+  kInstrumentsXmlTraceType,
 };
 
 constexpr size_t kGuessTraceMaxLookahead = 64;
diff --git a/src/traced/probes/ftrace/event_info.cc b/src/traced/probes/ftrace/event_info.cc
index 4bc6a0c..247b41d 100644
--- a/src/traced/probes/ftrace/event_info.cc
+++ b/src/traced/probes/ftrace/event_info.cc
@@ -8227,19 +8227,19 @@
             "new_pid", 2, ProtoSchemaType::kInt32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "cctr", 3, ProtoSchemaType::kUint32,
+            "cctr", 3, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr0", 4, ProtoSchemaType::kUint32,
+            "ctr0", 4, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr1", 5, ProtoSchemaType::kUint32,
+            "ctr1", 5, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr2", 6, ProtoSchemaType::kUint32,
+            "ctr2", 6, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr3", 7, ProtoSchemaType::kUint32,
+            "ctr3", 7, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "lctr0", 8, ProtoSchemaType::kUint32,
@@ -8248,10 +8248,10 @@
             "lctr1", 9, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr4", 10, ProtoSchemaType::kUint32,
+            "ctr4", 10, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
-            "ctr5", 11, ProtoSchemaType::kUint32,
+            "ctr5", 11, ProtoSchemaType::kUint64,
             TranslationStrategy::kInvalidTranslationStrategy},
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "prev_comm", 12, ProtoSchemaType::kString,
@@ -8271,10 +8271,51 @@
            {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
             "l3dm", 17, ProtoSchemaType::kUint32,
             TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "next_pid", 18, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "next_comm", 19, ProtoSchemaType::kString,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "prev_state", 20, ProtoSchemaType::kInt64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu0", 21, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu1", 22, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "amu2", 23, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
        },
        kUnsetFtraceId,
        487,
        kUnsetSize},
+      {"pixel_mm_kswapd_wake",
+       "pixel_mm",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "whatever", 1, ProtoSchemaType::kInt32,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       538,
+       kUnsetSize},
+      {"pixel_mm_kswapd_done",
+       "pixel_mm",
+       {
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "delta_nr_scanned", 1, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+           {kUnsetOffset, kUnsetSize, FtraceFieldType::kInvalidFtraceFieldType,
+            "delta_nr_reclaimed", 2, ProtoSchemaType::kUint64,
+            TranslationStrategy::kInvalidTranslationStrategy},
+       },
+       kUnsetFtraceId,
+       539,
+       kUnsetSize},
       {"cpu_frequency",
        "power",
        {
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format
new file mode 100644
index 0000000..8a06a55
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_done/format
@@ -0,0 +1,12 @@
+name: pixel_mm_kswapd_done
+ID: 1092
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:unsigned long delta_nr_scanned;	offset:8;	size:8;	signed:0;
+	field:unsigned long delta_nr_reclaimed;	offset:16;	size:8;	signed:0;
+
+print fmt: "delta_nr_scanned=%lu, delta_nr_reclaimed=%lu", REC->delta_nr_scanned, REC->delta_nr_reclaimed
diff --git a/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format
new file mode 100644
index 0000000..6697281
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic/events/pixel_mm/pixel_mm_kswapd_wake/format
@@ -0,0 +1,11 @@
+name: pixel_mm_kswapd_wake
+ID: 1091
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:int whatever;	offset:8;	size:4;	signed:1;
+
+print fmt: "%s", ""
diff --git a/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format b/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format
new file mode 100644
index 0000000..1e27eaf
--- /dev/null
+++ b/src/traced/probes/ftrace/test/data/synthetic_alt/events/perf_trace_counters/sched_switch_with_ctrs/format
@@ -0,0 +1,25 @@
+name: sched_switch_with_ctrs
+ID: 1241
+format:
+	field:unsigned short common_type;	offset:0;	size:2;	signed:0;
+	field:unsigned char common_flags;	offset:2;	size:1;	signed:0;
+	field:unsigned char common_preempt_count;	offset:3;	size:1;	signed:0;
+	field:int common_pid;	offset:4;	size:4;	signed:1;
+
+	field:pid_t prev_pid;	offset:8;	size:4;	signed:1;
+	field:pid_t next_pid;	offset:12;	size:4;	signed:1;
+	field:char prev_comm[16];	offset:16;	size:16;	signed:0;
+	field:char next_comm[16];	offset:32;	size:16;	signed:0;
+	field:long prev_state;	offset:48;	size:8;	signed:1;
+	field:unsigned long cctr;	offset:56;	size:8;	signed:0;
+	field:unsigned long ctr0;	offset:64;	size:8;	signed:0;
+	field:unsigned long ctr1;	offset:72;	size:8;	signed:0;
+	field:unsigned long ctr2;	offset:80;	size:8;	signed:0;
+	field:unsigned long ctr3;	offset:88;	size:8;	signed:0;
+	field:unsigned long ctr4;	offset:96;	size:8;	signed:0;
+	field:unsigned long ctr5;	offset:104;	size:8;	signed:0;
+	field:unsigned long amu0;	offset:112;	size:8;	signed:0;
+	field:unsigned long amu1;	offset:120;	size:8;	signed:0;
+	field:unsigned long amu2;	offset:128;	size:8;	signed:0;
+
+print fmt: "prev_comm=%s prev_pid=%d prev_state=%s%s ==> next_comm=%s next_pid=%d CCNTR=%lu CTR0=%lu CTR1=%lu CTR2=%lu CTR3=%lu CTR4=%lu CTR5=%lu, CYC: %lu, INST: %lu, STALL: %lu", REC->prev_comm, REC->prev_pid, (REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1)) ? __print_flags(REC->prev_state & ((((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) - 1), "|", { 0x00000001, "S" }, { 0x00000002, "D" }, { 0x00000004, "T" }, { 0x00000008, "t" }, { 0x00000010, "X" }, { 0x00000020, "Z" }, { 0x00000040, "P" }, { 0x00000080, "I" }) : "R", REC->prev_state & (((0x00000000 | 0x00000001 | 0x00000002 | 0x00000004 | 0x00000008 | 0x00000010 | 0x00000020 | 0x00000040) + 1) << 1) ? "+" : "", REC->next_comm, REC->next_pid, REC->cctr, REC->ctr0, REC->ctr1, REC->ctr2, REC->ctr3, REC->ctr4, REC->ctr5, REC->amu0, REC->amu1, REC->amu2
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 4ad7f44..6658512 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -30,6 +30,9 @@
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_descriptor.pbzero.h"
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+#include <os/signpost.h>
+#endif
 
 using perfetto::protos::pbzero::ClockSnapshot;
 
@@ -420,6 +423,18 @@
           thread_time_counter_track.uuid);
     }
 
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_MAC)
+    // Emit a MacOS point-of-interest signpost to synchonize Mac profiler time
+    // with boot time.
+    // TODO(leszeks): Consider allowing synchronization against other clocks
+    // than boot time.
+    static os_log_t log_handle = os_log_create(
+        "dev.perfetto.clock_sync", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
+    os_signpost_event_emit(
+        log_handle, OS_SIGNPOST_ID_EXCLUSIVE, "boottime", "%" PRId64,
+        static_cast<uint64_t>(perfetto::base::GetBootTimeNs().count()));
+#endif
+
     if (tls_state.default_clock != static_cast<uint32_t>(GetClockId())) {
       ClockSnapshot* clocks = packet->set_clock_snapshot();
       // Trace clock.
diff --git a/test/data/instruments_trace.xml.sha256 b/test/data/instruments_trace.xml.sha256
new file mode 100644
index 0000000..f524f24
--- /dev/null
+++ b/test/data/instruments_trace.xml.sha256
@@ -0,0 +1 @@
+1f87b2e3f5617f947c3c22fe2282e3341ed4d3b4f37ad2e6752c6dd54836db8a
\ No newline at end of file
diff --git a/test/data/instruments_trace_symbols.pb.sha256 b/test/data/instruments_trace_symbols.pb.sha256
new file mode 100644
index 0000000..8e33c19
--- /dev/null
+++ b/test/data/instruments_trace_symbols.pb.sha256
@@ -0,0 +1 @@
+1f5096a97bd9b7176b1ec52863d02e3e85e19eedbfbbe51cbbecedce8a0248cb
\ No newline at end of file
diff --git a/test/data/instruments_trace_with_symbols.zip.sha256 b/test/data/instruments_trace_with_symbols.zip.sha256
new file mode 100644
index 0000000..adbf7aa
--- /dev/null
+++ b/test/data/instruments_trace_with_symbols.zip.sha256
@@ -0,0 +1 @@
+70733124cf53b8065512204d9a72c7818ebd04afb10cf3c69cc926a2aa5ee07e
\ No newline at end of file
diff --git a/test/data/v8-samples.pftrace.sha256 b/test/data/v8-samples.pftrace.sha256
new file mode 100644
index 0000000..c9a7d2f
--- /dev/null
+++ b/test/data/v8-samples.pftrace.sha256
@@ -0,0 +1 @@
+564159912db8d8252562f143e6206ae9964759e16193ebd3addd04823d02a6ae
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 5840800..1f39362 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -72,6 +72,7 @@
 from diff_tests.parser.graphics.tests import GraphicsParser
 from diff_tests.parser.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
 from diff_tests.parser.graphics.tests_gpu_trace import GraphicsGpuTrace
+from diff_tests.parser.instruments.tests import Instruments
 from diff_tests.parser.json.tests import JsonParser
 from diff_tests.parser.memory.tests import MemoryParser
 from diff_tests.parser.network.tests import NetworkParser
@@ -238,6 +239,7 @@
       *Zip(index_path, 'parser/zip', 'Zip').fetch(),
       *AndroidInputEvent(index_path, 'parser/android',
                          'AndroidInputEvent').fetch(),
+      *Instruments(index_path, 'parser/instruments', 'Instruments').fetch(),
   ]
 
   metrics_tests = [
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index ff3de29..347f95f 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -375,47 +375,47 @@
         query=Metric("wattson_app_startup_rails"),
         out=Csv("""
         wattson_app_startup_rails {
-          metric_version: 2
+          metric_version: 3
           period_info {
             period_id: 1
             period_dur: 384847255
             cpu_subsystem {
-              estimate_mw: 4568.1772
+              estimated_mw: 4568.1772
               policy0 {
-                estimate_mw: 578.31256
+                estimated_mw: 578.31256
                 cpu0 {
-                  estimate_mw: 148.99423
+                  estimated_mw: 148.99423
                 }
                 cpu1 {
-                  estimate_mw: 130.13142
+                  estimated_mw: 130.13142
                 }
                 cpu2 {
-                  estimate_mw: 127.60357
+                  estimated_mw: 127.60357
                 }
                 cpu3 {
-                  estimate_mw: 171.58333
+                  estimated_mw: 171.58333
                 }
               }
               policy4 {
-                estimate_mw: 684.18835
+                estimated_mw: 684.18835
                 cpu4 {
-                  estimate_mw: 344.39563
+                  estimated_mw: 344.39563
                 }
                 cpu5 {
-                  estimate_mw: 339.7927
+                  estimated_mw: 339.7927
                 }
               }
               policy6 {
-                estimate_mw: 2163.158
+                estimated_mw: 2163.158
                 cpu6 {
-                  estimate_mw: 1080.6881
+                  estimated_mw: 1080.6881
                 }
                 cpu7 {
-                  estimate_mw: 1082.47
+                  estimated_mw: 1082.47
                 }
               }
               dsu_scu {
-                estimate_mw: 1142.5181
+                estimated_mw: 1142.5181
               }
             }
           }
@@ -428,29 +428,29 @@
         query=Metric("wattson_trace_rails"),
         out=Csv("""
         wattson_trace_rails {
-          metric_version: 2
+          metric_version: 3
           period_info {
             period_id: 1
             period_dur: 61792616758
             cpu_subsystem {
-              estimate_mw: 42.12355
+              estimated_mw: 42.12355
               policy0 {
-                estimate_mw: 34.71888
+                estimated_mw: 34.71888
                 cpu0 {
-                  estimate_mw: 10.7050705
+                  estimated_mw: 10.7050705
                 }
                 cpu1 {
-                  estimate_mw: 8.315672
+                  estimated_mw: 8.315672
                 }
                 cpu2 {
-                  estimate_mw: 7.7776303
+                  estimated_mw: 7.7776303
                 }
                 cpu3 {
-                  estimate_mw: 7.920505
+                  estimated_mw: 7.920505
                 }
               }
               dsu_scu {
-                estimate_mw: 7.404673
+                estimated_mw: 7.404673
               }
             }
           }
@@ -481,29 +481,29 @@
         query=Metric("wattson_markers_rails"),
         out=Csv("""
         wattson_markers_rails {
-          metric_version: 2
+          metric_version: 3
           period_info {
             period_id: 1
             period_dur: 2031871358
             cpu_subsystem {
-              estimate_mw: 46.540943
+              estimated_mw: 46.540943
               policy0 {
-                estimate_mw: 34.037483
+                estimated_mw: 34.037483
                 cpu0 {
-                  estimate_mw: 14.416655
+                  estimated_mw: 14.416655
                 }
                 cpu1 {
-                  estimate_mw: 6.641429
+                  estimated_mw: 6.641429
                 }
                 cpu2 {
-                  estimate_mw: 8.134797
+                  estimated_mw: 8.134797
                 }
                 cpu3 {
-                  estimate_mw: 4.8446035
+                  estimated_mw: 4.8446035
                 }
               }
               dsu_scu {
-                estimate_mw: 12.503458
+                estimated_mw: 12.503458
               }
             }
           }
diff --git a/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out b/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out
index 44a2490..28464f2 100644
--- a/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out
+++ b/test/trace_processor/diff_tests/metrics/android/wattson_markers_threads.out
@@ -1,655 +1,655 @@
 wattson_markers_threads {
-  metric_version: 1
+  metric_version: 2
   task_info {
-    estimate_mws: 15.333553
-    estimate_mw: 7.546518
+    estimated_mws: 15.333553
+    estimated_mw: 7.546518
     thread_name: "swapper"
     thread_id: 0
     process_id: 0
   }
   task_info {
-    estimate_mws: 11.805121
-    estimate_mw: 5.809974
+    estimated_mws: 11.805121
+    estimated_mw: 5.809974
     thread_name: "RenderThread"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3099
     process_id: 2710
   }
   task_info {
-    estimate_mws: 9.112684
-    estimate_mw: 4.484872
+    estimated_mws: 9.112684
+    estimated_mw: 4.484872
     thread_name: "binder:683_3"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
     thread_id: 816
     process_id: 683
   }
   task_info {
-    estimate_mws: 8.802570
-    estimate_mw: 4.332248
+    estimated_mws: 8.802570
+    estimated_mw: 4.332248
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 742
     process_id: 742
   }
   task_info {
-    estimate_mws: 4.007993
-    estimate_mw: 1.972562
+    estimated_mws: 4.007993
+    estimated_mw: 1.972562
     thread_name: ".wearable.sysui"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2710
     process_id: 2710
   }
   task_info {
-    estimate_mws: 1.779128
-    estimate_mw: 0.875610
+    estimated_mws: 1.779128
+    estimated_mw: 0.875610
     thread_name: "crtc_commit:80"
     process_name: "crtc_commit:80"
     thread_id: 300
     process_id: 300
   }
   task_info {
-    estimate_mws: 1.436499
-    estimate_mw: 0.706983
+    estimated_mws: 1.436499
+    estimated_mw: 0.706983
     thread_name: "binder:2710_E"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 6515
     process_id: 2710
   }
   task_info {
-    estimate_mws: 1.262685
-    estimate_mw: 0.621440
+    estimated_mws: 1.262685
+    estimated_mw: 0.621440
     thread_name: "TimerDispatch"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 819
     process_id: 742
   }
   task_info {
-    estimate_mws: 1.242906
-    estimate_mw: 0.611705
+    estimated_mws: 1.242906
+    estimated_mw: 0.611705
     thread_name: "kworker/u8:4"
     process_name: "kworker/u8:4"
     thread_id: 11407
     process_id: 11407
   }
   task_info {
-    estimate_mws: 1.231494
-    estimate_mw: 0.606089
+    estimated_mws: 1.231494
+    estimated_mw: 0.606089
     thread_name: "BckgrndExec HP"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 837
     process_id: 742
   }
   task_info {
-    estimate_mws: 1.194067
-    estimate_mw: 0.587669
+    estimated_mws: 1.194067
+    estimated_mw: 0.587669
     thread_name: "kworker/u8:5"
     process_name: "kworker/u8:5"
     thread_id: 10610
     process_id: 10610
   }
   task_info {
-    estimate_mws: 1.132809
-    estimate_mw: 0.557520
+    estimated_mws: 1.132809
+    estimated_mw: 0.557520
     thread_name: "binder:742_2"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 791
     process_id: 742
   }
   task_info {
-    estimate_mws: 1.065350
-    estimate_mw: 0.524320
+    estimated_mws: 1.065350
+    estimated_mw: 0.524320
     thread_name: "rcu_preempt"
     process_name: "rcu_preempt"
     thread_id: 14
     process_id: 14
   }
   task_info {
-    estimate_mws: 0.872391
-    estimate_mw: 0.429353
+    estimated_mws: 0.872391
+    estimated_mw: 0.429353
     thread_name: "binder:2710_7"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 5691
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.865098
-    estimate_mw: 0.425764
+    estimated_mws: 0.865098
+    estimated_mw: 0.425764
     thread_name: "traced_probes"
     process_name: "/system/bin/traced_probes"
     thread_id: 916
     process_id: 916
   }
   task_info {
-    estimate_mws: 0.844506
-    estimate_mw: 0.415630
+    estimated_mws: 0.844506
+    estimated_mw: 0.415630
     thread_name: "sleep"
     process_name: "sleep"
     thread_id: 11474
     process_id: 11474
   }
   task_info {
-    estimate_mws: 0.795564
-    estimate_mw: 0.391542
+    estimated_mws: 0.795564
+    estimated_mw: 0.391542
     thread_name: "kgsl_dispatcher"
     process_name: "kgsl_dispatcher"
     thread_id: 122
     process_id: 122
   }
   task_info {
-    estimate_mws: 0.715993
-    estimate_mw: 0.352381
+    estimated_mws: 0.715993
+    estimated_mw: 0.352381
     thread_name: "irq/33-4520300."
     process_name: "irq/33-4520300."
     thread_id: 307
     process_id: 307
   }
   task_info {
-    estimate_mws: 0.715212
-    estimate_mw: 0.351997
+    estimated_mws: 0.715212
+    estimated_mw: 0.351997
     thread_name: "binder:742_1"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 786
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.659174
-    estimate_mw: 0.324417
+    estimated_mws: 0.659174
+    estimated_mw: 0.324417
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 788
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.653970
-    estimate_mw: 0.321856
+    estimated_mws: 0.653970
+    estimated_mw: 0.321856
     thread_name: "app"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 820
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.463974
-    estimate_mw: 0.228348
+    estimated_mws: 0.463974
+    estimated_mw: 0.228348
     thread_name: "rcuog/0"
     process_name: "rcuog/0"
     thread_id: 15
     process_id: 15
   }
   task_info {
-    estimate_mws: 0.434532
-    estimate_mw: 0.213858
+    estimated_mws: 0.434532
+    estimated_mw: 0.213858
     thread_name: "Primes-Jank"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 5094
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.356684
-    estimate_mw: 0.175544
+    estimated_mws: 0.356684
+    estimated_mw: 0.175544
     thread_name: "crtc_event:80"
     process_name: "crtc_event:80"
     thread_id: 301
     process_id: 301
   }
   task_info {
-    estimate_mws: 0.271999
-    estimate_mw: 0.133866
+    estimated_mws: 0.271999
+    estimated_mw: 0.133866
     thread_name: "rcuog/2"
     process_name: "rcuog/2"
     thread_id: 40
     process_id: 40
   }
   task_info {
-    estimate_mws: 0.204649
-    estimate_mw: 0.100719
+    estimated_mws: 0.204649
+    estimated_mw: 0.100719
     thread_name: "binder:2710_2"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3100
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.197450
-    estimate_mw: 0.097176
+    estimated_mws: 0.197450
+    estimated_mw: 0.097176
     thread_name: "FileWatcherThre"
     process_name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
     thread_id: 1544
     process_id: 1529
   }
   task_info {
-    estimate_mws: 0.165350
-    estimate_mw: 0.081378
+    estimated_mws: 0.165350
+    estimated_mw: 0.081378
     thread_name: "rcuop/0"
     process_name: "rcuop/0"
     thread_id: 16
     process_id: 16
   }
   task_info {
-    estimate_mws: 0.123135
-    estimate_mw: 0.060602
+    estimated_mws: 0.123135
+    estimated_mw: 0.060602
     thread_name: "msm_irqbalance"
     process_name: "/vendor/bin/msm_irqbalance"
     thread_id: 3230
     process_id: 3230
   }
   task_info {
-    estimate_mws: 0.122703
-    estimate_mw: 0.060389
+    estimated_mws: 0.122703
+    estimated_mw: 0.060389
     thread_name: "kgsl-events"
     process_name: "kgsl-events"
     thread_id: 120
     process_id: 120
   }
   task_info {
-    estimate_mws: 0.106642
-    estimate_mw: 0.052485
+    estimated_mws: 0.106642
+    estimated_mw: 0.052485
     thread_name: "traced"
     process_name: "/system/bin/traced"
     thread_id: 919
     process_id: 919
   }
   task_info {
-    estimate_mws: 0.104195
-    estimate_mw: 0.051280
+    estimated_mws: 0.104195
+    estimated_mw: 0.051280
     thread_name: "kworker/2:0"
     process_name: "kworker/2:0"
     thread_id: 11444
     process_id: 11444
   }
   task_info {
-    estimate_mws: 0.095284
-    estimate_mw: 0.046894
+    estimated_mws: 0.095284
+    estimated_mw: 0.046894
     thread_name: "rcuop/2"
     process_name: "rcuop/2"
     thread_id: 41
     process_id: 41
   }
   task_info {
-    estimate_mws: 0.084534
-    estimate_mw: 0.041604
+    estimated_mws: 0.084534
+    estimated_mw: 0.041604
     thread_name: "RegSampIdle"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 826
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.076505
-    estimate_mw: 0.037652
+    estimated_mws: 0.076505
+    estimated_mw: 0.037652
     thread_name: "rcuop/1"
     process_name: "rcuop/1"
     thread_id: 32
     process_id: 32
   }
   task_info {
-    estimate_mws: 0.067736
-    estimate_mw: 0.033337
+    estimated_mws: 0.067736
+    estimated_mw: 0.033337
     thread_name: "sh"
     process_name: "/system/bin/sh"
     thread_id: 11472
     process_id: 11472
   }
   task_info {
-    estimate_mws: 0.065940
-    estimate_mw: 0.032453
+    estimated_mws: 0.065940
+    estimated_mw: 0.032453
     thread_name: "BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3524
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.053141
-    estimate_mw: 0.026154
+    estimated_mws: 0.053141
+    estimated_mw: 0.026154
     thread_name: "StateService"
     process_name: "com.google.android.apps.scone"
     thread_id: 3621
     process_id: 3505
   }
   task_info {
-    estimate_mws: 0.052200
-    estimate_mw: 0.025691
+    estimated_mws: 0.052200
+    estimated_mw: 0.025691
     thread_name: "Blocking Thread"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 11310
     process_id: 11279
   }
   task_info {
-    estimate_mws: 0.040767
-    estimate_mw: 0.020064
+    estimated_mws: 0.040767
+    estimated_mw: 0.020064
     thread_name: "kworker/0:1"
     process_name: "kworker/0:1"
     thread_id: 11436
     process_id: 11436
   }
   task_info {
-    estimate_mws: 0.040587
-    estimate_mw: 0.019975
+    estimated_mws: 0.040587
+    estimated_mw: 0.019975
     thread_name: "binder:1629_7"
     process_name: "system_server"
     thread_id: 2635
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.040484
-    estimate_mw: 0.019924
+    estimated_mws: 0.040484
+    estimated_mw: 0.019924
     thread_name: "rcuop/3"
     process_name: "rcuop/3"
     thread_id: 49
     process_id: 49
   }
   task_info {
-    estimate_mws: 0.038016
-    estimate_mw: 0.018710
+    estimated_mws: 0.038016
+    estimated_mw: 0.018710
     thread_name: "atchdog.monitor"
     process_name: "system_server"
     thread_id: 1669
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.036888
-    estimate_mw: 0.018155
+    estimated_mws: 0.036888
+    estimated_mw: 0.018155
     thread_name: "logd.writer"
     process_name: "/system/bin/logd"
     thread_id: 228
     process_id: 213
   }
   task_info {
-    estimate_mws: 0.032972
-    estimate_mw: 0.016227
+    estimated_mws: 0.032972
+    estimated_mw: 0.016227
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 828
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.032239
-    estimate_mw: 0.015867
+    estimated_mws: 0.032239
+    estimated_mw: 0.015867
     thread_name: "it.FitbitMobile"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 11279
     process_id: 11279
   }
   task_info {
-    estimate_mws: 0.031160
-    estimate_mw: 0.015336
+    estimated_mws: 0.031160
+    estimated_mw: 0.015336
     thread_name: "binder:11279_4"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 11426
     process_id: 11279
   }
   task_info {
-    estimate_mws: 0.028389
-    estimate_mw: 0.013972
+    estimated_mws: 0.028389
+    estimated_mw: 0.013972
     thread_name: "irq/207-dwc3"
     process_name: "irq/207-dwc3"
     thread_id: 9733
     process_id: 9733
   }
   task_info {
-    estimate_mws: 0.027208
-    estimate_mw: 0.013391
+    estimated_mws: 0.027208
+    estimated_mw: 0.013391
     thread_name: "UsbFfs-worker"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 9734
     process_id: 5154
   }
   task_info {
-    estimate_mws: 0.024832
-    estimate_mw: 0.012221
+    estimated_mws: 0.024832
+    estimated_mw: 0.012221
     thread_name: "logcat"
     process_name: "logcat"
     thread_id: 1199
     process_id: 1199
   }
   task_info {
-    estimate_mws: 0.023707
-    estimate_mw: 0.011668
+    estimated_mws: 0.023707
+    estimated_mw: 0.011668
     thread_name: "logd.reader.per"
     process_name: "/system/bin/logd"
     thread_id: 1227
     process_id: 213
   }
   task_info {
-    estimate_mws: 0.022160
-    estimate_mw: 0.010906
+    estimated_mws: 0.022160
+    estimated_mw: 0.010906
     thread_name: "kworker/u8:2"
     process_name: "kworker/u8:2"
     thread_id: 11458
     process_id: 11458
   }
   task_info {
-    estimate_mws: 0.019052
-    estimate_mw: 0.009376
+    estimated_mws: 0.019052
+    estimated_mw: 0.009376
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3575
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.018414
-    estimate_mw: 0.009063
+    estimated_mws: 0.018414
+    estimated_mw: 0.009063
     thread_name: "RegionSampling"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 825
     process_id: 742
   }
   task_info {
-    estimate_mws: 0.016701
-    estimate_mw: 0.008220
+    estimated_mws: 0.016701
+    estimated_mw: 0.008220
     thread_name: "halt_drain_rqs"
     process_name: "halt_drain_rqs"
     thread_id: 108
     process_id: 108
   }
   task_info {
-    estimate_mws: 0.011023
-    estimate_mw: 0.005425
+    estimated_mws: 0.011023
+    estimated_mw: 0.005425
     thread_name: "irq/26-4744000."
     process_name: "irq/26-4744000."
     thread_id: 112
     process_id: 112
   }
   task_info {
-    estimate_mws: 0.010004
-    estimate_mw: 0.004924
+    estimated_mws: 0.010004
+    estimated_mw: 0.004924
     thread_name: "migration/2"
     process_name: "migration/2"
     thread_id: 35
     process_id: 35
   }
   task_info {
-    estimate_mws: 0.008819
-    estimate_mw: 0.004341
+    estimated_mws: 0.008819
+    estimated_mw: 0.004341
     thread_name: "ksoftirqd/0"
     process_name: "ksoftirqd/0"
     thread_id: 13
     process_id: 13
   }
   task_info {
-    estimate_mws: 0.007911
-    estimate_mw: 0.003894
+    estimated_mws: 0.007911
+    estimated_mw: 0.003894
     thread_name: "watchdog"
     process_name: "system_server"
     thread_id: 1676
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.007796
-    estimate_mw: 0.003837
+    estimated_mws: 0.007796
+    estimated_mw: 0.003837
     thread_name: "pool-283-thread"
     process_name: "system_server"
     thread_id: 4427
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.007628
-    estimate_mw: 0.003754
+    estimated_mws: 0.007628
+    estimated_mw: 0.003754
     thread_name: "adbd"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5154
     process_id: 5154
   }
   task_info {
-    estimate_mws: 0.006796
-    estimate_mw: 0.003344
+    estimated_mws: 0.006796
+    estimated_mw: 0.003344
     thread_name: "pool-1-thread-1"
     process_name: "system_server"
     thread_id: 2655
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.005691
-    estimate_mw: 0.002801
+    estimated_mws: 0.005691
+    estimated_mw: 0.002801
     thread_name: "pool-1-thread-1"
     process_name: "com.google.android.apps.scone"
     thread_id: 3625
     process_id: 3505
   }
   task_info {
-    estimate_mws: 0.005476
-    estimate_mw: 0.002695
+    estimated_mws: 0.005476
+    estimated_mw: 0.002695
     thread_name: "binder:237_2"
     process_name: "/system/bin/vold"
     thread_id: 237
     process_id: 237
   }
   task_info {
-    estimate_mws: 0.004537
-    estimate_mw: 0.002233
+    estimated_mws: 0.004537
+    estimated_mw: 0.002233
     thread_name: "shell svc 11472"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 11473
     process_id: 5154
   }
   task_info {
-    estimate_mws: 0.003924
-    estimate_mw: 0.001931
+    estimated_mws: 0.003924
+    estimated_mw: 0.001931
     thread_name: "ksoftirqd/1"
     process_name: "ksoftirqd/1"
     thread_id: 29
     process_id: 29
   }
   task_info {
-    estimate_mws: 0.002908
-    estimate_mw: 0.001431
+    estimated_mws: 0.002908
+    estimated_mw: 0.001431
     thread_name: "BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 5230
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.002492
-    estimate_mw: 0.001226
+    estimated_mws: 0.002492
+    estimated_mw: 0.001226
     thread_name: "kworker/3:2"
     process_name: "kworker/3:2"
     thread_id: 9832
     process_id: 9832
   }
   task_info {
-    estimate_mws: 0.002333
-    estimate_mw: 0.001148
+    estimated_mws: 0.002333
+    estimated_mw: 0.001148
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3577
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.002293
-    estimate_mw: 0.001128
+    estimated_mws: 0.002293
+    estimated_mw: 0.001128
     thread_name: "InputReader"
     process_name: "system_server"
     thread_id: 2560
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.002261
-    estimate_mw: 0.001113
+    estimated_mws: 0.002261
+    estimated_mw: 0.001113
     thread_name: "DefaultDispatch"
     process_name: "com.google.android.wearable.media.sessions"
     thread_id: 3618
     process_id: 3553
   }
   task_info {
-    estimate_mws: 0.002226
-    estimate_mw: 0.001095
+    estimated_mws: 0.002226
+    estimated_mw: 0.001095
     thread_name: "migration/3"
     process_name: "migration/3"
     thread_id: 44
     process_id: 44
   }
   task_info {
-    estimate_mws: 0.002100
-    estimate_mw: 0.001034
+    estimated_mws: 0.002100
+    estimated_mw: 0.001034
     thread_name: "InputDispatcher"
     process_name: "system_server"
     thread_id: 2559
     process_id: 1629
   }
   task_info {
-    estimate_mws: 0.001973
-    estimate_mw: 0.000971
+    estimated_mws: 0.001973
+    estimated_mw: 0.000971
     thread_name: "kworker/1:0"
     process_name: "kworker/1:0"
     thread_id: 10984
     process_id: 10984
   }
   task_info {
-    estimate_mws: 0.001966
-    estimate_mw: 0.000967
+    estimated_mws: 0.001966
+    estimated_mw: 0.000967
     thread_name: "irq/193-wdog-ba"
     process_name: "irq/193-wdog-ba"
     thread_id: 344
     process_id: 344
   }
   task_info {
-    estimate_mws: 0.001867
-    estimate_mw: 0.000919
+    estimated_mws: 0.001867
+    estimated_mw: 0.000919
     thread_name: "DefaultDispatch"
     process_name: "com.google.android.wearable.media.sessions"
     thread_id: 3615
     process_id: 3553
   }
   task_info {
-    estimate_mws: 0.001867
-    estimate_mw: 0.000919
+    estimated_mws: 0.001867
+    estimated_mw: 0.000919
     thread_name: "irq/25-mmc0"
     process_name: "irq/25-mmc0"
     thread_id: 115
     process_id: 115
   }
   task_info {
-    estimate_mws: 0.001728
-    estimate_mw: 0.000851
+    estimated_mws: 0.001728
+    estimated_mw: 0.000851
     thread_name: "iou-wrk-214"
     process_name: "/system/bin/lmkd"
     thread_id: 11440
     process_id: 214
   }
   task_info {
-    estimate_mws: 0.001600
-    estimate_mw: 0.000787
+    estimated_mws: 0.001600
+    estimated_mw: 0.000787
     thread_name: "DefaultDispatch"
     process_name: "com.google.android.wearable.media.sessions"
     thread_id: 3616
     process_id: 3553
   }
   task_info {
-    estimate_mws: 0.001393
-    estimate_mw: 0.000686
+    estimated_mws: 0.001393
+    estimated_mw: 0.000686
     thread_name: "kworker/u8:1"
     process_name: "kworker/u8:1"
     thread_id: 11185
     process_id: 11185
   }
   task_info {
-    estimate_mws: 0.001373
-    estimate_mw: 0.000676
+    estimated_mws: 0.001373
+    estimated_mw: 0.000676
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3576
     process_id: 2710
   }
   task_info {
-    estimate_mws: 0.000811
-    estimate_mw: 0.000399
+    estimated_mws: 0.000811
+    estimated_mw: 0.000399
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3622
diff --git a/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out b/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
index 77fe7cd..76b3c98 100644
--- a/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
+++ b/test/trace_processor/diff_tests/metrics/android/wattson_trace_threads.out
@@ -1,3835 +1,3835 @@
 wattson_trace_threads {
-  metric_version: 1
+  metric_version: 2
   task_info {
-    estimate_mws: 34.415016
-    estimate_mw: 3.979049
+    estimated_mws: 34.415016
+    estimated_mw: 3.979049
     thread_name: "swapper"
     thread_id: 0
     process_id: 0
   }
   task_info {
-    estimate_mws: 19.853703
-    estimate_mw: 2.295476
+    estimated_mws: 19.853703
+    estimated_mw: 2.295476
     thread_name: "RenderThread"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1986
     process_id: 1926
   }
   task_info {
-    estimate_mws: 17.530441
-    estimate_mw: 2.026862
+    estimated_mws: 17.530441
+    estimated_mw: 2.026862
     thread_name: "Jit thread pool"
     process_name: "system_server"
     thread_id: 1344
     process_id: 1302
   }
   task_info {
-    estimate_mws: 16.980274
-    estimate_mw: 1.963252
+    estimated_mws: 16.980274
+    estimated_mw: 1.963252
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 755
     process_id: 755
   }
   task_info {
-    estimate_mws: 14.908094
-    estimate_mw: 1.723667
+    estimated_mws: 14.908094
+    estimated_mw: 1.723667
     thread_name: ".wearable.sysui"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1926
     process_id: 1926
   }
   task_info {
-    estimate_mws: 13.373355
-    estimate_mw: 1.546221
+    estimated_mws: 13.373355
+    estimated_mw: 1.546221
     thread_name: "binder:685_3"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
     thread_id: 804
     process_id: 685
   }
   task_info {
-    estimate_mws: 6.747261
-    estimate_mw: 0.780115
+    estimated_mws: 6.747261
+    estimated_mw: 0.780115
     thread_name: "binder:1302_7"
     process_name: "system_server"
     thread_id: 1671
     process_id: 1302
   }
   task_info {
-    estimate_mws: 6.504173
-    estimate_mw: 0.752010
+    estimated_mws: 6.504173
+    estimated_mw: 0.752010
     thread_name: "binder:1302_A"
     process_name: "system_server"
     thread_id: 2015
     process_id: 1302
   }
   task_info {
-    estimate_mws: 4.858775
-    estimate_mw: 0.561769
+    estimated_mws: 4.858775
+    estimated_mw: 0.561769
     thread_name: "android.anim"
     process_name: "system_server"
     thread_id: 1419
     process_id: 1302
   }
   task_info {
-    estimate_mws: 4.769800
-    estimate_mw: 0.551482
+    estimated_mws: 4.769800
+    estimated_mw: 0.551482
     thread_name: "RenderEngine"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 788
     process_id: 755
   }
   task_info {
-    estimate_mws: 4.672233
-    estimate_mw: 0.540201
+    estimated_mws: 4.672233
+    estimated_mw: 0.540201
     thread_name: "kswapd0"
     process_name: "kswapd0"
     thread_id: 63
     process_id: 63
   }
   task_info {
-    estimate_mws: 4.314495
-    estimate_mw: 0.498840
+    estimated_mws: 4.314495
+    estimated_mw: 0.498840
     thread_name: "lowpool[2]"
     process_name: "com.google.android.gms"
     thread_id: 3525
     process_id: 2856
   }
   task_info {
-    estimate_mws: 4.117818
-    estimate_mw: 0.476100
+    estimated_mws: 4.117818
+    estimated_mw: 0.476100
     thread_name: "logd.writer"
     process_name: "/system/bin/logd"
     thread_id: 221
     process_id: 211
   }
   task_info {
-    estimate_mws: 4.108276
-    estimate_mw: 0.474997
+    estimated_mws: 4.108276
+    estimated_mw: 0.474997
     thread_name: "binder:1302_17"
     process_name: "system_server"
     thread_id: 5202
     process_id: 1302
   }
   task_info {
-    estimate_mws: 3.723955
-    estimate_mw: 0.430562
+    estimated_mws: 3.723955
+    estimated_mw: 0.430562
     thread_name: "binder:1302_6"
     process_name: "system_server"
     thread_id: 1662
     process_id: 1302
   }
   task_info {
-    estimate_mws: 3.666289
-    estimate_mw: 0.423895
+    estimated_mws: 3.666289
+    estimated_mw: 0.423895
     thread_name: "e.watchface.rwf"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 1999
     process_id: 1999
   }
   task_info {
-    estimate_mws: 3.524869
-    estimate_mw: 0.407544
+    estimated_mws: 3.524869
+    estimated_mw: 0.407544
     thread_name: "killall"
     process_name: "/system/bin/sh"
     thread_id: 5620
     process_id: 5620
   }
   task_info {
-    estimate_mws: 3.495762
-    estimate_mw: 0.404178
+    estimated_mws: 3.495762
+    estimated_mw: 0.404178
     thread_name: "CachedAppOptimi"
     process_name: "system_server"
     thread_id: 1773
     process_id: 1302
   }
   task_info {
-    estimate_mws: 3.459922
-    estimate_mw: 0.400035
+    estimated_mws: 3.459922
+    estimated_mw: 0.400035
     thread_name: "logcat"
     process_name: "logcat"
     thread_id: 1230
     process_id: 1230
   }
   task_info {
-    estimate_mws: 3.429554
-    estimate_mw: 0.396524
+    estimated_mws: 3.429554
+    estimated_mw: 0.396524
     thread_name: "system_server"
     process_name: "system_server"
     thread_id: 1302
     process_id: 1302
   }
   task_info {
-    estimate_mws: 3.300661
-    estimate_mw: 0.381621
+    estimated_mws: 3.300661
+    estimated_mw: 0.381621
     thread_name: "crtc_commit:80"
     process_name: "crtc_commit:80"
     thread_id: 244
     process_id: 244
   }
   task_info {
-    estimate_mws: 3.194881
-    estimate_mw: 0.369391
+    estimated_mws: 3.194881
+    estimated_mw: 0.369391
     thread_name: "InputDispatcher"
     process_name: "system_server"
     thread_id: 1783
     process_id: 1302
   }
   task_info {
-    estimate_mws: 3.011913
-    estimate_mw: 0.348236
+    estimated_mws: 3.011913
+    estimated_mw: 0.348236
     thread_name: "binder:755_1"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 782
     process_id: 755
   }
   task_info {
-    estimate_mws: 3.006022
-    estimate_mw: 0.347555
+    estimated_mws: 3.006022
+    estimated_mw: 0.347555
     thread_name: "android.display"
     process_name: "system_server"
     thread_id: 1418
     process_id: 1302
   }
   task_info {
-    estimate_mws: 2.856301
-    estimate_mw: 0.330244
+    estimated_mws: 2.856301
+    estimated_mw: 0.330244
     thread_name: "binder:524_2"
     process_name: "/vendor/bin/mcu_mgmtd"
     thread_id: 524
     process_id: 524
   }
   task_info {
-    estimate_mws: 2.712443
-    estimate_mw: 0.313611
+    estimated_mws: 2.712443
+    estimated_mw: 0.313611
     thread_name: "traced_probes"
     process_name: "/system/bin/traced_probes"
     thread_id: 904
     process_id: 904
   }
   task_info {
-    estimate_mws: 2.550916
-    estimate_mw: 0.294936
+    estimated_mws: 2.550916
+    estimated_mw: 0.294936
     thread_name: "kworker/u8:0"
     process_name: "kworker/u8:0"
     thread_id: 8
     process_id: 8
   }
   task_info {
-    estimate_mws: 2.487099
-    estimate_mw: 0.287557
+    estimated_mws: 2.487099
+    estimated_mw: 0.287557
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 883
     process_id: 755
   }
   task_info {
-    estimate_mws: 2.386123
-    estimate_mw: 0.275883
+    estimated_mws: 2.386123
+    estimated_mw: 0.275883
     thread_name: "binder:1302_15"
     process_name: "system_server"
     thread_id: 3754
     process_id: 1302
   }
   task_info {
-    estimate_mws: 2.258779
-    estimate_mw: 0.261159
+    estimated_mws: 2.258779
+    estimated_mw: 0.261159
     thread_name: "logd.reader.per"
     process_name: "/system/bin/logd"
     thread_id: 1274
     process_id: 211
   }
   task_info {
-    estimate_mws: 2.171289
-    estimate_mw: 0.251044
+    estimated_mws: 2.171289
+    estimated_mw: 0.251044
     thread_name: "RenderThread"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 2301
     process_id: 1999
   }
   task_info {
-    estimate_mws: 2.143151
-    estimate_mw: 0.247790
+    estimated_mws: 2.143151
+    estimated_mw: 0.247790
     thread_name: "InputReader"
     process_name: "system_server"
     thread_id: 1784
     process_id: 1302
   }
   task_info {
-    estimate_mws: 2.091430
-    estimate_mw: 0.241810
+    estimated_mws: 2.091430
+    estimated_mw: 0.241810
     thread_name: "rcu_preempt"
     process_name: "rcu_preempt"
     thread_id: 14
     process_id: 14
   }
   task_info {
-    estimate_mws: 2.048920
-    estimate_mw: 0.236895
+    estimated_mws: 2.048920
+    estimated_mw: 0.236895
     thread_name: "binder:1926_4"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2262
     process_id: 1926
   }
   task_info {
-    estimate_mws: 1.914560
-    estimate_mw: 0.221361
+    estimated_mws: 1.914560
+    estimated_mw: 0.221361
     thread_name: "arable.systemui"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2171
     process_id: 2171
   }
   task_info {
-    estimate_mws: 1.854433
-    estimate_mw: 0.214409
+    estimated_mws: 1.854433
+    estimated_mw: 0.214409
     thread_name: "android.ui"
     process_name: "system_server"
     thread_id: 1416
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.777087
-    estimate_mw: 0.205466
+    estimated_mws: 1.777087
+    estimated_mw: 0.205466
     thread_name: "kworker/u8:4"
     process_name: "kworker/u8:4"
     thread_id: 431
     process_id: 431
   }
   task_info {
-    estimate_mws: 1.773777
-    estimate_mw: 0.205083
+    estimated_mws: 1.773777
+    estimated_mw: 0.205083
     thread_name: "TimerDispatch"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 865
     process_id: 755
   }
   task_info {
-    estimate_mws: 1.760400
-    estimate_mw: 0.203537
+    estimated_mws: 1.760400
+    estimated_mw: 0.203537
     thread_name: "ActivityManager"
     process_name: "system_server"
     thread_id: 1431
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.733169
-    estimate_mw: 0.200388
+    estimated_mws: 1.733169
+    estimated_mw: 0.200388
     thread_name: "PowerManagerSer"
     process_name: "system_server"
     thread_id: 1506
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.639501
-    estimate_mw: 0.189558
+    estimated_mws: 1.639501
+    estimated_mw: 0.189558
     thread_name: "WifiHandlerThre"
     process_name: "system_server"
     thread_id: 1818
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.631037
-    estimate_mw: 0.188580
+    estimated_mws: 1.631037
+    estimated_mw: 0.188580
     thread_name: "binder:755_5"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 1987
     process_id: 755
   }
   task_info {
-    estimate_mws: 1.605931
-    estimate_mw: 0.185677
+    estimated_mws: 1.605931
+    estimated_mw: 0.185677
     thread_name: "kgsl_dispatcher"
     process_name: "kgsl_dispatcher"
     thread_id: 111
     process_id: 111
   }
   task_info {
-    estimate_mws: 1.564964
-    estimate_mw: 0.180940
+    estimated_mws: 1.564964
+    estimated_mw: 0.180940
     thread_name: "binder:1302_8"
     process_name: "system_server"
     thread_id: 1679
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.476619
-    estimate_mw: 0.170726
+    estimated_mws: 1.476619
+    estimated_mw: 0.170726
     thread_name: "lowpool[5]"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3489
     process_id: 1949
   }
   task_info {
-    estimate_mws: 1.470155
-    estimate_mw: 0.169979
+    estimated_mws: 1.470155
+    estimated_mw: 0.169979
     thread_name: "-Executor] idle"
     process_name: "com.google.android.gms"
     thread_id: 5591
     process_id: 2856
   }
   task_info {
-    estimate_mws: 1.469958
-    estimate_mw: 0.169956
+    estimated_mws: 1.469958
+    estimated_mw: 0.169956
     thread_name: "binder:1302_B"
     process_name: "system_server"
     thread_id: 2033
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.390635
-    estimate_mw: 0.160785
+    estimated_mws: 1.390635
+    estimated_mw: 0.160785
     thread_name: "binder:755_4"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 1125
     process_id: 755
   }
   task_info {
-    estimate_mws: 1.327049
-    estimate_mw: 0.153433
+    estimated_mws: 1.327049
+    estimated_mw: 0.153433
     thread_name: "batterystats-ha"
     process_name: "system_server"
     thread_id: 1484
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.312721
-    estimate_mw: 0.151776
+    estimated_mws: 1.312721
+    estimated_mw: 0.151776
     thread_name: "statsd.writer"
     process_name: "/apex/com.android.os.statsd/bin/statsd"
     thread_id: 980
     process_id: 545
   }
   task_info {
-    estimate_mws: 1.252738
-    estimate_mw: 0.144841
+    estimated_mws: 1.252738
+    estimated_mw: 0.144841
     thread_name: "kworker/u8:2"
     process_name: "kworker/u8:2"
     thread_id: 62
     process_id: 62
   }
   task_info {
-    estimate_mws: 1.251944
-    estimate_mw: 0.144749
+    estimated_mws: 1.251944
+    estimated_mw: 0.144749
     thread_name: "app"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 867
     process_id: 755
   }
   task_info {
-    estimate_mws: 1.233674
-    estimate_mw: 0.142637
+    estimated_mws: 1.233674
+    estimated_mw: 0.142637
     thread_name: "system_server"
     process_name: "system_server"
     thread_id: 1343
     process_id: 1302
   }
   task_info {
-    estimate_mws: 1.228813
-    estimate_mw: 0.142075
+    estimated_mws: 1.228813
+    estimated_mw: 0.142075
     thread_name: "irq/33-4520300."
     process_name: "irq/33-4520300.qcom,bwmon-ddr"
     thread_id: 95
     process_id: 95
   }
   task_info {
-    estimate_mws: 1.068197
-    estimate_mw: 0.123504
+    estimated_mws: 1.068197
+    estimated_mw: 0.123504
     thread_name: "logd.klogd"
     process_name: "/system/bin/logd"
     thread_id: 234
     process_id: 211
   }
   task_info {
-    estimate_mws: 1.007070
-    estimate_mw: 0.116437
+    estimated_mws: 1.007070
+    estimated_mw: 0.116437
     thread_name: "android.fg"
     process_name: "system_server"
     thread_id: 1415
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.969980
-    estimate_mw: 0.112149
+    estimated_mws: 0.969980
+    estimated_mw: 0.112149
     thread_name: "rcuog/0"
     process_name: "rcuog/0"
     thread_id: 15
     process_id: 15
   }
   task_info {
-    estimate_mws: 0.952077
-    estimate_mw: 0.110079
+    estimated_mws: 0.952077
+    estimated_mw: 0.110079
     thread_name: "binder:1926_3"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1940
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.946746
-    estimate_mw: 0.109462
+    estimated_mws: 0.946746
+    estimated_mw: 0.109462
     thread_name: "gle.android.gms"
     process_name: "com.google.android.gms"
     thread_id: 2856
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.930774
-    estimate_mw: 0.107616
+    estimated_mws: 0.930774
+    estimated_mw: 0.107616
     thread_name: "crtc_event:80"
     process_name: "crtc_event:80"
     thread_id: 245
     process_id: 245
   }
   task_info {
-    estimate_mws: 0.907425
-    estimate_mw: 0.104916
+    estimated_mws: 0.907425
+    estimated_mw: 0.104916
     thread_name: "binder:755_3"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 1124
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.897620
-    estimate_mw: 0.103782
+    estimated_mws: 0.897620
+    estimated_mw: 0.103782
     thread_name: "init"
     process_name: "/system/bin/init"
     thread_id: 143
     process_id: 1
   }
   task_info {
-    estimate_mws: 0.880853
-    estimate_mw: 0.101844
+    estimated_mws: 0.880853
+    estimated_mw: 0.101844
     thread_name: "wmshell.main"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2260
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.870598
-    estimate_mw: 0.100658
+    estimated_mws: 0.870598
+    estimated_mw: 0.100658
     thread_name: "Primes-1"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1944
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.847641
-    estimate_mw: 0.098004
+    estimated_mws: 0.847641
+    estimated_mw: 0.098004
     thread_name: "init"
     process_name: "/system/bin/init"
     thread_id: 1
     process_id: 1
   }
   task_info {
-    estimate_mws: 0.846054
-    estimate_mw: 0.097820
+    estimated_mws: 0.846054
+    estimated_mw: 0.097820
     thread_name: "binder:1302_D"
     process_name: "system_server"
     thread_id: 2043
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.844958
-    estimate_mw: 0.097694
+    estimated_mws: 0.844958
+    estimated_mw: 0.097694
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 786
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.833920
-    estimate_mw: 0.096417
+    estimated_mws: 0.833920
+    estimated_mw: 0.096417
     thread_name: "kworker/u8:5"
     process_name: "kworker/u8:5"
     thread_id: 5304
     process_id: 5304
   }
   task_info {
-    estimate_mws: 0.780835
-    estimate_mw: 0.090280
+    estimated_mws: 0.780835
+    estimated_mw: 0.090280
     thread_name: "kworker/2:4"
     process_name: "kworker/2:4"
     thread_id: 4995
     process_id: 4995
   }
   task_info {
-    estimate_mws: 0.747755
-    estimate_mw: 0.086455
+    estimated_mws: 0.747755
+    estimated_mw: 0.086455
     thread_name: "binder:2171_4"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2374
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.746488
-    estimate_mw: 0.086309
+    estimated_mws: 0.746488
+    estimated_mw: 0.086309
     thread_name: "binder:1999_5"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 3678
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.744159
-    estimate_mw: 0.086039
+    estimated_mws: 0.744159
+    estimated_mw: 0.086039
     thread_name: "servicemanager"
     process_name: "/system/bin/servicemanager"
     thread_id: 213
     process_id: 213
   }
   task_info {
-    estimate_mws: 0.717520
-    estimate_mw: 0.082959
+    estimated_mws: 0.717520
+    estimated_mw: 0.082959
     thread_name: "wmshell.anim"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2269
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.699681
-    estimate_mw: 0.080897
+    estimated_mws: 0.699681
+    estimated_mw: 0.080897
     thread_name: "GoogleApiHandle"
     process_name: "com.google.android.gms"
     thread_id: 3208
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.675997
-    estimate_mw: 0.078159
+    estimated_mws: 0.675997
+    estimated_mw: 0.078159
     thread_name: "binder:1302_4"
     process_name: "system_server"
     thread_id: 1592
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.647999
-    estimate_mw: 0.074921
+    estimated_mws: 0.647999
+    estimated_mw: 0.074921
     thread_name: "batterystats-wo"
     process_name: "system_server"
     thread_id: 1487
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.640576
-    estimate_mw: 0.074063
+    estimated_mws: 0.640576
+    estimated_mw: 0.074063
     thread_name: ".gms.persistent"
     process_name: "com.google.android.gms.persistent"
     thread_id: 1949
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.631830
-    estimate_mw: 0.073052
+    estimated_mws: 0.631830
+    estimated_mw: 0.073052
     thread_name: "binder:1926_6"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 5211
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.627672
-    estimate_mw: 0.072571
+    estimated_mws: 0.627672
+    estimated_mw: 0.072571
     thread_name: "DisplayOffloadB"
     process_name: "system_server"
     thread_id: 1512
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.627487
-    estimate_mw: 0.072550
+    estimated_mws: 0.627487
+    estimated_mw: 0.072550
     thread_name: "binder:682_2"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
     thread_id: 682
     process_id: 682
   }
   task_info {
-    estimate_mws: 0.624294
-    estimate_mw: 0.072181
+    estimated_mws: 0.624294
+    estimated_mw: 0.072181
     thread_name: "rcuog/2"
     process_name: "rcuog/2"
     thread_id: 37
     process_id: 37
   }
   task_info {
-    estimate_mws: 0.623909
-    estimate_mw: 0.072136
+    estimated_mws: 0.623909
+    estimated_mw: 0.072136
     thread_name: "kworker/0:6"
     process_name: "kworker/0:6"
     thread_id: 586
     process_id: 586
   }
   task_info {
-    estimate_mws: 0.597177
-    estimate_mw: 0.069045
+    estimated_mws: 0.597177
+    estimated_mw: 0.069045
     thread_name: "diag-router"
     process_name: "/vendor/bin/diag-router"
     thread_id: 634
     process_id: 634
   }
   task_info {
-    estimate_mws: 0.582498
-    estimate_mw: 0.067348
+    estimated_mws: 0.582498
+    estimated_mw: 0.067348
     thread_name: "HeapTaskDaemon"
     process_name: "com.google.android.gms"
     thread_id: 2882
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.579675
-    estimate_mw: 0.067022
+    estimated_mws: 0.579675
+    estimated_mw: 0.067022
     thread_name: "FileWatcherThre"
     process_name: "/vendor/bin/hw/android.hardware.thermal-service.pixel"
     thread_id: 1411
     process_id: 1404
   }
   task_info {
-    estimate_mws: 0.568415
-    estimate_mw: 0.065720
+    estimated_mws: 0.568415
+    estimated_mw: 0.065720
     thread_name: "TaskSnapshotPer"
     process_name: "system_server"
     thread_id: 1913
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.565566
-    estimate_mw: 0.065390
+    estimated_mws: 0.565566
+    estimated_mw: 0.065390
     thread_name: "lmkd"
     process_name: "/system/bin/lmkd"
     thread_id: 212
     process_id: 212
   }
   task_info {
-    estimate_mws: 0.554734
-    estimate_mw: 0.064138
+    estimated_mws: 0.554734
+    estimated_mw: 0.064138
     thread_name: "binder:1949_8"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3269
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.517529
-    estimate_mw: 0.059836
+    estimated_mws: 0.517529
+    estimated_mw: 0.059836
     thread_name: "appSf"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 868
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.514221
-    estimate_mw: 0.059454
+    estimated_mws: 0.514221
+    estimated_mw: 0.059454
     thread_name: "kworker/1:1"
     process_name: "kworker/1:1"
     thread_id: 47
     process_id: 47
   }
   task_info {
-    estimate_mws: 0.507581
-    estimate_mw: 0.058686
+    estimated_mws: 0.507581
+    estimated_mw: 0.058686
     thread_name: "android.hardwar"
     process_name: "/vendor/bin/hw/android.hardware.usb-service.qti"
     thread_id: 1861
     process_id: 665
   }
   task_info {
-    estimate_mws: 0.504068
-    estimate_mw: 0.058280
+    estimated_mws: 0.504068
+    estimated_mw: 0.058280
     thread_name: "Primes-Jank"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2389
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.493578
-    estimate_mw: 0.057067
+    estimated_mws: 0.493578
+    estimated_mw: 0.057067
     thread_name: "binder:2171_3"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2235
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.490345
-    estimate_mw: 0.056694
+    estimated_mws: 0.490345
+    estimated_mw: 0.056694
     thread_name: "traced"
     process_name: "/system/bin/traced"
     thread_id: 905
     process_id: 905
   }
   task_info {
-    estimate_mws: 0.468415
-    estimate_mw: 0.054158
+    estimated_mws: 0.468415
+    estimated_mw: 0.054158
     thread_name: "eduling.default"
     process_name: "system_server"
     thread_id: 1761
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.462913
-    estimate_mw: 0.053522
+    estimated_mws: 0.462913
+    estimated_mw: 0.053522
     thread_name: "binder:545_2"
     process_name: "/apex/com.android.os.statsd/bin/statsd"
     thread_id: 553
     process_id: 545
   }
   task_info {
-    estimate_mws: 0.462537
-    estimate_mw: 0.053478
+    estimated_mws: 0.462537
+    estimated_mw: 0.053478
     thread_name: "User"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2234
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.454063
-    estimate_mw: 0.052499
+    estimated_mws: 0.454063
+    estimated_mw: 0.052499
     thread_name: "putmethod.latin"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 4997
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.450612
-    estimate_mw: 0.052100
+    estimated_mws: 0.450612
+    estimated_mw: 0.052100
     thread_name: "ueventd"
     process_name: "/system/bin/ueventd"
     thread_id: 145
     process_id: 145
   }
   task_info {
-    estimate_mws: 0.448044
-    estimate_mw: 0.051803
+    estimated_mws: 0.448044
+    estimated_mw: 0.051803
     thread_name: "wpa_supplicant"
     process_name: "/vendor/bin/hw/wpa_supplicant"
     thread_id: 5214
     process_id: 5214
   }
   task_info {
-    estimate_mws: 0.431304
-    estimate_mw: 0.049867
+    estimated_mws: 0.431304
+    estimated_mw: 0.049867
     thread_name: "rcuop/0"
     process_name: "rcuop/0"
     thread_id: 16
     process_id: 16
   }
   task_info {
-    estimate_mws: 0.416635
-    estimate_mw: 0.048171
+    estimated_mws: 0.416635
+    estimated_mw: 0.048171
     thread_name: "Jit thread pool"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1933
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.404592
-    estimate_mw: 0.046779
+    estimated_mws: 0.404592
+    estimated_mw: 0.046779
     thread_name: "pixelstats-vend"
     process_name: "/vendor/bin/pixelstats-vendor"
     thread_id: 267
     process_id: 255
   }
   task_info {
-    estimate_mws: 0.396838
-    estimate_mw: 0.045882
+    estimated_mws: 0.396838
+    estimated_mw: 0.045882
     thread_name: "irq/236-NVT-ts"
     process_name: "irq/236-NVT-ts"
     thread_id: 505
     process_id: 505
   }
   task_info {
-    estimate_mws: 0.393249
-    estimate_mw: 0.045467
+    estimated_mws: 0.393249
+    estimated_mw: 0.045467
     thread_name: "nanohub"
     process_name: "nanohub"
     thread_id: 297
     process_id: 297
   }
   task_info {
-    estimate_mws: 0.376686
-    estimate_mw: 0.043552
+    estimated_mws: 0.376686
+    estimated_mw: 0.043552
     thread_name: "android.bg"
     process_name: "system_server"
     thread_id: 1430
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.375869
-    estimate_mw: 0.043458
+    estimated_mws: 0.375869
+    estimated_mw: 0.043458
     thread_name: "chre"
     process_name: "/vendor/bin/chre"
     thread_id: 1041
     process_id: 1041
   }
   task_info {
-    estimate_mws: 0.373520
-    estimate_mw: 0.043186
+    estimated_mws: 0.373520
+    estimated_mw: 0.043186
     thread_name: "lowpool[1]"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2279
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.366001
-    estimate_mw: 0.042317
+    estimated_mws: 0.366001
+    estimated_mw: 0.042317
     thread_name: "TracingMuxer"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 783
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.359494
-    estimate_mw: 0.041565
+    estimated_mws: 0.359494
+    estimated_mw: 0.041565
     thread_name: "kgsl-events"
     process_name: "kgsl-events"
     thread_id: 109
     process_id: 109
   }
   task_info {
-    estimate_mws: 0.359130
-    estimate_mw: 0.041522
+    estimated_mws: 0.359130
+    estimated_mw: 0.041522
     thread_name: "IpClient.wlan0"
     process_name: "com.android.networkstack.process"
     thread_id: 5216
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.346517
-    estimate_mw: 0.040064
+    estimated_mws: 0.346517
+    estimated_mw: 0.040064
     thread_name: "binder:257_5"
     process_name: "/system/bin/hw/android.system.suspend-service"
     thread_id: 1491
     process_id: 257
   }
   task_info {
-    estimate_mws: 0.341200
-    estimate_mw: 0.039449
+    estimated_mws: 0.341200
+    estimated_mw: 0.039449
     thread_name: "binder:1901_3"
     process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
     thread_id: 1905
     process_id: 1901
   }
   task_info {
-    estimate_mws: 0.335534
-    estimate_mw: 0.038794
+    estimated_mws: 0.335534
+    estimated_mw: 0.038794
     thread_name: "binder:740_1"
     process_name: "/system/bin/audioserver"
     thread_id: 821
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.331405
-    estimate_mw: 0.038317
+    estimated_mws: 0.331405
+    estimated_mw: 0.038317
     thread_name: "BG"
     process_name: "com.google.wear.services"
     thread_id: 2023
     process_id: 1948
   }
   task_info {
-    estimate_mws: 0.326344
-    estimate_mw: 0.037732
+    estimated_mws: 0.326344
+    estimated_mw: 0.037732
     thread_name: "kworker/0:5H"
     process_name: "kworker/0:5H"
     thread_id: 1337
     process_id: 1337
   }
   task_info {
-    estimate_mws: 0.322384
-    estimate_mw: 0.037274
+    estimated_mws: 0.322384
+    estimated_mw: 0.037274
     thread_name: "binder:755_2"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 784
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.319511
-    estimate_mw: 0.036942
+    estimated_mws: 0.319511
+    estimated_mw: 0.036942
     thread_name: "audioserver"
     process_name: "/system/bin/audioserver"
     thread_id: 740
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.310996
-    estimate_mw: 0.035957
+    estimated_mws: 0.310996
+    estimated_mw: 0.035957
     thread_name: "binder:1949_2"
     process_name: "com.google.android.gms.persistent"
     thread_id: 1978
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.302115
-    estimate_mw: 0.034930
+    estimated_mws: 0.302115
+    estimated_mw: 0.034930
     thread_name: "-Executor] idle"
     process_name: "com.google.android.gms.persistent"
     thread_id: 5602
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.301578
-    estimate_mw: 0.034868
+    estimated_mws: 0.301578
+    estimated_mw: 0.034868
     thread_name: "pool-11-thread-"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3329
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.299169
-    estimate_mw: 0.034590
+    estimated_mws: 0.299169
+    estimated_mw: 0.034590
     thread_name: "android.io"
     process_name: "system_server"
     thread_id: 1417
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.296825
-    estimate_mw: 0.034319
+    estimated_mws: 0.296825
+    estimated_mw: 0.034319
     thread_name: "binder:1901_3"
     process_name: "/vendor/bin/hw/android.hardware.wifi-service-lazy"
     thread_id: 5205
     process_id: 1901
   }
   task_info {
-    estimate_mws: 0.294242
-    estimate_mw: 0.034020
+    estimated_mws: 0.294242
+    estimated_mw: 0.034020
     thread_name: "rcuop/1"
     process_name: "rcuop/1"
     thread_id: 30
     process_id: 30
   }
   task_info {
-    estimate_mws: 0.286642
-    estimate_mw: 0.033141
+    estimated_mws: 0.286642
+    estimated_mw: 0.033141
     thread_name: "binder:1948_6"
     process_name: "com.google.wear.services"
     thread_id: 5315
     process_id: 1948
   }
   task_info {
-    estimate_mws: 0.285983
-    estimate_mw: 0.033065
+    estimated_mws: 0.285983
+    estimated_mw: 0.033065
     thread_name: "AssistantHandle"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4081
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.283378
-    estimate_mw: 0.032764
+    estimated_mws: 0.283378
+    estimated_mw: 0.032764
     thread_name: "binder:1999_1"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 2016
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.279959
-    estimate_mw: 0.032369
+    estimated_mws: 0.279959
+    estimated_mw: 0.032369
     thread_name: "binder:2182_7"
     process_name: "com.android.phone"
     thread_id: 2694
     process_id: 2182
   }
   task_info {
-    estimate_mws: 0.279816
-    estimate_mw: 0.032352
+    estimated_mws: 0.279816
+    estimated_mw: 0.032352
     thread_name: "kworker/3:2H"
     process_name: "kworker/3:2H"
     thread_id: 226
     process_id: 226
   }
   task_info {
-    estimate_mws: 0.277230
-    estimate_mw: 0.032053
+    estimated_mws: 0.277230
+    estimated_mw: 0.032053
     thread_name: "BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 3005
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.274735
-    estimate_mw: 0.031765
+    estimated_mws: 0.274735
+    estimated_mw: 0.031765
     thread_name: "lowpool[3]"
     process_name: "com.google.android.gms"
     thread_id: 3527
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.267749
-    estimate_mw: 0.030957
+    estimated_mws: 0.267749
+    estimated_mw: 0.030957
     thread_name: "hvdcp_opti"
     process_name: "/vendor/bin/hvdcp_opti"
     thread_id: 1276
     process_id: 1270
   }
   task_info {
-    estimate_mws: 0.262081
-    estimate_mw: 0.030302
+    estimated_mws: 0.262081
+    estimated_mw: 0.030302
     thread_name: "binder:1926_3"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2022
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.259248
-    estimate_mw: 0.029974
+    estimated_mws: 0.259248
+    estimated_mw: 0.029974
     thread_name: "kworker/3:5"
     process_name: "kworker/3:5"
     thread_id: 104
     process_id: 104
   }
   task_info {
-    estimate_mws: 0.256714
-    estimate_mw: 0.029681
+    estimated_mws: 0.256714
+    estimated_mw: 0.029681
     thread_name: "binder:257_2"
     process_name: "/system/bin/hw/android.system.suspend-service"
     thread_id: 264
     process_id: 257
   }
   task_info {
-    estimate_mws: 0.247037
-    estimate_mw: 0.028562
+    estimated_mws: 0.247037
+    estimated_mw: 0.028562
     thread_name: "SDM_EventThread"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
     thread_id: 727
     process_id: 685
   }
   task_info {
-    estimate_mws: 0.244112
-    estimate_mw: 0.028224
+    estimated_mws: 0.244112
+    estimated_mw: 0.028224
     thread_name: "POSIX timer 2"
     process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
     thread_id: 1600
     process_id: 664
   }
   task_info {
-    estimate_mws: 0.242754
-    estimate_mw: 0.028067
+    estimated_mws: 0.242754
+    estimated_mw: 0.028067
     thread_name: "binder:2856_4"
     process_name: "com.google.android.gms"
     thread_id: 3679
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.241348
-    estimate_mw: 0.027905
+    estimated_mws: 0.241348
+    estimated_mw: 0.027905
     thread_name: "pool-2-thread-1"
     process_name: "com.android.networkstack.process"
     thread_id: 2416
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.231145
-    estimate_mw: 0.026725
+    estimated_mws: 0.231145
+    estimated_mw: 0.026725
     thread_name: "rcuop/3"
     process_name: "rcuop/3"
     thread_id: 45
     process_id: 45
   }
   task_info {
-    estimate_mws: 0.230341
-    estimate_mw: 0.026632
+    estimated_mws: 0.230341
+    estimated_mw: 0.026632
     thread_name: "f2fs_ckpt-254:4"
     process_name: "f2fs_ckpt-254:43"
     thread_id: 347
     process_id: 347
   }
   task_info {
-    estimate_mws: 0.229722
-    estimate_mw: 0.026560
+    estimated_mws: 0.229722
+    estimated_mw: 0.026560
     thread_name: "OomAdjuster"
     process_name: "system_server"
     thread_id: 1482
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.226417
-    estimate_mw: 0.026178
+    estimated_mws: 0.226417
+    estimated_mw: 0.026178
     thread_name: "binder:740_6"
     process_name: "/system/bin/audioserver"
     thread_id: 2639
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.226007
-    estimate_mw: 0.026131
+    estimated_mws: 0.226007
+    estimated_mw: 0.026131
     thread_name: "rcuop/2"
     process_name: "rcuop/2"
     thread_id: 38
     process_id: 38
   }
   task_info {
-    estimate_mws: 0.225133
-    estimate_mw: 0.026030
+    estimated_mws: 0.225133
+    estimated_mw: 0.026030
     thread_name: "kworker/0:7"
     process_name: "kworker/0:7"
     thread_id: 598
     process_id: 598
   }
   task_info {
-    estimate_mws: 0.212788
-    estimate_mw: 0.024602
+    estimated_mws: 0.212788
+    estimated_mw: 0.024602
     thread_name: "queued-work-loo"
     process_name: "system_server"
     thread_id: 1886
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.202562
-    estimate_mw: 0.023420
+    estimated_mws: 0.202562
+    estimated_mw: 0.023420
     thread_name: "pool-13-thread-"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3327
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.201687
-    estimate_mw: 0.023319
+    estimated_mws: 0.201687
+    estimated_mw: 0.023319
     thread_name: "WearSdkThread"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2207
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.201119
-    estimate_mw: 0.023253
+    estimated_mws: 0.201119
+    estimated_mw: 0.023253
     thread_name: "qrtr_ns"
     process_name: "qrtr_ns"
     thread_id: 88
     process_id: 88
   }
   task_info {
-    estimate_mws: 0.200639
-    estimate_mw: 0.023198
+    estimated_mws: 0.200639
+    estimated_mw: 0.023198
     thread_name: "binder:740_7"
     process_name: "/system/bin/audioserver"
     thread_id: 5206
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.196587
-    estimate_mw: 0.022729
+    estimated_mws: 0.196587
+    estimated_mw: 0.022729
     thread_name: "binder:4997_4"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5122
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.194754
-    estimate_mw: 0.022517
+    estimated_mws: 0.194754
+    estimated_mw: 0.022517
     thread_name: "m.android.phone"
     process_name: "com.android.phone"
     thread_id: 2182
     process_id: 2182
   }
   task_info {
-    estimate_mws: 0.192336
-    estimate_mw: 0.022238
+    estimated_mws: 0.192336
+    estimated_mw: 0.022238
     thread_name: "HwcAsyncWorker"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 835
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.190522
-    estimate_mw: 0.022028
+    estimated_mws: 0.190522
+    estimated_mw: 0.022028
     thread_name: "binder:636_2"
     process_name: "/vendor/bin/hw/android.hardware.audio.service"
     thread_id: 636
     process_id: 636
   }
   task_info {
-    estimate_mws: 0.188908
-    estimate_mw: 0.021841
+    estimated_mws: 0.188908
+    estimated_mw: 0.021841
     thread_name: "SettingsProvide"
     process_name: "system_server"
     thread_id: 1771
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.181172
-    estimate_mw: 0.020947
+    estimated_mws: 0.181172
+    estimated_mw: 0.020947
     thread_name: "binder:1302_2"
     process_name: "system_server"
     thread_id: 1350
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.177579
-    estimate_mw: 0.020532
+    estimated_mws: 0.177579
+    estimated_mw: 0.020532
     thread_name: "RegSampIdle"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 872
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.168738
-    estimate_mw: 0.019509
+    estimated_mws: 0.168738
+    estimated_mw: 0.019509
     thread_name: "ice] processing"
     process_name: "com.google.android.gms"
     thread_id: 3238
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.162808
-    estimate_mw: 0.018824
+    estimated_mws: 0.162808
+    estimated_mw: 0.018824
     thread_name: "binder:682_3"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.allocator-service"
     thread_id: 2308
     process_id: 682
   }
   task_info {
-    estimate_mws: 0.158857
-    estimate_mw: 0.018367
+    estimated_mws: 0.158857
+    estimated_mw: 0.018367
     thread_name: "binder:678_3"
     process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
     thread_id: 1884
     process_id: 678
   }
   task_info {
-    estimate_mws: 0.158692
-    estimate_mw: 0.018348
+    estimated_mws: 0.158692
+    estimated_mw: 0.018348
     thread_name: "binder:650_4"
     process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
     thread_id: 5498
     process_id: 650
   }
   task_info {
-    estimate_mws: 0.157956
-    estimate_mw: 0.018263
+    estimated_mws: 0.157956
+    estimated_mw: 0.018263
     thread_name: "vndservicemanag"
     process_name: "/vendor/bin/vndservicemanager"
     thread_id: 215
     process_id: 215
   }
   task_info {
-    estimate_mws: 0.153600
-    estimate_mw: 0.017759
+    estimated_mws: 0.153600
+    estimated_mw: 0.017759
     thread_name: "GoogleLocationS"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3355
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.153038
-    estimate_mw: 0.017694
+    estimated_mws: 0.153038
+    estimated_mw: 0.017694
     thread_name: "TransportThread"
     process_name: "/vendor/bin/chre"
     thread_id: 1078
     process_id: 1041
   }
   task_info {
-    estimate_mws: 0.152058
-    estimate_mw: 0.017581
+    estimated_mws: 0.152058
+    estimated_mw: 0.017581
     thread_name: "kworker/2:1H"
     process_name: "kworker/2:1H"
     thread_id: 123
     process_id: 123
   }
   task_info {
-    estimate_mws: 0.148559
-    estimate_mw: 0.017176
+    estimated_mws: 0.148559
+    estimated_mw: 0.017176
     thread_name: "BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2120
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.148352
-    estimate_mw: 0.017152
+    estimated_mws: 0.148352
+    estimated_mw: 0.017152
     thread_name: "vendor.google.w"
     process_name: "/apex/com.google.wearable.wac.whshal/bin/hw/vendor.google.wearable.wac.whshal@2.0-service"
     thread_id: 1881
     process_id: 678
   }
   task_info {
-    estimate_mws: 0.140864
-    estimate_mw: 0.016287
+    estimated_mws: 0.140864
+    estimated_mw: 0.016287
     thread_name: "NetworkStats"
     process_name: "system_server"
     thread_id: 1814
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.138579
-    estimate_mw: 0.016022
+    estimated_mws: 0.138579
+    estimated_mw: 0.016022
     thread_name: "binder:969_2"
     process_name: "/system/vendor/bin/cnd"
     thread_id: 1011
     process_id: 969
   }
   task_info {
-    estimate_mws: 0.134607
-    estimate_mw: 0.015563
+    estimated_mws: 0.134607
+    estimated_mw: 0.015563
     thread_name: "dmabuf-deferred"
     process_name: "dmabuf-deferred-free-worker"
     thread_id: 69
     process_id: 69
   }
   task_info {
-    estimate_mws: 0.129449
-    estimate_mw: 0.014967
+    estimated_mws: 0.129449
+    estimated_mw: 0.014967
     thread_name: "highpool[5]"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3354
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.126973
-    estimate_mw: 0.014681
+    estimated_mws: 0.126973
+    estimated_mw: 0.014681
     thread_name: "ice] processing"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2363
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.126830
-    estimate_mw: 0.014664
+    estimated_mws: 0.126830
+    estimated_mw: 0.014664
     thread_name: "ediator.Toggler"
     process_name: "system_server"
     thread_id: 1910
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.125742
-    estimate_mw: 0.014538
+    estimated_mws: 0.125742
+    estimated_mw: 0.014538
     thread_name: "surfaceflinger"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 875
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.123833
-    estimate_mw: 0.014317
+    estimated_mws: 0.123833
+    estimated_mw: 0.014317
     thread_name: "wificond"
     process_name: "/system/bin/wificond"
     thread_id: 964
     process_id: 964
   }
   task_info {
-    estimate_mws: 0.123248
-    estimate_mw: 0.014250
+    estimated_mws: 0.123248
+    estimated_mw: 0.014250
     thread_name: "MobileDataStats"
     process_name: "system_server"
     thread_id: 1912
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.119885
-    estimate_mw: 0.013861
+    estimated_mws: 0.119885
+    estimated_mw: 0.013861
     thread_name: "GlobalScheduler"
     process_name: "com.google.android.gms"
     thread_id: 3156
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.119479
-    estimate_mw: 0.013814
+    estimated_mws: 0.119479
+    estimated_mw: 0.013814
     thread_name: "RenderThread"
     thread_id: 5599
   }
   task_info {
-    estimate_mws: 0.119243
-    estimate_mw: 0.013787
+    estimated_mws: 0.119243
+    estimated_mw: 0.013787
     thread_name: "TouchTimer"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 866
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.114249
-    estimate_mw: 0.013209
+    estimated_mws: 0.114249
+    estimated_mw: 0.013209
     thread_name: "binder:4038_1"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4050
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.112705
-    estimate_mw: 0.013031
+    estimated_mws: 0.112705
+    estimated_mw: 0.013031
     thread_name: "displayoffload@"
     process_name: "/vendor/bin/hw/vendor.google_clockwork.displayoffload@2.0-service.1p"
     thread_id: 937
     process_id: 937
   }
   task_info {
-    estimate_mws: 0.111279
-    estimate_mw: 0.012866
+    estimated_mws: 0.111279
+    estimated_mw: 0.012866
     thread_name: "adbd"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5544
     process_id: 5544
   }
   task_info {
-    estimate_mws: 0.108114
-    estimate_mw: 0.012500
+    estimated_mws: 0.108114
+    estimated_mw: 0.012500
     thread_name: "kworker/1:2H"
     process_name: "kworker/1:2H"
     thread_id: 300
     process_id: 300
   }
   task_info {
-    estimate_mws: 0.107442
-    estimate_mw: 0.012422
+    estimated_mws: 0.107442
+    estimated_mw: 0.012422
     thread_name: "RenderThread"
     thread_id: 5584
   }
   task_info {
-    estimate_mws: 0.105863
-    estimate_mw: 0.012240
+    estimated_mws: 0.105863
+    estimated_mw: 0.012240
     thread_name: "Primes-2"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1946
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.104306
-    estimate_mw: 0.012060
+    estimated_mws: 0.104306
+    estimated_mw: 0.012060
     thread_name: "iptables-restor"
     process_name: "/system/bin/iptables-restore"
     thread_id: 558
     process_id: 558
   }
   task_info {
-    estimate_mws: 0.104093
-    estimate_mw: 0.012035
+    estimated_mws: 0.104093
+    estimated_mw: 0.012035
     thread_name: "RenderThread"
     process_name: "system_server"
     thread_id: 5223
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.102912
-    estimate_mw: 0.011899
+    estimated_mws: 0.102912
+    estimated_mw: 0.011899
     thread_name: "irq/168-nanohub"
     process_name: "irq/168-nanohub-irq1"
     thread_id: 296
     process_id: 296
   }
   task_info {
-    estimate_mws: 0.102167
-    estimate_mw: 0.011812
+    estimated_mws: 0.102167
+    estimated_mw: 0.011812
     thread_name: "RenderThread"
     thread_id: 5604
   }
   task_info {
-    estimate_mws: 0.101945
-    estimate_mw: 0.011787
+    estimated_mws: 0.101945
+    estimated_mw: 0.011787
     thread_name: "ksoftirqd/2"
     process_name: "ksoftirqd/2"
     thread_id: 34
     process_id: 34
   }
   task_info {
-    estimate_mws: 0.101282
-    estimate_mw: 0.011710
+    estimated_mws: 0.101282
+    estimated_mw: 0.011710
     thread_name: "PhotonicModulat"
     process_name: "system_server"
     thread_id: 1899
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.100285
-    estimate_mw: 0.011595
+    estimated_mws: 0.100285
+    estimated_mw: 0.011595
     thread_name: "ip6tables-resto"
     process_name: "/system/bin/ip6tables-restore"
     thread_id: 559
     process_id: 559
   }
   task_info {
-    estimate_mws: 0.099432
-    estimate_mw: 0.011496
+    estimated_mws: 0.099432
+    estimated_mw: 0.011496
     thread_name: "init"
     process_name: "/system/bin/init"
     thread_id: 144
     process_id: 144
   }
   task_info {
-    estimate_mws: 0.096314
-    estimate_mw: 0.011136
+    estimated_mws: 0.096314
+    estimated_mw: 0.011136
     thread_name: "FrameworkReceiv"
     process_name: ".qtidataservices"
     thread_id: 2793
     process_id: 2118
   }
   task_info {
-    estimate_mws: 0.095442
-    estimate_mw: 0.011035
+    estimated_mws: 0.095442
+    estimated_mw: 0.011035
     thread_name: "Jit thread pool"
     process_name: "com.google.android.gms.persistent"
     thread_id: 1969
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.094448
-    estimate_mw: 0.010920
+    estimated_mws: 0.094448
+    estimated_mw: 0.010920
     thread_name: "pool-14-thread-"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3314
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.093937
-    estimate_mw: 0.010861
+    estimated_mws: 0.093937
+    estimated_mw: 0.010861
     thread_name: "binder:1926_2"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1939
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.093621
-    estimate_mw: 0.010824
+    estimated_mws: 0.093621
+    estimated_mw: 0.010824
     thread_name: "ChreMsgHandler"
     process_name: "/vendor/bin/chre"
     thread_id: 1080
     process_id: 1041
   }
   task_info {
-    estimate_mws: 0.091738
-    estimate_mw: 0.010607
+    estimated_mws: 0.091738
+    estimated_mw: 0.010607
     thread_name: "DispatcherModul"
     process_name: "/vendor/bin/hw/qcrilNrd"
     thread_id: 1673
     process_id: 1062
   }
   task_info {
-    estimate_mws: 0.091698
-    estimate_mw: 0.010602
+    estimated_mws: 0.091698
+    estimated_mw: 0.010602
     thread_name: "irq/234-pixart_"
     process_name: "irq/234-pixart_pat9126_irq"
     thread_id: 500
     process_id: 500
   }
   task_info {
-    estimate_mws: 0.090883
-    estimate_mw: 0.010508
+    estimated_mws: 0.090883
+    estimated_mw: 0.010508
     thread_name: "scheduler_threa"
     process_name: "scheduler_thread"
     thread_id: 5198
     process_id: 5198
   }
   task_info {
-    estimate_mws: 0.089001
-    estimate_mw: 0.010290
+    estimated_mws: 0.089001
+    estimated_mw: 0.010290
     thread_name: "binder:2085_4"
     process_name: "com.google.android.bluetooth"
     thread_id: 2713
     process_id: 2085
   }
   task_info {
-    estimate_mws: 0.086934
-    estimate_mw: 0.010051
+    estimated_mws: 0.086934
+    estimated_mw: 0.010051
     thread_name: "binder:3028_5"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 5434
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.085941
-    estimate_mw: 0.009936
+    estimated_mws: 0.085941
+    estimated_mw: 0.009936
     thread_name: "binder:2670_6"
     process_name: "com.android.nfc"
     thread_id: 3159
     process_id: 2670
   }
   task_info {
-    estimate_mws: 0.083859
-    estimate_mw: 0.009696
+    estimated_mws: 0.083859
+    estimated_mw: 0.009696
     thread_name: "psimon"
     process_name: "psimon"
     thread_id: 1480
     process_id: 1480
   }
   task_info {
-    estimate_mws: 0.083773
-    estimate_mw: 0.009686
+    estimated_mws: 0.083773
+    estimated_mw: 0.009686
     thread_name: "binder:233_2"
     process_name: "/system/bin/vold"
     thread_id: 252
     process_id: 233
   }
   task_info {
-    estimate_mws: 0.080959
-    estimate_mw: 0.009360
+    estimated_mws: 0.080959
+    estimated_mw: 0.009360
     thread_name: "binder:2049_2"
     process_name: "com.android.networkstack.process"
     thread_id: 2068
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.080298
-    estimate_mw: 0.009284
+    estimated_mws: 0.080298
+    estimated_mw: 0.009284
     thread_name: "netd"
     process_name: "/system/bin/netd"
     thread_id: 568
     process_id: 546
   }
   task_info {
-    estimate_mws: 0.080265
-    estimate_mw: 0.009280
+    estimated_mws: 0.080265
+    estimated_mw: 0.009280
     thread_name: "UEventObserver"
     process_name: "system_server"
     thread_id: 1857
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.079337
-    estimate_mw: 0.009173
+    estimated_mws: 0.079337
+    estimated_mw: 0.009173
     thread_name: "RenderThread"
     thread_id: 5619
   }
   task_info {
-    estimate_mws: 0.078696
-    estimate_mw: 0.009099
+    estimated_mws: 0.078696
+    estimated_mw: 0.009099
     thread_name: "pool-8-thread-1"
     process_name: "com.google.android.gms"
     thread_id: 3102
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.077362
-    estimate_mw: 0.008945
+    estimated_mws: 0.077362
+    estimated_mw: 0.008945
     thread_name: "mcu_mgmtd"
     process_name: "/vendor/bin/mcu_mgmtd"
     thread_id: 594
     process_id: 524
   }
   task_info {
-    estimate_mws: 0.074501
-    estimate_mw: 0.008614
+    estimated_mws: 0.074501
+    estimated_mw: 0.008614
     thread_name: "spi0"
     process_name: "spi0"
     thread_id: 295
     process_id: 295
   }
   task_info {
-    estimate_mws: 0.073914
-    estimate_mw: 0.008546
+    estimated_mws: 0.073914
+    estimated_mw: 0.008546
     thread_name: "com.android.nfc"
     process_name: "com.android.nfc"
     thread_id: 2670
     process_id: 2670
   }
   task_info {
-    estimate_mws: 0.072671
-    estimate_mw: 0.008402
+    estimated_mws: 0.072671
+    estimated_mw: 0.008402
     thread_name: "rcu_exp_gp_kthr"
     process_name: "rcu_exp_gp_kthread_worker"
     thread_id: 19
     process_id: 19
   }
   task_info {
-    estimate_mws: 0.070587
-    estimate_mw: 0.008161
+    estimated_mws: 0.070587
+    estimated_mw: 0.008161
     thread_name: "adbd"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5546
     process_id: 5544
   }
   task_info {
-    estimate_mws: 0.070105
-    estimate_mw: 0.008105
+    estimated_mws: 0.070105
+    estimated_mw: 0.008105
     thread_name: "servicemanager"
     thread_id: 5598
   }
   task_info {
-    estimate_mws: 0.069214
-    estimate_mw: 0.008002
+    estimated_mws: 0.069214
+    estimated_mw: 0.008002
     thread_name: "android.imms"
     process_name: "system_server"
     thread_id: 1791
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.068600
-    estimate_mw: 0.007931
+    estimated_mws: 0.068600
+    estimated_mw: 0.007931
     thread_name: "lowpool[2]"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2321
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.068467
-    estimate_mw: 0.007916
+    estimated_mws: 0.068467
+    estimated_mw: 0.007916
     thread_name: "FileObserver"
     process_name: "com.google.android.gms"
     thread_id: 3035
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.068316
-    estimate_mw: 0.007899
+    estimated_mws: 0.068316
+    estimated_mw: 0.007899
     thread_name: "binder:546_3"
     process_name: "/system/bin/netd"
     thread_id: 546
     process_id: 546
   }
   task_info {
-    estimate_mws: 0.066410
-    estimate_mw: 0.007678
+    estimated_mws: 0.066410
+    estimated_mw: 0.007678
     thread_name: "pool-51-thread-"
     process_name: "com.google.android.gms"
     thread_id: 4215
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.064761
-    estimate_mw: 0.007488
+    estimated_mws: 0.064761
+    estimated_mw: 0.007488
     thread_name: "AudioService"
     process_name: "system_server"
     thread_id: 1844
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.064339
-    estimate_mw: 0.007439
+    estimated_mws: 0.064339
+    estimated_mw: 0.007439
     thread_name: "adbd"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5545
     process_id: 5544
   }
   task_info {
-    estimate_mws: 0.063188
-    estimate_mw: 0.007306
+    estimated_mws: 0.063188
+    estimated_mw: 0.007306
     thread_name: "droid.bluetooth"
     process_name: "com.google.android.bluetooth"
     thread_id: 2085
     process_id: 2085
   }
   task_info {
-    estimate_mws: 0.061069
-    estimate_mw: 0.007061
+    estimated_mws: 0.061069
+    estimated_mw: 0.007061
     thread_name: "msm_irqbalance"
     process_name: "/vendor/bin/msm_irqbalance"
     thread_id: 2466
     process_id: 2466
   }
   task_info {
-    estimate_mws: 0.060920
-    estimate_mw: 0.007044
+    estimated_mws: 0.060920
+    estimated_mw: 0.007044
     thread_name: "BackgroundInsta"
     process_name: "system_server"
     thread_id: 1875
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.059901
-    estimate_mw: 0.006926
+    estimated_mws: 0.059901
+    estimated_mw: 0.006926
     thread_name: "ConnectivitySer"
     process_name: "system_server"
     thread_id: 1827
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.059295
-    estimate_mw: 0.006856
+    estimated_mws: 0.059295
+    estimated_mw: 0.006856
     thread_name: "pool-1-thread-1"
     process_name: "system_server"
     thread_id: 1873
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.059196
-    estimate_mw: 0.006844
+    estimated_mws: 0.059196
+    estimated_mw: 0.006844
     thread_name: "binder:3028_1"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3049
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.058807
-    estimate_mw: 0.006799
+    estimated_mws: 0.058807
+    estimated_mw: 0.006799
     thread_name: "roid.apps.scone"
     process_name: "com.google.android.apps.scone"
     thread_id: 5245
     process_id: 5245
   }
   task_info {
-    estimate_mws: 0.057400
-    estimate_mw: 0.006637
+    estimated_mws: 0.057400
+    estimated_mw: 0.006637
     thread_name: "android.hardwar"
     process_name: "/vendor/bin/hw/android.hardware.health-service.eos"
     thread_id: 1271
     process_id: 1271
   }
   task_info {
-    estimate_mws: 0.056947
-    estimate_mw: 0.006584
+    estimated_mws: 0.056947
+    estimated_mw: 0.006584
     thread_name: "bgres-controlle"
     process_name: "system_server"
     thread_id: 1495
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.056879
-    estimate_mw: 0.006576
+    estimated_mws: 0.056879
+    estimated_mw: 0.006576
     thread_name: "netd"
     process_name: "/system/bin/netd"
     thread_id: 569
     process_id: 546
   }
   task_info {
-    estimate_mws: 0.055771
-    estimate_mw: 0.006448
+    estimated_mws: 0.055771
+    estimated_mw: 0.006448
     thread_name: "UsbFfs-worker"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5560
     process_id: 5544
   }
   task_info {
-    estimate_mws: 0.055642
-    estimate_mw: 0.006433
+    estimated_mws: 0.055642
+    estimated_mw: 0.006433
     thread_name: "system_server"
     thread_id: 5590
   }
   task_info {
-    estimate_mws: 0.054764
-    estimate_mw: 0.006332
+    estimated_mws: 0.054764
+    estimated_mw: 0.006332
     thread_name: "binder:2049_4"
     process_name: "com.android.networkstack.process"
     thread_id: 2083
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.053127
-    estimate_mw: 0.006142
+    estimated_mws: 0.053127
+    estimated_mw: 0.006142
     thread_name: "binder:1949_4"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2302
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.052984
-    estimate_mw: 0.006126
+    estimated_mws: 0.052984
+    estimated_mw: 0.006126
     thread_name: "oid.grilservice"
     process_name: "com.google.android.grilservice"
     thread_id: 2129
     process_id: 2129
   }
   task_info {
-    estimate_mws: 0.052980
-    estimate_mw: 0.006126
+    estimated_mws: 0.052980
+    estimated_mw: 0.006126
     thread_name: "binder:2856_9"
     process_name: "com.google.android.gms"
     thread_id: 5585
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.052715
-    estimate_mw: 0.006095
+    estimated_mws: 0.052715
+    estimated_mw: 0.006095
     thread_name: "StateService"
     process_name: "com.google.android.apps.scone"
     thread_id: 5269
     process_id: 5245
   }
   task_info {
-    estimate_mws: 0.052232
-    estimate_mw: 0.006039
+    estimated_mws: 0.052232
+    estimated_mw: 0.006039
     thread_name: "vndservicemanag"
     thread_id: 5597
   }
   task_info {
-    estimate_mws: 0.052027
-    estimate_mw: 0.006015
+    estimated_mws: 0.052027
+    estimated_mw: 0.006015
     thread_name: "binder:1949_9"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3359
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.051717
-    estimate_mw: 0.005979
+    estimated_mws: 0.051717
+    estimated_mw: 0.005979
     thread_name: "Ipc-5004:1"
     process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
     thread_id: 5483
     process_id: 650
   }
   task_info {
-    estimate_mws: 0.049546
-    estimate_mw: 0.005729
+    estimated_mws: 0.049546
+    estimated_mw: 0.005729
     thread_name: "PackageManager"
     process_name: "system_server"
     thread_id: 1530
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.048227
-    estimate_mw: 0.005576
+    estimated_mws: 0.048227
+    estimated_mw: 0.005576
     thread_name: "android.hardwar"
     process_name: "/vendor/bin/hw/android.hardware.power-service"
     thread_id: 660
     process_id: 660
   }
   task_info {
-    estimate_mws: 0.047454
-    estimate_mw: 0.005487
+    estimated_mws: 0.047454
+    estimated_mw: 0.005487
     thread_name: "BluetoothScanMa"
     process_name: "com.google.android.bluetooth"
     thread_id: 2609
     process_id: 2085
   }
   task_info {
-    estimate_mws: 0.046472
-    estimate_mw: 0.005373
+    estimated_mws: 0.046472
+    estimated_mw: 0.005373
     thread_name: "subsystem_ramdu"
     process_name: "/system/vendor/bin/subsystem_ramdump"
     thread_id: 816
     process_id: 799
   }
   task_info {
-    estimate_mws: 0.046295
-    estimate_mw: 0.005353
+    estimated_mws: 0.046295
+    estimated_mw: 0.005353
     thread_name: "Ipc-5004:2"
     process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
     thread_id: 5484
     process_id: 650
   }
   task_info {
-    estimate_mws: 0.045805
-    estimate_mw: 0.005296
+    estimated_mws: 0.045805
+    estimated_mw: 0.005296
     thread_name: "binder:975_2"
     process_name: "/vendor/bin/imsdaemon"
     thread_id: 1047
     process_id: 975
   }
   task_info {
-    estimate_mws: 0.045282
-    estimate_mw: 0.005235
+    estimated_mws: 0.045282
+    estimated_mw: 0.005235
     thread_name: ".healthservices"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3028
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.045087
-    estimate_mw: 0.005213
+    estimated_mws: 0.045087
+    estimated_mw: 0.005213
     thread_name: "queued-work-loo"
     process_name: "com.google.android.gms"
     thread_id: 3236
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.043921
-    estimate_mw: 0.005078
+    estimated_mws: 0.043921
+    estimated_mw: 0.005078
     thread_name: "powerstateservi"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.powerstateservice@1.0-service"
     thread_id: 276
     process_id: 269
   }
   task_info {
-    estimate_mws: 0.043653
-    estimate_mw: 0.005047
+    estimated_mws: 0.043653
+    estimated_mw: 0.005047
     thread_name: "wlan_logging_th"
     process_name: "wlan_logging_thread"
     thread_id: 368
     process_id: 368
   }
   task_info {
-    estimate_mws: 0.043574
-    estimate_mw: 0.005038
+    estimated_mws: 0.043574
+    estimated_mw: 0.005038
     thread_name: "FlpThread"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3279
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.043436
-    estimate_mw: 0.005022
+    estimated_mws: 0.043436
+    estimated_mw: 0.005022
     thread_name: "Light-P0-2"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5071
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.042985
-    estimate_mw: 0.004970
+    estimated_mws: 0.042985
+    estimated_mw: 0.004970
     thread_name: "binder:5245_4"
     process_name: "com.google.android.apps.scone"
     thread_id: 5270
     process_id: 5245
   }
   task_info {
-    estimate_mws: 0.042946
-    estimate_mw: 0.004965
+    estimated_mws: 0.042946
+    estimated_mw: 0.004965
     thread_name: "HWC_UeventThrea"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.display.composer-service"
     thread_id: 717
     process_id: 685
   }
   task_info {
-    estimate_mws: 0.042762
-    estimate_mw: 0.004944
+    estimated_mws: 0.042762
+    estimated_mw: 0.004944
     thread_name: "pool-12-thread-"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2371
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.042752
-    estimate_mw: 0.004943
+    estimated_mws: 0.042752
+    estimated_mw: 0.004943
     thread_name: "android.hardwar"
     process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
     thread_id: 668
     process_id: 644
   }
   task_info {
-    estimate_mws: 0.042468
-    estimate_mw: 0.004910
+    estimated_mws: 0.042468
+    estimated_mw: 0.004910
     thread_name: "binder:1948_3"
     process_name: "com.google.wear.services"
     thread_id: 1976
     process_id: 1948
   }
   task_info {
-    estimate_mws: 0.042220
-    estimate_mw: 0.004881
+    estimated_mws: 0.042220
+    estimated_mw: 0.004881
     thread_name: "netlink socket"
     process_name: "/system/vendor/bin/ipacm"
     thread_id: 538
     process_id: 523
   }
   task_info {
-    estimate_mws: 0.040507
-    estimate_mw: 0.004683
+    estimated_mws: 0.040507
+    estimated_mw: 0.004683
     thread_name: "RegionSampling"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 871
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.040385
-    estimate_mw: 0.004669
+    estimated_mws: 0.040385
+    estimated_mw: 0.004669
     thread_name: "TransportThread"
     process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
     thread_id: 794
     process_id: 664
   }
   task_info {
-    estimate_mws: 0.040349
-    estimate_mw: 0.004665
+    estimated_mws: 0.040349
+    estimated_mw: 0.004665
     thread_name: "binder:2856_1"
     process_name: "com.google.android.gms"
     thread_id: 2898
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.040180
-    estimate_mw: 0.004646
+    estimated_mws: 0.040180
+    estimated_mw: 0.004646
     thread_name: "servicemanager"
     thread_id: 5595
   }
   task_info {
-    estimate_mws: 0.039525
-    estimate_mw: 0.004570
+    estimated_mws: 0.039525
+    estimated_mw: 0.004570
     thread_name: "pd-mapper"
     process_name: "/vendor/bin/pd-mapper"
     thread_id: 752
     process_id: 725
   }
   task_info {
-    estimate_mws: 0.039196
-    estimate_mw: 0.004532
+    estimated_mws: 0.039196
+    estimated_mw: 0.004532
     thread_name: "vndservicemanag"
     thread_id: 5618
   }
   task_info {
-    estimate_mws: 0.039101
-    estimate_mw: 0.004521
+    estimated_mws: 0.039101
+    estimated_mw: 0.004521
     thread_name: "vndservicemanag"
     thread_id: 5605
   }
   task_info {
-    estimate_mws: 0.038960
-    estimate_mw: 0.004505
+    estimated_mws: 0.038960
+    estimated_mw: 0.004505
     thread_name: "WifiScanningSer"
     process_name: "system_server"
     thread_id: 1823
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.038580
-    estimate_mw: 0.004461
+    estimated_mws: 0.038580
+    estimated_mw: 0.004461
     thread_name: "cnss-daemon"
     process_name: "/system/vendor/bin/cnss-daemon"
     thread_id: 5204
     process_id: 1009
   }
   task_info {
-    estimate_mws: 0.038053
-    estimate_mw: 0.004400
+    estimated_mws: 0.038053
+    estimated_mw: 0.004400
     thread_name: "shell svc 5620"
     process_name: "/apex/com.android.adbd/bin/adbd"
     thread_id: 5622
     process_id: 5544
   }
   task_info {
-    estimate_mws: 0.037116
-    estimate_mw: 0.004291
+    estimated_mws: 0.037116
+    estimated_mw: 0.004291
     thread_name: "rmt_storage"
     process_name: "/vendor/bin/rmt_storage"
     thread_id: 758
     process_id: 758
   }
   task_info {
-    estimate_mws: 0.036357
-    estimate_mw: 0.004204
+    estimated_mws: 0.036357
+    estimated_mw: 0.004204
     thread_name: "halt_drain_rqs"
     process_name: "halt_drain_rqs"
     thread_id: 105
     process_id: 105
   }
   task_info {
-    estimate_mws: 0.035907
-    estimate_mw: 0.004152
+    estimated_mws: 0.035907
+    estimated_mw: 0.004152
     thread_name: "BG Thread #2"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4106
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.035876
-    estimate_mw: 0.004148
+    estimated_mws: 0.035876
+    estimated_mw: 0.004148
     thread_name: "-Executor] idle"
     process_name: "com.google.android.gms"
     thread_id: 5592
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.035444
-    estimate_mw: 0.004098
+    estimated_mws: 0.035444
+    estimated_mw: 0.004098
     thread_name: "tftp_server"
     process_name: "/vendor/bin/tftp_server"
     thread_id: 759
     process_id: 759
   }
   task_info {
-    estimate_mws: 0.035386
-    estimate_mw: 0.004091
+    estimated_mws: 0.035386
+    estimated_mw: 0.004091
     thread_name: "FinalizerWatchd"
     process_name: "com.google.android.gms"
     thread_id: 2887
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.035171
-    estimate_mw: 0.004066
+    estimated_mws: 0.035171
+    estimated_mw: 0.004066
     thread_name: "servicemanager"
     thread_id: 5606
   }
   task_info {
-    estimate_mws: 0.035157
-    estimate_mw: 0.004065
+    estimated_mws: 0.035157
+    estimated_mw: 0.004065
     thread_name: "Blocking Thread"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 5587
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.035034
-    estimate_mw: 0.004051
+    estimated_mws: 0.035034
+    estimated_mw: 0.004051
     thread_name: "vndservicemanag"
     thread_id: 5611
   }
   task_info {
-    estimate_mws: 0.034307
-    estimate_mw: 0.003967
+    estimated_mws: 0.034307
+    estimated_mw: 0.003967
     thread_name: "vndservicemanag"
     thread_id: 5593
   }
   task_info {
-    estimate_mws: 0.034030
-    estimate_mw: 0.003935
+    estimated_mws: 0.034030
+    estimated_mw: 0.003935
     thread_name: "servicemanager"
     thread_id: 5621
   }
   task_info {
-    estimate_mws: 0.032631
-    estimate_mw: 0.003773
+    estimated_mws: 0.032631
+    estimated_mw: 0.003773
     thread_name: "binder:685_3"
     thread_id: 5586
   }
   task_info {
-    estimate_mws: 0.031847
-    estimate_mw: 0.003682
+    estimated_mws: 0.031847
+    estimated_mw: 0.003682
     thread_name: "radioext@1.0-se"
     process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
     thread_id: 676
     process_id: 676
   }
   task_info {
-    estimate_mws: 0.031818
-    estimate_mw: 0.003679
+    estimated_mws: 0.031818
+    estimated_mw: 0.003679
     thread_name: "BgBroadcastRegi"
     process_name: "com.google.wear.services"
     thread_id: 2017
     process_id: 1948
   }
   task_info {
-    estimate_mws: 0.031204
-    estimate_mw: 0.003608
+    estimated_mws: 0.031204
+    estimated_mw: 0.003608
     thread_name: "DefaultExecutor"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 5600
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.030138
-    estimate_mw: 0.003484
+    estimated_mws: 0.030138
+    estimated_mw: 0.003484
     thread_name: "Light-P0-1"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5064
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.029778
-    estimate_mw: 0.003443
+    estimated_mws: 0.029778
+    estimated_mw: 0.003443
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1368
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.029627
-    estimate_mw: 0.003425
+    estimated_mws: 0.029627
+    estimated_mw: 0.003425
     thread_name: "atchdog.monitor"
     process_name: "system_server"
     thread_id: 1414
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.029590
-    estimate_mw: 0.003421
+    estimated_mws: 0.029590
+    estimated_mw: 0.003421
     thread_name: "UsfHalWorker"
     process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
     thread_id: 792
     process_id: 664
   }
   task_info {
-    estimate_mws: 0.028316
-    estimate_mw: 0.003274
+    estimated_mws: 0.028316
+    estimated_mw: 0.003274
     thread_name: "binder:1999_5"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 4985
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.028097
-    estimate_mw: 0.003249
+    estimated_mws: 0.028097
+    estimated_mw: 0.003249
     thread_name: "SatelliteContro"
     process_name: "com.android.phone"
     thread_id: 2382
     process_id: 2182
   }
   task_info {
-    estimate_mws: 0.027797
-    estimate_mw: 0.003214
+    estimated_mws: 0.027797
+    estimated_mw: 0.003214
     thread_name: "pm-service"
     process_name: "/vendor/bin/pm-service"
     thread_id: 745
     process_id: 730
   }
   task_info {
-    estimate_mws: 0.027301
-    estimate_mw: 0.003157
+    estimated_mws: 0.027301
+    estimated_mw: 0.003157
     thread_name: "-Executor] idle"
     process_name: "com.google.android.gms.persistent"
     thread_id: 5603
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.027287
-    estimate_mw: 0.003155
+    estimated_mws: 0.027287
+    estimated_mw: 0.003155
     thread_name: "perfetto"
     process_name: "perfetto"
     thread_id: 5581
     process_id: 5581
   }
   task_info {
-    estimate_mws: 0.026942
-    estimate_mw: 0.003115
+    estimated_mws: 0.026942
+    estimated_mw: 0.003115
     thread_name: "ExeSeq-P10-1"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5074
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.026776
-    estimate_mw: 0.003096
+    estimated_mws: 0.026776
+    estimated_mw: 0.003096
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1364
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.026328
-    estimate_mw: 0.003044
+    estimated_mws: 0.026328
+    estimated_mw: 0.003044
     thread_name: "HwBinder:2129_1"
     process_name: "com.google.android.grilservice"
     thread_id: 3649
     process_id: 2129
   }
   task_info {
-    estimate_mws: 0.026013
-    estimate_mw: 0.003008
+    estimated_mws: 0.026013
+    estimated_mw: 0.003008
     thread_name: "DefaultExecutor"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 5588
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.025701
-    estimate_mw: 0.002972
+    estimated_mws: 0.025701
+    estimated_mw: 0.002972
     thread_name: "rkstack.process"
     process_name: "com.android.networkstack.process"
     thread_id: 2049
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.024843
-    estimate_mw: 0.002872
+    estimated_mws: 0.024843
+    estimated_mw: 0.002872
     thread_name: "hwuiTask1"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1997
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.024811
-    estimate_mw: 0.002869
+    estimated_mws: 0.024811
+    estimated_mw: 0.002869
     thread_name: "pool-1-thread-1"
     process_name: "com.google.android.apps.scone"
     thread_id: 5271
     process_id: 5245
   }
   task_info {
-    estimate_mws: 0.024633
-    estimate_mw: 0.002848
+    estimated_mws: 0.024633
+    estimated_mw: 0.002848
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1360
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.023564
-    estimate_mw: 0.002724
+    estimated_mws: 0.023564
+    estimated_mw: 0.002724
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1366
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.023405
-    estimate_mw: 0.002706
+    estimated_mws: 0.023405
+    estimated_mw: 0.002706
     thread_name: "cnss-daemon"
     process_name: "/system/vendor/bin/cnss-daemon"
     thread_id: 5613
     process_id: 1009
   }
   task_info {
-    estimate_mws: 0.023393
-    estimate_mw: 0.002705
+    estimated_mws: 0.023393
+    estimated_mw: 0.002705
     thread_name: "servicemanager"
     thread_id: 5608
   }
   task_info {
-    estimate_mws: 0.022813
-    estimate_mw: 0.002638
+    estimated_mws: 0.022813
+    estimated_mw: 0.002638
     thread_name: "android.hardwar"
     process_name: "/vendor/bin/hw/android.hardware.contexthub-service.wac"
     thread_id: 644
     process_id: 644
   }
   task_info {
-    estimate_mws: 0.022774
-    estimate_mw: 0.002633
+    estimated_mws: 0.022774
+    estimated_mw: 0.002633
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1362
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.022714
-    estimate_mw: 0.002626
+    estimated_mws: 0.022714
+    estimated_mw: 0.002626
     thread_name: "binder:685_3"
     thread_id: 5594
   }
   task_info {
-    estimate_mws: 0.022642
-    estimate_mw: 0.002618
+    estimated_mws: 0.022642
+    estimated_mw: 0.002618
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1358
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.022617
-    estimate_mw: 0.002615
+    estimated_mws: 0.022617
+    estimated_mw: 0.002615
     thread_name: "vndservicemanag"
     thread_id: 5607
   }
   task_info {
-    estimate_mws: 0.021814
-    estimate_mw: 0.002522
+    estimated_mws: 0.021814
+    estimated_mw: 0.002522
     thread_name: "it.FitbitMobile"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5377
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.021313
-    estimate_mw: 0.002464
+    estimated_mws: 0.021313
+    estimated_mw: 0.002464
     thread_name: "irq/199-dwc3"
     process_name: "irq/199-dwc3"
     thread_id: 5559
     process_id: 5559
   }
   task_info {
-    estimate_mws: 0.021307
-    estimate_mw: 0.002463
+    estimated_mws: 0.021307
+    estimated_mw: 0.002463
     thread_name: "SysUiBg"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2294
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.020941
-    estimate_mw: 0.002421
+    estimated_mws: 0.020941
+    estimated_mw: 0.002421
     thread_name: "-Executor] idle"
     process_name: "com.google.android.gms.persistent"
     thread_id: 5623
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.020393
-    estimate_mw: 0.002358
+    estimated_mws: 0.020393
+    estimated_mw: 0.002358
     thread_name: "vndservicemanag"
     thread_id: 5582
   }
   task_info {
-    estimate_mws: 0.019946
-    estimate_mw: 0.002306
+    estimated_mws: 0.019946
+    estimated_mw: 0.002306
     thread_name: "qcom,system-poo"
     process_name: "qcom,system-pool-refill-thread"
     thread_id: 81
     process_id: 81
   }
   task_info {
-    estimate_mws: 0.019901
-    estimate_mw: 0.002301
+    estimated_mws: 0.019901
+    estimated_mw: 0.002301
     thread_name: "binder:2129_9"
     process_name: "com.google.android.grilservice"
     thread_id: 5203
     process_id: 2129
   }
   task_info {
-    estimate_mws: 0.019723
-    estimate_mw: 0.002280
+    estimated_mws: 0.019723
+    estimated_mw: 0.002280
     thread_name: "servicemanager"
     thread_id: 5610
   }
   task_info {
-    estimate_mws: 0.019537
-    estimate_mw: 0.002259
+    estimated_mws: 0.019537
+    estimated_mw: 0.002259
     thread_name: "cnss-daemon"
     process_name: "/system/vendor/bin/cnss-daemon"
     thread_id: 1009
     process_id: 1009
   }
   task_info {
-    estimate_mws: 0.019390
-    estimate_mw: 0.002242
+    estimated_mws: 0.019390
+    estimated_mw: 0.002242
     thread_name: "pool-9-thread-1"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 2073
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.019219
-    estimate_mw: 0.002222
+    estimated_mws: 0.019219
+    estimated_mw: 0.002222
     thread_name: "binder:1302_3"
     process_name: "system_server"
     thread_id: 1433
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.019059
-    estimate_mw: 0.002204
+    estimated_mws: 0.019059
+    estimated_mw: 0.002204
     thread_name: "mcu_mgmtd"
     process_name: "/vendor/bin/mcu_mgmtd"
     thread_id: 587
     process_id: 524
   }
   task_info {
-    estimate_mws: 0.018619
-    estimate_mw: 0.002153
+    estimated_mws: 0.018619
+    estimated_mw: 0.002153
     thread_name: "WCMTelemetryLog"
     process_name: "system_server"
     thread_id: 1906
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.018508
-    estimate_mw: 0.002140
+    estimated_mws: 0.018508
+    estimated_mw: 0.002140
     thread_name: "droid.tethering"
     process_name: "com.android.networkstack.process"
     thread_id: 2158
     process_id: 2049
   }
   task_info {
-    estimate_mws: 0.018055
-    estimate_mw: 0.002087
+    estimated_mws: 0.018055
+    estimated_mw: 0.002087
     thread_name: "DefaultWallpape"
     process_name: "com.google.android.wearable.watchface.rwf"
     thread_id: 2082
     process_id: 1999
   }
   task_info {
-    estimate_mws: 0.017725
-    estimate_mw: 0.002049
+    estimated_mws: 0.017725
+    estimated_mw: 0.002049
     thread_name: "HeapTaskDaemon"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5386
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.017125
-    estimate_mw: 0.001980
+    estimated_mws: 0.017125
+    estimated_mw: 0.001980
     thread_name: "WearConnectionT"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2172
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.016887
-    estimate_mw: 0.001952
+    estimated_mws: 0.016887
+    estimated_mw: 0.001952
     thread_name: "wear-services-w"
     process_name: "com.google.wear.services"
     thread_id: 2029
     process_id: 1948
   }
   task_info {
-    estimate_mws: 0.016252
-    estimate_mw: 0.001879
+    estimated_mws: 0.016252
+    estimated_mw: 0.001879
     thread_name: "BG Thread #1"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4080
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.016215
-    estimate_mw: 0.001875
+    estimated_mws: 0.016215
+    estimated_mw: 0.001875
     thread_name: "GlobalScheduler"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2276
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.016141
-    estimate_mw: 0.001866
+    estimated_mws: 0.016141
+    estimated_mw: 0.001866
     thread_name: "pool-31-thread-"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 5617
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.016069
-    estimate_mw: 0.001858
+    estimated_mws: 0.016069
+    estimated_mw: 0.001858
     thread_name: "servicemanager"
     thread_id: 5583
   }
   task_info {
-    estimate_mws: 0.015376
-    estimate_mw: 0.001778
+    estimated_mws: 0.015376
+    estimated_mw: 0.001778
     thread_name: "RenderEngine"
     process_name: "/system/bin/surfaceflinger"
     thread_id: 5601
     process_id: 755
   }
   task_info {
-    estimate_mws: 0.015374
-    estimate_mw: 0.001777
+    estimated_mws: 0.015374
+    estimated_mw: 0.001777
     thread_name: "pool-11-thread-"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 5596
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.015097
-    estimate_mw: 0.001745
+    estimated_mws: 0.015097
+    estimated_mw: 0.001745
     thread_name: "dsi_err_workq"
     process_name: "dsi_err_workq"
     thread_id: 5589
     process_id: 5589
   }
   task_info {
-    estimate_mws: 0.015062
-    estimate_mw: 0.001741
+    estimated_mws: 0.015062
+    estimated_mw: 0.001741
     thread_name: "InteractionJank"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2300
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.015034
-    estimate_mw: 0.001738
+    estimated_mws: 0.015034
+    estimated_mw: 0.001738
     thread_name: "vndservicemanag"
     thread_id: 5609
   }
   task_info {
-    estimate_mws: 0.014592
-    estimate_mw: 0.001687
+    estimated_mws: 0.014592
+    estimated_mw: 0.001687
     thread_name: "hwuiTask0"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1996
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.014520
-    estimate_mw: 0.001679
+    estimated_mws: 0.014520
+    estimated_mw: 0.001679
     thread_name: "tworkPolicy.uid"
     process_name: "system_server"
     thread_id: 1817
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.014278
-    estimate_mw: 0.001651
+    estimated_mws: 0.014278
+    estimated_mw: 0.001651
     thread_name: "highpool[10]"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3417
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.014123
-    estimate_mw: 0.001633
+    estimated_mws: 0.014123
+    estimated_mw: 0.001633
     thread_name: "LowMemThread"
     process_name: "system_server"
     thread_id: 1481
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.014026
-    estimate_mw: 0.001622
+    estimated_mws: 0.014026
+    estimated_mw: 0.001622
     thread_name: "pixelstats-vend"
     process_name: "/vendor/bin/pixelstats-vendor"
     thread_id: 266
     process_id: 255
   }
   task_info {
-    estimate_mws: 0.013579
-    estimate_mw: 0.001570
+    estimated_mws: 0.013579
+    estimated_mw: 0.001570
     thread_name: "kworker/u9:0"
     process_name: "kworker/u9:0"
     thread_id: 64
     process_id: 64
   }
   task_info {
-    estimate_mws: 0.013322
-    estimate_mw: 0.001540
+    estimated_mws: 0.013322
+    estimated_mw: 0.001540
     thread_name: "migration/1"
     process_name: "migration/1"
     thread_id: 25
     process_id: 25
   }
   task_info {
-    estimate_mws: 0.013321
-    estimate_mw: 0.001540
+    estimated_mws: 0.013321
+    estimated_mw: 0.001540
     thread_name: "GlobalDispatchi"
     process_name: "com.google.android.gms.persistent"
     thread_id: 2290
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.013127
-    estimate_mw: 0.001518
+    estimated_mws: 0.013127
+    estimated_mw: 0.001518
     thread_name: "main"
     process_name: "/vendor/bin/hw/qcrilNrd"
     thread_id: 1568
     process_id: 1062
   }
   task_info {
-    estimate_mws: 0.012863
-    estimate_mw: 0.001487
+    estimated_mws: 0.012863
+    estimated_mw: 0.001487
     thread_name: "servicemanager"
     thread_id: 5612
   }
   task_info {
-    estimate_mws: 0.012835
-    estimate_mw: 0.001484
+    estimated_mws: 0.012835
+    estimated_mw: 0.001484
     thread_name: "qtidataservices"
     process_name: ".qtidataservices"
     thread_id: 2846
     process_id: 2118
   }
   task_info {
-    estimate_mws: 0.012734
-    estimate_mw: 0.001472
+    estimated_mws: 0.012734
+    estimated_mw: 0.001472
     thread_name: "shortcut"
     process_name: "system_server"
     thread_id: 1874
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.012433
-    estimate_mw: 0.001438
+    estimated_mws: 0.012433
+    estimated_mw: 0.001438
     thread_name: "irq/25-mmc0"
     process_name: "irq/25-mmc0"
     thread_id: 120
     process_id: 120
   }
   task_info {
-    estimate_mws: 0.012244
-    estimate_mw: 0.001416
+    estimated_mws: 0.012244
+    estimated_mw: 0.001416
     thread_name: "GlobalDispatchi"
     process_name: "com.google.android.gms"
     thread_id: 3155
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.012130
-    estimate_mw: 0.001402
+    estimated_mws: 0.012130
+    estimated_mw: 0.001402
     thread_name: "ConnectivityThr"
     process_name: "com.google.android.gms"
     thread_id: 4172
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.011932
-    estimate_mw: 0.001380
+    estimated_mws: 0.011932
+    estimated_mw: 0.001380
     thread_name: "RenderThread"
     thread_id: 5616
   }
   task_info {
-    estimate_mws: 0.011783
-    estimate_mw: 0.001362
+    estimated_mws: 0.011783
+    estimated_mw: 0.001362
     thread_name: "AGMIPC@1.0-serv"
     process_name: "/vendor/bin/hw/vendor.qti.hardware.AGMIPC@1.0-service"
     thread_id: 1454
     process_id: 1446
   }
   task_info {
-    estimate_mws: 0.011706
-    estimate_mw: 0.001353
+    estimated_mws: 0.011706
+    estimated_mw: 0.001353
     thread_name: "binder:2171_2"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2209
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.011675
-    estimate_mw: 0.001350
+    estimated_mws: 0.011675
+    estimated_mw: 0.001350
     thread_name: "radioext@1.0-se"
     process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
     thread_id: 714
     process_id: 676
   }
   task_info {
-    estimate_mws: 0.011615
-    estimate_mw: 0.001343
+    estimated_mws: 0.011615
+    estimated_mw: 0.001343
     thread_name: "servicemanager"
     thread_id: 5615
   }
   task_info {
-    estimate_mws: 0.011467
-    estimate_mw: 0.001326
+    estimated_mws: 0.011467
+    estimated_mw: 0.001326
     thread_name: "LocApiMsgTask"
     process_name: "/vendor/bin/hw/android.hardware.gnss-aidl-service-qti"
     thread_id: 694
     process_id: 650
   }
   task_info {
-    estimate_mws: 0.011318
-    estimate_mw: 0.001309
+    estimated_mws: 0.011318
+    estimated_mw: 0.001309
     thread_name: "LocApiMsgTask"
     process_name: "xtra-daemon"
     thread_id: 1090
     process_id: 1031
   }
   task_info {
-    estimate_mws: 0.011273
-    estimate_mw: 0.001303
+    estimated_mws: 0.011273
+    estimated_mw: 0.001303
     thread_name: "vndservicemanag"
     thread_id: 5614
   }
   task_info {
-    estimate_mws: 0.011024
-    estimate_mw: 0.001275
+    estimated_mws: 0.011024
+    estimated_mw: 0.001275
     thread_name: "TimerThread"
     process_name: "/system/bin/audioserver"
     thread_id: 1486
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.010869
-    estimate_mw: 0.001257
+    estimated_mws: 0.010869
+    estimated_mw: 0.001257
     thread_name: "irq/26-4744000."
     process_name: "irq/26-4744000.sdhci"
     thread_id: 117
     process_id: 117
   }
   task_info {
-    estimate_mws: 0.010764
-    estimate_mw: 0.001245
+    estimated_mws: 0.010764
+    estimated_mw: 0.001245
     thread_name: "SurfaceSyncGrou"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1994
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.010731
-    estimate_mw: 0.001241
+    estimated_mws: 0.010731
+    estimated_mw: 0.001241
     thread_name: "migration/3"
     process_name: "migration/3"
     thread_id: 40
     process_id: 40
   }
   task_info {
-    estimate_mws: 0.010156
-    estimate_mw: 0.001174
+    estimated_mws: 0.010156
+    estimated_mw: 0.001174
     thread_name: "id.wearable.app"
     process_name: "com.google.android.wearable.app"
     thread_id: 3857
     process_id: 3857
   }
   task_info {
-    estimate_mws: 0.009691
-    estimate_mw: 0.001121
+    estimated_mws: 0.009691
+    estimated_mw: 0.001121
     thread_name: "ksoftirqd/0"
     process_name: "ksoftirqd/0"
     thread_id: 13
     process_id: 13
   }
   task_info {
-    estimate_mws: 0.009668
-    estimate_mw: 0.001118
+    estimated_mws: 0.009668
+    estimated_mw: 0.001118
     thread_name: "cnss-daemon"
     process_name: "/system/vendor/bin/cnss-daemon"
     thread_id: 1052
     process_id: 1009
   }
   task_info {
-    estimate_mws: 0.009639
-    estimate_mw: 0.001114
+    estimated_mws: 0.009639
+    estimated_mw: 0.001114
     thread_name: "kworker/u9:2"
     process_name: "kworker/u9:2"
     thread_id: 338
     process_id: 338
   }
   task_info {
-    estimate_mws: 0.009404
-    estimate_mw: 0.001087
+    estimated_mws: 0.009404
+    estimated_mw: 0.001087
     thread_name: "binder:2856_3"
     process_name: "com.google.android.gms"
     thread_id: 3157
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.009364
-    estimate_mw: 0.001083
+    estimated_mws: 0.009364
+    estimated_mw: 0.001083
     thread_name: "binder:969_3"
     process_name: "/system/vendor/bin/cnd"
     thread_id: 1013
     process_id: 969
   }
   task_info {
-    estimate_mws: 0.008837
-    estimate_mw: 0.001022
+    estimated_mws: 0.008837
+    estimated_mw: 0.001022
     thread_name: "binder:2118_2"
     process_name: ".qtidataservices"
     thread_id: 2142
     process_id: 2118
   }
   task_info {
-    estimate_mws: 0.008775
-    estimate_mw: 0.001015
+    estimated_mws: 0.008775
+    estimated_mw: 0.001015
     thread_name: "thermal-engine-"
     process_name: "/vendor/bin/thermal-engine-v2"
     thread_id: 2520
     process_id: 2493
   }
   task_info {
-    estimate_mws: 0.008724
-    estimate_mw: 0.001009
+    estimated_mws: 0.008724
+    estimated_mw: 0.001009
     thread_name: "binder:2856_7"
     process_name: "com.google.android.gms"
     thread_id: 4825
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.008275
-    estimate_mw: 0.000957
+    estimated_mws: 0.008275
+    estimated_mw: 0.000957
     thread_name: "binder:2856_6"
     process_name: "com.google.android.gms"
     thread_id: 4824
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.008136
-    estimate_mw: 0.000941
+    estimated_mws: 0.008136
+    estimated_mw: 0.000941
     thread_name: "binder:3857_3"
     process_name: "com.google.android.wearable.app"
     thread_id: 3872
     process_id: 3857
   }
   task_info {
-    estimate_mws: 0.007913
-    estimate_mw: 0.000915
+    estimated_mws: 0.007913
+    estimated_mw: 0.000915
     thread_name: "binder:975_3"
     process_name: "/vendor/bin/imsdaemon"
     thread_id: 1630
     process_id: 975
   }
   task_info {
-    estimate_mws: 0.007892
-    estimate_mw: 0.000913
+    estimated_mws: 0.007892
+    estimated_mw: 0.000913
     thread_name: "time_daemon"
     process_name: "/vendor/bin/time_daemon"
     thread_id: 525
     process_id: 522
   }
   task_info {
-    estimate_mws: 0.007750
-    estimate_mw: 0.000896
+    estimated_mws: 0.007750
+    estimated_mw: 0.000896
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1044
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.007591
-    estimate_mw: 0.000878
+    estimated_mws: 0.007591
+    estimated_mw: 0.000878
     thread_name: "binder:2856_5"
     process_name: "com.google.android.gms"
     thread_id: 3681
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.007392
-    estimate_mw: 0.000855
+    estimated_mws: 0.007392
+    estimated_mw: 0.000855
     thread_name: "binder:2182_1"
     process_name: "com.android.phone"
     thread_id: 2231
     process_id: 2182
   }
   task_info {
-    estimate_mws: 0.007384
-    estimate_mw: 0.000854
+    estimated_mws: 0.007384
+    estimated_mw: 0.000854
     thread_name: "binder:2171_5"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2638
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.007245
-    estimate_mw: 0.000838
+    estimated_mws: 0.007245
+    estimated_mw: 0.000838
     thread_name: "qrtr_rx"
     process_name: "qrtr_rx"
     thread_id: 1556
     process_id: 1556
   }
   task_info {
-    estimate_mws: 0.007086
-    estimate_mw: 0.000819
+    estimated_mws: 0.007086
+    estimated_mw: 0.000819
     thread_name: "radioext@1.0-se"
     process_name: "/vendor/bin/hw/vendor.google.radioext@1.0-service"
     thread_id: 1605
     process_id: 676
   }
   task_info {
-    estimate_mws: 0.006850
-    estimate_mw: 0.000792
+    estimated_mws: 0.006850
+    estimated_mw: 0.000792
     thread_name: "hwservicemanage"
     process_name: "/system/system_ext/bin/hwservicemanager"
     thread_id: 214
     process_id: 214
   }
   task_info {
-    estimate_mws: 0.006731
-    estimate_mw: 0.000778
+    estimated_mws: 0.006731
+    estimated_mw: 0.000778
     thread_name: "rcub/0"
     process_name: "rcub/0"
     thread_id: 17
     process_id: 17
   }
   task_info {
-    estimate_mws: 0.006663
-    estimate_mw: 0.000770
+    estimated_mws: 0.006663
+    estimated_mw: 0.000770
     thread_name: "binder:2856_8"
     process_name: "com.google.android.gms"
     thread_id: 4826
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.006650
-    estimate_mw: 0.000769
+    estimated_mws: 0.006650
+    estimated_mw: 0.000769
     thread_name: "kthreadd"
     process_name: "kthreadd"
     thread_id: 2
     process_id: 2
   }
   task_info {
-    estimate_mws: 0.006574
-    estimate_mw: 0.000760
+    estimated_mws: 0.006574
+    estimated_mw: 0.000760
     thread_name: "binder:233_2"
     process_name: "/system/bin/vold"
     thread_id: 233
     process_id: 233
   }
   task_info {
-    estimate_mws: 0.006115
-    estimate_mw: 0.000707
+    estimated_mws: 0.006115
+    estimated_mw: 0.000707
     thread_name: "cds_ol_rx_threa"
     process_name: "cds_ol_rx_thread"
     thread_id: 5199
     process_id: 5199
   }
   task_info {
-    estimate_mws: 0.005829
-    estimate_mw: 0.000674
+    estimated_mws: 0.005829
+    estimated_mw: 0.000674
     thread_name: "NsdService"
     process_name: "system_server"
     thread_id: 1831
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.005734
-    estimate_mw: 0.000663
+    estimated_mws: 0.005734
+    estimated_mw: 0.000663
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1367
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.005699
-    estimate_mw: 0.000659
+    estimated_mws: 0.005699
+    estimated_mw: 0.000659
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2895
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.005530
-    estimate_mw: 0.000639
+    estimated_mws: 0.005530
+    estimated_mw: 0.000639
     thread_name: "binder:2856_2"
     process_name: "com.google.android.gms"
     thread_id: 2903
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.005484
-    estimate_mw: 0.000634
+    estimated_mws: 0.005484
+    estimated_mw: 0.000634
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1361
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.005366
-    estimate_mw: 0.000620
+    estimated_mws: 0.005366
+    estimated_mw: 0.000620
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1357
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.005364
-    estimate_mw: 0.000620
+    estimated_mws: 0.005364
+    estimated_mw: 0.000620
     thread_name: "FileObserver"
     process_name: "system_server"
     thread_id: 1498
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.005278
-    estimate_mw: 0.000610
+    estimated_mws: 0.005278
+    estimated_mw: 0.000610
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1359
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.005261
-    estimate_mw: 0.000608
+    estimated_mws: 0.005261
+    estimated_mw: 0.000608
     thread_name: "Lite Thread #0"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4109
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.005011
-    estimate_mw: 0.000579
+    estimated_mws: 0.005011
+    estimated_mw: 0.000579
     thread_name: "kworker/3:1H"
     process_name: "kworker/3:1H"
     thread_id: 122
     process_id: 122
   }
   task_info {
-    estimate_mws: 0.004996
-    estimate_mw: 0.000578
+    estimated_mws: 0.004996
+    estimated_mw: 0.000578
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1365
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.004986
-    estimate_mw: 0.000576
+    estimated_mws: 0.004986
+    estimated_mw: 0.000576
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2890
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.004877
-    estimate_mw: 0.000564
+    estimated_mws: 0.004877
+    estimated_mw: 0.000564
     thread_name: "perfetto_hprof_"
     process_name: "com.google.android.gms"
     thread_id: 2880
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.004761
-    estimate_mw: 0.000550
+    estimated_mws: 0.004761
+    estimated_mw: 0.000550
     thread_name: "backup-0"
     process_name: "system_server"
     thread_id: 2660
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.004757
-    estimate_mw: 0.000550
+    estimated_mws: 0.004757
+    estimated_mw: 0.000550
     thread_name: "tts-player-0"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4209
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.004444
-    estimate_mw: 0.000514
+    estimated_mws: 0.004444
+    estimated_mw: 0.000514
     thread_name: "Signal Catcher"
     process_name: "com.google.android.gms"
     thread_id: 2878
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.004331
-    estimate_mw: 0.000501
+    estimated_mws: 0.004331
+    estimated_mw: 0.000501
     thread_name: "binder:978_2"
     process_name: "/system/vendor/bin/nicmd"
     thread_id: 1363
     process_id: 978
   }
   task_info {
-    estimate_mws: 0.004292
-    estimate_mw: 0.000496
+    estimated_mws: 0.004292
+    estimated_mw: 0.000496
     thread_name: "f2fs_discard-25"
     process_name: "f2fs_discard-254:43"
     thread_id: 349
     process_id: 349
   }
   task_info {
-    estimate_mws: 0.004283
-    estimate_mw: 0.000495
+    estimated_mws: 0.004283
+    estimated_mw: 0.000495
     thread_name: "irq/24-glink-na"
     process_name: "irq/24-glink-native-rpm-glink"
     thread_id: 86
     process_id: 86
   }
   task_info {
-    estimate_mws: 0.004252
-    estimate_mw: 0.000492
+    estimated_mws: 0.004252
+    estimated_mw: 0.000492
     thread_name: "pool-4-thread-1"
     process_name: "system_server"
     thread_id: 1774
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.004010
-    estimate_mw: 0.000464
+    estimated_mws: 0.004010
+    estimated_mw: 0.000464
     thread_name: "PasspointProvis"
     process_name: "system_server"
     thread_id: 1821
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.003934
-    estimate_mw: 0.000455
+    estimated_mws: 0.003934
+    estimated_mw: 0.000455
     thread_name: "binder:5377_5"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5573
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.003880
-    estimate_mw: 0.000449
+    estimated_mws: 0.003880
+    estimated_mw: 0.000449
     thread_name: "BG Thread #3"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4107
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.003872
-    estimate_mw: 0.000448
+    estimated_mws: 0.003872
+    estimated_mw: 0.000448
     thread_name: "TransportThread"
     process_name: "/vendor/bin/mcu_mgmtd"
     thread_id: 3540
     process_id: 524
   }
   task_info {
-    estimate_mws: 0.003835
-    estimate_mw: 0.000443
+    estimated_mws: 0.003835
+    estimated_mw: 0.000443
     thread_name: "kworker/1:1H"
     process_name: "kworker/1:1H"
     thread_id: 127
     process_id: 127
   }
   task_info {
-    estimate_mws: 0.003754
-    estimate_mw: 0.000434
+    estimated_mws: 0.003754
+    estimated_mw: 0.000434
     thread_name: "watchdog"
     process_name: "system_server"
     thread_id: 1421
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.003628
-    estimate_mw: 0.000419
+    estimated_mws: 0.003628
+    estimated_mw: 0.000419
     thread_name: "PackageInstalle"
     process_name: "system_server"
     thread_id: 1744
     process_id: 1302
   }
   task_info {
-    estimate_mws: 0.003469
-    estimate_mw: 0.000401
+    estimated_mws: 0.003469
+    estimated_mw: 0.000401
     thread_name: "Lite Thread #1"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4118
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.003393
-    estimate_mw: 0.000392
+    estimated_mws: 0.003393
+    estimated_mw: 0.000392
     thread_name: "FinalizerWatchd"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5389
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.003365
-    estimate_mw: 0.000389
+    estimated_mws: 0.003365
+    estimated_mw: 0.000389
     thread_name: "DFacilitator-1"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5128
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.003243
-    estimate_mw: 0.000375
+    estimated_mws: 0.003243
+    estimated_mw: 0.000375
     thread_name: "pool-8-thread-1"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 3308
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.002873
-    estimate_mw: 0.000332
+    estimated_mws: 0.002873
+    estimated_mw: 0.000332
     thread_name: "lowpool[1]"
     process_name: "com.google.android.gms"
     thread_id: 3503
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002842
-    estimate_mw: 0.000329
+    estimated_mws: 0.002842
+    estimated_mw: 0.000329
     thread_name: "ReferenceQueueD"
     process_name: "com.google.android.gms"
     thread_id: 2883
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002778
-    estimate_mw: 0.000321
+    estimated_mws: 0.002778
+    estimated_mw: 0.000321
     thread_name: "ipacm-diag"
     process_name: "/system/vendor/bin/ipacm-diag"
     thread_id: 976
     process_id: 976
   }
   task_info {
-    estimate_mws: 0.002739
-    estimate_mw: 0.000317
+    estimated_mws: 0.002739
+    estimated_mw: 0.000317
     thread_name: "migration/2"
     process_name: "migration/2"
     thread_id: 32
     process_id: 32
   }
   task_info {
-    estimate_mws: 0.002654
-    estimate_mw: 0.000307
+    estimated_mws: 0.002654
+    estimated_mw: 0.000307
     thread_name: "qrtr_rx"
     process_name: "qrtr_rx"
     thread_id: 564
     process_id: 564
   }
   task_info {
-    estimate_mws: 0.002601
-    estimate_mw: 0.000301
+    estimated_mws: 0.002601
+    estimated_mw: 0.000301
     thread_name: "card0-crtc0"
     process_name: "card0-crtc0"
     thread_id: 247
     process_id: 247
   }
   task_info {
-    estimate_mws: 0.002574
-    estimate_mw: 0.000298
+    estimated_mws: 0.002574
+    estimated_mw: 0.000298
     thread_name: "pool-7-thread-3"
     process_name: "com.google.android.wearable.healthservices"
     thread_id: 5435
     process_id: 3028
   }
   task_info {
-    estimate_mws: 0.002458
-    estimate_mw: 0.000284
+    estimated_mws: 0.002458
+    estimated_mw: 0.000284
     thread_name: "RenderThread"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2319
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.002443
-    estimate_mw: 0.000283
+    estimated_mws: 0.002443
+    estimated_mw: 0.000283
     thread_name: "highpool[0]"
     process_name: "com.google.android.gms"
     thread_id: 3154
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002437
-    estimate_mw: 0.000282
+    estimated_mws: 0.002437
+    estimated_mw: 0.000282
     thread_name: "lowpool[0]"
     process_name: "com.google.android.gms"
     thread_id: 3478
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002241
-    estimate_mw: 0.000259
+    estimated_mws: 0.002241
+    estimated_mw: 0.000259
     thread_name: "queued-work-loo"
     process_name: "com.google.android.gms.persistent"
     thread_id: 3533
     process_id: 1949
   }
   task_info {
-    estimate_mws: 0.002222
-    estimate_mw: 0.000257
+    estimated_mws: 0.002222
+    estimated_mw: 0.000257
     thread_name: "arch_disk_io_2"
     process_name: "com.google.android.gms"
     thread_id: 4174
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002160
-    estimate_mw: 0.000250
+    estimated_mws: 0.002160
+    estimated_mw: 0.000250
     thread_name: "binder:523_2"
     process_name: "/system/vendor/bin/ipacm"
     thread_id: 537
     process_id: 523
   }
   task_info {
-    estimate_mws: 0.002129
-    estimate_mw: 0.000246
+    estimated_mws: 0.002129
+    estimated_mw: 0.000246
     thread_name: "Jit thread pool"
     process_name: "com.google.android.gms"
     thread_id: 2881
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002082
-    estimate_mw: 0.000241
+    estimated_mws: 0.002082
+    estimated_mw: 0.000241
     thread_name: "pool-48-thread-"
     process_name: "com.google.android.gms"
     thread_id: 4110
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.002071
-    estimate_mw: 0.000239
+    estimated_mws: 0.002071
+    estimated_mw: 0.000239
     thread_name: "RenderThread"
     process_name: "com.google.android.inputmethod.latin"
     thread_id: 5120
     process_id: 4997
   }
   task_info {
-    estimate_mws: 0.002020
-    estimate_mw: 0.000234
+    estimated_mws: 0.002020
+    estimated_mw: 0.000234
     thread_name: "arch_disk_io_0"
     process_name: "com.google.android.gms"
     thread_id: 4031
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.001985
-    estimate_mw: 0.000229
+    estimated_mws: 0.001985
+    estimated_mw: 0.000229
     thread_name: "arch_disk_io_1"
     process_name: "com.google.android.gms"
     thread_id: 4034
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.001856
-    estimate_mw: 0.000215
+    estimated_mws: 0.001856
+    estimated_mw: 0.000215
     thread_name: "BG Thread #0"
     process_name: "com.google.android.wearable.assistant"
     thread_id: 4061
     process_id: 4038
   }
   task_info {
-    estimate_mws: 0.001782
-    estimate_mw: 0.000206
+    estimated_mws: 0.001782
+    estimated_mw: 0.000206
     thread_name: "binder:1926_1"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 1938
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.001776
-    estimate_mw: 0.000205
+    estimated_mws: 0.001776
+    estimated_mw: 0.000205
     thread_name: "highpool[3]"
     process_name: "com.google.android.gms"
     thread_id: 3470
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.001776
-    estimate_mw: 0.000205
+    estimated_mws: 0.001776
+    estimated_mw: 0.000205
     thread_name: "migration/0"
     process_name: "migration/0"
     thread_id: 21
     process_id: 21
   }
   task_info {
-    estimate_mws: 0.001772
-    estimate_mw: 0.000205
+    estimated_mws: 0.001772
+    estimated_mw: 0.000205
     thread_name: "AsyncTask #2"
     process_name: "com.google.android.gms"
     thread_id: 4164
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.001720
-    estimate_mw: 0.000199
+    estimated_mws: 0.001720
+    estimated_mw: 0.000199
     thread_name: "arch_disk_io_3"
     process_name: "com.google.android.gms"
     thread_id: 4175
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.001562
-    estimate_mw: 0.000181
+    estimated_mws: 0.001562
+    estimated_mw: 0.000181
     thread_name: "POSIX timer 0"
     process_name: "/vendor/bin/hw/android.hardware.sensors-service.multihal"
     thread_id: 850
     process_id: 664
   }
   task_info {
-    estimate_mws: 0.001520
-    estimate_mw: 0.000176
+    estimated_mws: 0.001520
+    estimated_mw: 0.000176
     thread_name: "ksoftirqd/3"
     process_name: "ksoftirqd/3"
     thread_id: 42
     process_id: 42
   }
   task_info {
-    estimate_mws: 0.001401
-    estimate_mw: 0.000162
+    estimated_mws: 0.001401
+    estimated_mw: 0.000162
     thread_name: "Primes-1"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5394
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001320
-    estimate_mw: 0.000153
+    estimated_mws: 0.001320
+    estimated_mw: 0.000153
     thread_name: "binder:5377_3"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5392
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001316
-    estimate_mw: 0.000152
+    estimated_mws: 0.001316
+    estimated_mw: 0.000152
     thread_name: "msm-watchdog"
     process_name: "msm-watchdog"
     thread_id: 76
     process_id: 76
   }
   task_info {
-    estimate_mws: 0.001222
-    estimate_mw: 0.000141
+    estimated_mws: 0.001222
+    estimated_mw: 0.000141
     thread_name: "Lite Thread #1"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5421
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001220
-    estimate_mw: 0.000141
+    estimated_mws: 0.001220
+    estimated_mw: 0.000141
     thread_name: "Signal Catcher"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5382
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001179
-    estimate_mw: 0.000136
+    estimated_mws: 0.001179
+    estimated_mw: 0.000136
     thread_name: "GoogleApiHandle"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5398
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001127
-    estimate_mw: 0.000130
+    estimated_mws: 0.001127
+    estimated_mw: 0.000130
     thread_name: "binder:2171_6"
     process_name: "com.google.android.apps.wearable.systemui"
     thread_id: 2678
     process_id: 2171
   }
   task_info {
-    estimate_mws: 0.001103
-    estimate_mw: 0.000128
+    estimated_mws: 0.001103
+    estimated_mw: 0.000128
     thread_name: "Blocking Thread"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5574
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.001055
-    estimate_mw: 0.000122
+    estimated_mws: 0.001055
+    estimated_mw: 0.000122
     thread_name: "WM.task-3"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5430
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000990
-    estimate_mw: 0.000114
+    estimated_mws: 0.000990
+    estimated_mw: 0.000114
     thread_name: "highpool[1]"
     process_name: "com.google.android.gms"
     thread_id: 3373
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.000984
-    estimate_mw: 0.000114
+    estimated_mws: 0.000984
+    estimated_mw: 0.000114
     thread_name: "Primes-nativecr"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5397
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000961
-    estimate_mw: 0.000111
+    estimated_mws: 0.000961
+    estimated_mw: 0.000111
     thread_name: "binder:740_4"
     process_name: "/system/bin/audioserver"
     thread_id: 2183
     process_id: 740
   }
   task_info {
-    estimate_mws: 0.000954
-    estimate_mw: 0.000110
+    estimated_mws: 0.000954
+    estimated_mw: 0.000110
     thread_name: "Lite Thread #0"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5404
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000927
-    estimate_mw: 0.000107
+    estimated_mws: 0.000927
+    estimated_mw: 0.000107
     thread_name: "BG Thread #0"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5395
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000926
-    estimate_mw: 0.000107
+    estimated_mws: 0.000926
+    estimated_mw: 0.000107
     thread_name: "FinalizerDaemon"
     process_name: "com.google.android.gms"
     thread_id: 2885
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.000887
-    estimate_mw: 0.000103
+    estimated_mws: 0.000887
+    estimated_mw: 0.000103
     thread_name: "BG Thread #1"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5396
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000865
-    estimate_mw: 0.000100
+    estimated_mws: 0.000865
+    estimated_mw: 0.000100
     thread_name: "Jit thread pool"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5385
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000858
-    estimate_mw: 0.000099
+    estimated_mws: 0.000858
+    estimated_mw: 0.000099
     thread_name: "highpool[2]"
     process_name: "com.google.android.gms"
     thread_id: 3375
     process_id: 2856
   }
   task_info {
-    estimate_mws: 0.000855
-    estimate_mw: 0.000099
+    estimated_mws: 0.000855
+    estimated_mw: 0.000099
     thread_name: "ConnectivityThr"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5423
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000828
-    estimate_mw: 0.000096
+    estimated_mws: 0.000828
+    estimated_mw: 0.000096
     thread_name: "Profile Saver"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5393
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000808
-    estimate_mw: 0.000093
+    estimated_mws: 0.000808
+    estimated_mw: 0.000093
     thread_name: "ReferenceQueueD"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5387
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000803
-    estimate_mw: 0.000093
+    estimated_mws: 0.000803
+    estimated_mw: 0.000093
     thread_name: "binder:5377_4"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5433
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000783
-    estimate_mw: 0.000091
+    estimated_mws: 0.000783
+    estimated_mw: 0.000091
     thread_name: "ksoftirqd/1"
     process_name: "ksoftirqd/1"
     thread_id: 27
     process_id: 27
   }
   task_info {
-    estimate_mws: 0.000782
-    estimate_mw: 0.000090
+    estimated_mws: 0.000782
+    estimated_mw: 0.000090
     thread_name: "HsConnectionMan"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5422
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000769
-    estimate_mw: 0.000089
+    estimated_mws: 0.000769
+    estimated_mw: 0.000089
     thread_name: "ADB-JDWP Connec"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5384
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000742
-    estimate_mw: 0.000086
+    estimated_mws: 0.000742
+    estimated_mw: 0.000086
     thread_name: "Scheduler Threa"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5428
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000733
-    estimate_mw: 0.000085
+    estimated_mws: 0.000733
+    estimated_mw: 0.000085
     thread_name: "WM.task-2"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5429
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000730
-    estimate_mw: 0.000084
+    estimated_mws: 0.000730
+    estimated_mw: 0.000084
     thread_name: "Scheduled BG"
     process_name: "com.google.android.wearable.sysui"
     thread_id: 2896
     process_id: 1926
   }
   task_info {
-    estimate_mws: 0.000727
-    estimate_mw: 0.000084
+    estimated_mws: 0.000727
+    estimated_mw: 0.000084
     thread_name: "DefaultDispatch"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5431
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000726
-    estimate_mw: 0.000084
+    estimated_mws: 0.000726
+    estimated_mw: 0.000084
     thread_name: "BG Thread #3"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5400
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000724
-    estimate_mw: 0.000084
+    estimated_mws: 0.000724
+    estimated_mw: 0.000084
     thread_name: "WM.task-1"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5427
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000718
-    estimate_mw: 0.000083
+    estimated_mws: 0.000718
+    estimated_mw: 0.000083
     thread_name: "binder:5377_1"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5390
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000689
-    estimate_mw: 0.000080
+    estimated_mws: 0.000689
+    estimated_mw: 0.000080
     thread_name: "DefaultDispatch"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5432
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000669
-    estimate_mw: 0.000077
+    estimated_mws: 0.000669
+    estimated_mw: 0.000077
     thread_name: "BG Thread #2"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5399
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000630
-    estimate_mw: 0.000073
+    estimated_mws: 0.000630
+    estimated_mw: 0.000073
     thread_name: "binder:5377_2"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5391
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000583
-    estimate_mw: 0.000067
+    estimated_mws: 0.000583
+    estimated_mw: 0.000067
     thread_name: "perfetto_hprof_"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5383
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000507
-    estimate_mw: 0.000059
+    estimated_mws: 0.000507
+    estimated_mw: 0.000059
     thread_name: "Primes-2"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5444
     process_id: 5377
   }
   task_info {
-    estimate_mws: 0.000403
-    estimate_mw: 0.000047
+    estimated_mws: 0.000403
+    estimated_mw: 0.000047
     thread_name: "FinalizerDaemon"
     process_name: "com.fitbit.FitbitMobile"
     thread_id: 5388
diff --git a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
index d4a0dc4..f77a47a 100644
--- a/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
+++ b/test/trace_processor/diff_tests/parser/chrome/tests_v8.py
@@ -100,3 +100,41 @@
 0
 """),
     )
+
+  def test_v8_cpu_samples(self):
+    return DiffTestBlueprint(
+        trace=DataPath('v8-samples.pftrace'),
+        query='''
+          include perfetto module callstacks.stack_profile;
+
+          select name, source_file, self_count
+          from _callstacks_for_cpu_profile_stack_samples!(
+            cpu_profile_stack_sample
+          )
+          where self_count > 0
+          order by self_count desc
+          limit 20
+        ''',
+        out=Csv('''
+        "name","source_file","self_count"
+        "(program)","[NULL]",17083
+        "(program)","[NULL]",15399
+        "(program)","[NULL]",9853
+        "(program)","[NULL]",9391
+        "(program)","[NULL]",7299
+        "(program)","[NULL]",5245
+        "(program)","[NULL]",2443
+        "(garbage collector)","[NULL]",107
+        "_.mg","chrome-untrusted://new-tab-page/one-google-bar?paramsencoded=",38
+        "(garbage collector)","[NULL]",34
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",33
+        "_.m.Ddb","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/am=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAACAEKAAAABR4AAAAgAAAAAAAAAAQIAQDEAQAAAgA4AAAEAQAEABQQAAAKEATgUTYAgAAwAQAIAAAQAAACQAAACAAAAAMAACAIAAAAAKAAAAAAAAAAAAAAAAAAYAABBAAAAAAAAAAAAIACAAAAoAMAAAAAgAAAgIAAANghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=1/ed=1/dg=3/br=1/rs=ACT90oH8sSQRHJq5R0DO9ABVW-vZJa5Baw/ee=ALeJib:B8gLwd;AfeaP:TkrAjf;BMxAGc:E5bFse;BgS6mb:fidj5d;BjwMce:cXX2Wb;CxXAWb:YyRLvc;DULqB:RKfG5c;Dkk6ge:wJqrrd;DpcR3d:zL72xf;EABSZ:MXZt9d;ESrPQc:mNTJvc;EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;EnlcNd:WeHg4;Erl4fe:FloWmf,FloWmf;F9mqte:UoRcbe;Fmv9Nc:O1Tzwc;G0KhTb:LIaoZ;G6wU6e:hezEbd;GleZL:J1A7Od;HMDDWe:G8QUdb;HoYVKb:PkDN7e;HqeXPd:cmbnH;IBADCc:RYquRb;IZrNqe:P8ha2c;IoGlCf:b5lhvb;IsdWVc:qzxzOb;JXS8fb:Qj0suc;JbMT3:M25sS;JsbNhc:Xd8iUd;KOxcK:OZqGte;KQzWid:ZMKkN;KcokUb:KiuZBf;KpRAue:Tia57b;LBgRLc:SdcwHb,XVMNvd;LEikZe:byfTOb,lsjVmc;LXA8b:q7OdKd;LsNahb:ucGLNb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Np8Qkd:Dpx6qc;Nyt6ic:jn2sGd;OgagBe:",18
+        "da","https://www.google.com/",15
+        "","https://www.gstatic.com/_/mss/boq-one-google/_/js/k=boq-one-google.OneGoogleWidgetUi.en.Dv_TT86KXl4.es5.O/ck=boq-one-google.OneGoogleWidgetUi.xexmpZqkioA.L.B1.O/am=QKBgwGw/d=1/exm=_b,_tp/excm=_b,_tp,calloutview/ed=1/wt=2/ujg=1/rs=AM-SdHu61g-i-YBZiLcGm3tURf4VJO5hyA/ee=EVNhjf:pw70Gc;EmZ2Bf:zr1jrb;Erl4fe:FloWmf;JsbNhc:Xd8iUd;LBgRLc:SdcwHb;Me32dd:MEeYgc;NPKaK:SdcwHb;NSEoX:lazG7b;Oj465e:KG2eXe;Pjplud:EEDORb;QGR0gd:Mlhmy;SNUn3:ZwDk9d;a56pNe:JEfCwb;cEt90b:ws9Tlc;dIoSBb:SpsfSb;eBAeSb:zbML3c;iFQyKf:QIhFr;io8t5d:yDVVkb;kMFpHd:OTA3Ae;nAFL3:s39S4;oGtAuc:sOXFj;pXdRYb:MdUzUe;qddgKe:xQtZb;sP4Vbe:VwDzFe;uY49fb:COQbmf;ul9GGd:VDovNc;wR5FRb:O1Gjze;xqZiqf:wmnU7d;yxTchf:KUM7Z;zxnPse:GkRiKb/m=ws9Tlc,n73qwf,GkRiKb,e5qFLc,IZT63,UUJqVe,O1Gjze,byfTOb,lsjVmc,xUdipf,OTA3Ae,COQbmf,fKUV3e,aurFic,U0aPgd,ZwDk9d,V3dDOb,mI3LFb,yYB61,O6y8ed,PrPYRd,MpJwZc,LEikZe,NwH0H,OmgaI,lazG7b,XVMNvd,L1AAkb,KUM7Z,Mlhmy,s39S4,lwddkf,gychg,w9hDv,EEDORb,RMhBfe,SdcwHb,aW3pY,pw70Gc,EFQ78c,Ulmmrd,ZfAoz,mdR7q,wmnU7d,xQtZb,JNoxi,kWgXee,MI6k7c,kjKdXe,BVgquf,QIhFr,ov",13
+        "updateAttrs","https://ui.perfetto.dev/v46.0-0a53e685b/frontend_bundle.js",12
+        "","https://www.gstatic.com/_/mss/boq-one-google/_/js/k=boq-one-google.OneGoogleWidgetUi.en.Dv_TT86KXl4.es5.O/am=QKBgwGw/d=1/excm=_b,_tp,calloutview/ed=1/dg=0/wt=2/ujg=1/rs=AM-SdHsuxqEW2z6uUf-9MJvUVpOyFk0ecQ/m=_b,_tp",11
+        "a._isVisible","https://ogs.google.com/widget/callout?prid=19037050&pgid=19037049&puid=6a851fbb7ce797ac&eom=1&cce=1&dc=1&origin=https%3A%2F%2Fwww.google.com&cn=callout&pid=1&spid=538&hl=en&dm=",11
+        "","chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js",11
+        "","https://www.google.com/xjs/_/js/k=xjs.hd.en.nSJdbfIGUiE.O/ck=xjs.hd.F00K1IyvS9A.L.B1.O/am=IFEAAAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAIAAAEAAAAAAAASAEakAAABZ5sAMBiAAAABAAIAAQIAQDEAQAAAwQ4AAAEAQAUABQREAEKEgTgUTYAhIAwAQQoQAgUQAICQBCFCAAAAAMAACEIDDAMQKgAYBQgAAAAAEBABAAAYAA3BhAgAMAPAAAYAKICAAAhoAMQAAABgAJAgIACAtghAwgAAAQAAACgDwCCB8AghQcAAAAAAAAAAAAAAAKQIJgLCSgIQAAAAAAAAAAAAAAAAACkpIkLCw/d=0/dg=0/br=1/ujg=1/rs=ACT90oFYV6TnvY5P3NcVPbMRvVPRlxmm8A/m=sb_wiz,aa,abd,sytt,syts,sytn,syfx,sytr,sytd,sy101,syz7,syti,syz6,syto,sytq,sytm,syu7,sytb,syu8,syu9,syu0,syu4,sytj,syty,syu1,syu2,sytv,sytw,syte,sytf,sys4,syru,syrs,syrr,syth,syz5,syug,syuh,syuf,async,syvk,ifl,pHXghd,sf,sy1c2,sy1c5,sy4e0,sonic,TxCJfd,sy4e4,qzxzOb,IsdWVc,sy4e6,sy1gs,sy1d4,sy1d0,syrq,syro,syrp,syrn,syrm,sy4cl,sy4co,sy2ib,sy18p,sy18r,sy13l,sy13m,syrj,syrh,syfb,sybv,syby,sybt,sybx,sybw,sycp,spch,sys7,sys6,rtH1bd,sy1ea,sy19r,sy18g,syg9,sy1e9,sy13t,sy1e8,sy18h,sygb,sy1eb,SMquOb,sy8f,sygh,sygf,sygg,sygi,syge,sygp,sygn,sygl,sygd,sycm,sych,syck,syak,syac,syb6,syaj,syai,sya",10
+        "maybeUpdateMoreOptions","chrome-untrusted://read-anything-side-panel.top-chrome/read_anything.js",10
+        '''))
diff --git a/test/trace_processor/diff_tests/parser/instruments/tests.py b/test/trace_processor/diff_tests/parser/instruments/tests.py
new file mode 100644
index 0000000..5558ec9
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/instruments/tests.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, Path, DataPath
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+# These diff tests use some locally collected trace.
+class Instruments(TestSuite):
+
+  def test_xml_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace.xml'),
+        query='''
+          WITH
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                rel_pc AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                JOIN stack_profile_frame f ON (f.id = frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.rel_pc || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN stack_profile_frame f ON (f.id = frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,16095,15891,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16021"
+          4,179687000,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+          5,180683708,1,"24915,16107,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16047,16005"
+        '''))
+
+  def test_symbolized_frames(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          SELECT
+            f.id,
+            m.name,
+            m.build_id,
+            f.rel_pc,
+            s.name,
+            s.source_file,
+            s.line_number
+          FROM
+            stack_profile_frame f
+            JOIN stack_profile_mapping m ON f.mapping = m.id
+            JOIN stack_profile_symbol s ON f.symbol_set_id = s.symbol_set_id
+        ''',
+        out=Csv('''
+          "id","name","build_id","rel_pc","name","source_file","line_number"
+          26,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16095,"main","/tmp/test.cpp",25
+          27,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",15891,"EmitSignpost()","/tmp/test.cpp",8
+          38,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16107,"main","/tmp/test.cpp",27
+          39,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16047,"fib(int)","/tmp/test.cpp",21
+          40,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16021,"fib(int)","/tmp/test.cpp",22
+          41,"/private/tmp/test","c3b3bdbd348730f18f9ddd08b7708d49",16005,"fib(int)","/tmp/test.cpp",15
+        '''))
+
+  def test_symbolized_stacks(self):
+    return DiffTestBlueprint(
+        trace=DataPath('instruments_trace_with_symbols.zip'),
+        query='''
+          WITH
+            frame AS (
+              SELECT
+                f.id AS frame_id,
+                COALESCE(s.name || ':' || s.line_number, f.rel_pc) as name
+              FROM
+                stack_profile_frame f
+                LEFT JOIN stack_profile_symbol s USING (symbol_set_id)
+            ),
+            child AS (
+              SELECT
+                spc.id AS root,
+                spc.id,
+                spc.parent_id,
+                name AS path
+              FROM
+                instruments_sample s
+                JOIN stack_profile_callsite spc ON (s.callsite_id = spc.id)
+                LEFT JOIN frame f USING (frame_id)
+              UNION ALL
+              SELECT
+                child.root,
+                parent.id,
+                parent.parent_id,
+                COALESCE(f.name || ',', '') || child.path AS path
+              FROM
+                child
+                JOIN stack_profile_callsite parent ON (child.parent_id = parent.id)
+                LEFT JOIN frame f USING (frame_id)
+            )
+          SELECT
+            s.id,
+            s.ts,
+            s.utid,
+            c.path
+          FROM
+            instruments_sample s
+            JOIN child c ON s.callsite_id = c.root
+          WHERE
+            c.parent_id IS NULL
+        ''',
+        out=Csv('''
+          "id","ts","utid","path"
+          0,175685291,1,"23999,34891,37935,334037"
+          1,176684208,1,"24307,28687,265407,160467,120123,391295,336787,8955,340991,392555,136711,5707,7603,10507,207839,207495,23655,17383,23211,208391,6225"
+          2,177685166,1,"24915,main:25,EmitSignpost():8,32211,91151,26907,87887,60651,28343,29471,30159,11087,36269"
+          3,178683916,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):22"
+          4,179687000,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+          5,180683708,1,"24915,main:27,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):21,fib(int):15"
+        '''))
diff --git a/test/trace_processor/diff_tests/stdlib/linux/cpu.py b/test/trace_processor/diff_tests/stdlib/linux/cpu.py
index 6dfe8bd..5251151 100644
--- a/test/trace_processor/diff_tests/stdlib/linux/cpu.py
+++ b/test/trace_processor/diff_tests/stdlib/linux/cpu.py
@@ -323,3 +323,58 @@
          "cpu","state","count","dur","avg_dur","idle_percent"
          0,2,2,2000000,1000000,40.000000
          """))
+
+  def test_linux_cpu_idle_time_in_state(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000000
+              }
+            }
+          }
+          timestamp: 200000000000
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000100
+              }
+            }
+          }
+          timestamp: 200001000000
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          sys_stats {
+            cpuidle_state {
+              cpu_id: 0
+              cpuidle_state_entry {
+                state: "C8"
+                duration_us: 1000200
+              }
+            }
+          }
+          timestamp: 200002000000
+          trusted_packet_sequence_id: 2
+        }
+         """),
+        query="""
+         INCLUDE PERFETTO MODULE linux.cpu.idle_time_in_state;
+         SELECT * FROM cpu_idle_time_in_state_counters;
+         """,
+        out=Csv("""
+         "ts","state_name","idle_percentage","total_residency","time_slice"
+          200001000000,"cpuidle.C8",10.000000,100.000000,1000
+          200002000000,"cpuidle.C8",10.000000,100.000000,1000
+          200001000000,"cpuidle.C0",90.000000,900.000000,1000
+          200002000000,"cpuidle.C0",90.000000,900.000000,1000
+         """))
diff --git a/test/trace_processor/diff_tests/stdlib/wattson/tests.py b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
index 69d787a..1ddead7 100644
--- a/test/trace_processor/diff_tests/stdlib/wattson/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/wattson/tests.py
@@ -362,7 +362,7 @@
         query=("""
             INCLUDE PERFETTO MODULE wattson.curves.idle_attribution;
             SELECT
-              SUM(estimate_mw * dur) / 1000000000 as idle_transition_cost_mws,
+              SUM(estimated_mw * dur) / 1000000000 as idle_transition_cost_mws,
               utid,
               upid
             FROM _idle_transition_cost
diff --git a/tools/gen_amalgamated_sql.py b/tools/gen_amalgamated_sql.py
index 17fbe1e..ac7a530 100755
--- a/tools/gen_amalgamated_sql.py
+++ b/tools/gen_amalgamated_sql.py
@@ -66,8 +66,10 @@
 
 
 def filename_to_variable(filename: str):
-  return "k" + "".join(
-      [x.capitalize() for x in filename.replace(os.path.sep, '_').split("_")])
+  return "k" + "".join([
+      x.capitalize()
+      for x in filename.replace(os.path.sep, '_').replace('-', '_').split("_")
+  ])
 
 
 def main():
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 9dbeac7..032b38a 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -382,6 +382,16 @@
     module.shared_libs.add('libz')
 
 
+def enable_expat(module):
+  if module.type == 'cc_binary_host':
+    module.static_libs.add('libexpat')
+  elif module.host_supported:
+    module.android.shared_libs.add('libexpat')
+    module.host.static_libs.add('libexpat')
+  else:
+    module.shared_libs.add('libexpat')
+
+
 def enable_uapi_headers(module):
   module.include_dirs.add('bionic/libc/kernel')
 
@@ -417,6 +427,8 @@
         enable_sqlite,
     '//gn:zlib':
         enable_zlib,
+    '//gn:expat':
+        enable_expat,
     '//gn:bionic_kernel_uapi_headers':
         enable_uapi_headers,
     '//src/profiling/memory:bionic_libc_platform_headers_on_android':
diff --git a/tools/gen_bazel b/tools/gen_bazel
index ec48704..4f9df8f 100755
--- a/tools/gen_bazel
+++ b/tools/gen_bazel
@@ -144,6 +144,7 @@
 external_deps = {
     '//gn:default_deps': [],
     '//gn:base_platform': ['PERFETTO_CONFIG.deps.base_platform'],
+    '//gn:expat': ['PERFETTO_CONFIG.deps.expat'],
     '//gn:jsoncpp': ['PERFETTO_CONFIG.deps.jsoncpp'],
     '//gn:linenoise': ['PERFETTO_CONFIG.deps.linenoise'],
     '//gn:protobuf_full': ['PERFETTO_CONFIG.deps.protobuf_full'],
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 771f43a..7b2b8d7 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -257,6 +257,15 @@
         'all',
         'all'),
 
+    # Libexpat for Instruments XML import.
+    # If updating the version, also update bazel/deps.bzl.
+    Dependency(
+        'buildtools/expat/src',
+        'https://chromium.googlesource.com/external/github.com/libexpat/libexpat.git',
+        'fa75b96546c069d17b8f80d91e0f4ef0cde3790d',  # refs/tags/upstream/R_2_6_2.
+        'all',
+        'all'),
+
     # Archive with only the demangling sources from llvm-project.
     # See tools/repackage_llvm_demangler.sh on how to update this.
     # File suffix is the git reference to the commit at which we rearchived the
diff --git a/ui/build.js b/ui/build.js
index 2df02c3..2e6269e 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -575,9 +575,10 @@
 }
 
 function startServer() {
+  const host = cfg.httpServerListenHost == '127.0.0.1' ? 'localhost' : cfg.httpServerListenHost;
   console.log(
       'Starting HTTP server on',
-      `http://${cfg.httpServerListenHost}:${cfg.httpServerListenPort}`);
+      `http://${host}:${cfg.httpServerListenPort}`);
   http.createServer(function(req, res) {
         console.debug(req.method, req.url);
         let uri = req.url.split('?', 1)[0];
diff --git a/ui/release/channels.json b/ui/release/channels.json
index 75a3d6b..23336d5 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,11 +2,11 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "206f403988fb603720111dcabb14f38f6ebc1a54"
+      "rev": "0a53e685b5a2744119b2a7f86cb98bd6c5674480"
     },
     {
       "name": "canary",
-      "rev": "f098f373db79c554c96aaecb68997a798ee4e86f"
+      "rev": "2a491b8cca83a103a80335acc048f6a4a882cb49"
     },
     {
       "name": "autopush",
diff --git a/ui/src/assets/widgets/popup.scss b/ui/src/assets/widgets/popup.scss
index 6d26981..6d18d47 100644
--- a/ui/src/assets/widgets/popup.scss
+++ b/ui/src/assets/widgets/popup.scss
@@ -21,6 +21,13 @@
   // When width = 0 it can cause layout issues in popup content, so we give this
   // element some width manually
   width: 100%;
+
+  // Move the portal to the top of the page. This appears to fix issues where
+  // popups can sometimes be rendered below the rest of the page momentarily,
+  // causing the whole page to judder up and down while popper.js sorts out the
+  // positioning.
+  // TODO(stevegolton): There is probably a better way to fix this issue.
+  top: 0;
 }
 
 .pf-popup {
diff --git a/ui/src/base/fuzzy.ts b/ui/src/base/fuzzy.ts
index 7bc00a1..bd15658 100644
--- a/ui/src/base/fuzzy.ts
+++ b/ui/src/base/fuzzy.ts
@@ -26,14 +26,14 @@
 
 // Finds approx matching in arbitrary lists of items.
 export class FuzzyFinder<T> {
-  private items: T[];
-  private keyLookup: KeyLookup<T>;
+  private readonly items: ReadonlyArray<T>;
+  private readonly keyLookup: KeyLookup<T>;
 
   // Because we operate on arbitrary lists, a key lookup function is required to
   // so we know which part of the list is to be be searched. It should return
   // the relevant search string for each item.
-  constructor(items: ArrayLike<T>, keyLookup: KeyLookup<T>) {
-    this.items = Array.from(items);
+  constructor(items: ReadonlyArray<T>, keyLookup: KeyLookup<T>) {
+    this.items = items;
     this.keyLookup = keyLookup;
   }
 
diff --git a/ui/src/base/hotkeys.ts b/ui/src/base/hotkeys.ts
index 2dec16c..aad5bd6 100644
--- a/ui/src/base/hotkeys.ts
+++ b/ui/src/base/hotkeys.ts
@@ -281,3 +281,26 @@
 export function getPlatform(): Platform {
   return window.navigator.platform.indexOf('Mac') !== -1 ? 'Mac' : 'PC';
 }
+
+// Returns a cross-platform check for whether the event has "Mod" key pressed
+// (e.g. as a part of Mod-Click UX pattern).
+// On Mac, Mod-click is actually Command-click and on PC it's Control-click,
+// so this function handles this for all platforms.
+export function hasModKey(event: {
+  readonly metaKey: boolean;
+  readonly ctrlKey: boolean;
+}): boolean {
+  if (getPlatform() === 'Mac') {
+    return event.metaKey;
+  } else {
+    return event.ctrlKey;
+  }
+}
+
+export function modKey(): {metaKey?: boolean; ctrlKey?: boolean} {
+  if (getPlatform() === 'Mac') {
+    return {metaKey: true};
+  } else {
+    return {ctrlKey: true};
+  }
+}
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 51e765d..577ae09 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,7 +15,7 @@
 import {Draft} from 'immer';
 
 import {SortDirection} from '../base/comparison_utils';
-import {assertExists, assertTrue} from '../base/logging';
+import {assertTrue} from '../base/logging';
 import {duration, time} from '../base/time';
 import {RecordConfig} from '../controller/record_config_types';
 import {randomColor} from '../core/colorizer';
@@ -45,6 +45,7 @@
 } from './metatracing';
 import {
   AdbRecordingTarget,
+  Area,
   EngineMode,
   LoadedConfig,
   NewEngineMode,
@@ -52,28 +53,14 @@
   OmniboxState,
   PendingDeeplinkState,
   PivotTableResult,
-  PrimaryTrackSortKey,
   ProfileType,
   RecordingTarget,
-  SCROLLING_TRACK_GROUP,
   State,
   Status,
-  ThreadTrackSortKey,
-  TrackSortKey,
-  UtidToTrackSortKey,
 } from './state';
 
 type StateDraft = Draft<State>;
 
-export interface AddTrackArgs {
-  key?: string;
-  uri: string;
-  name: string;
-  trackSortKey: TrackSortKey;
-  trackGroup?: string;
-  closeable?: boolean;
-}
-
 export interface PostedTrace {
   buffer: ArrayBuffer;
   title: string;
@@ -127,32 +114,6 @@
   return nextId;
 }
 
-// A helper to clean the state for a given removeable track.
-// This is not exported as action to make it clear that not all
-// tracks are removeable.
-function removeTrack(state: StateDraft, trackKey: string) {
-  const track = state.tracks[trackKey];
-  if (track === undefined) {
-    return;
-  }
-  delete state.tracks[trackKey];
-
-  const removeTrackId = (arr: string[]) => {
-    const index = arr.indexOf(trackKey);
-    if (index !== -1) arr.splice(index, 1);
-  };
-
-  if (track.trackGroup === SCROLLING_TRACK_GROUP) {
-    removeTrackId(state.scrollingTracks);
-  } else if (track.trackGroup !== undefined) {
-    const trackGroup = state.trackGroups[track.trackGroup];
-    if (trackGroup !== undefined) {
-      removeTrackId(trackGroup.tracks);
-    }
-  }
-  state.pinnedTracks = state.pinnedTracks.filter((key) => key !== trackKey);
-}
-
 let statusTraceEvent: TraceEventScope | undefined;
 
 export const StateActions = {
@@ -200,126 +161,6 @@
     state.traceUuid = args.traceUuid;
   },
 
-  addTracks(state: StateDraft, args: {tracks: AddTrackArgs[]}) {
-    args.tracks.forEach((track) => {
-      const trackKey =
-        track.key === undefined ? generateNextId(state) : track.key;
-      const name = track.name;
-      state.tracks[trackKey] = {
-        key: trackKey,
-        name,
-        trackSortKey: track.trackSortKey,
-        trackGroup: track.trackGroup,
-        uri: track.uri,
-        closeable: track.closeable,
-      };
-      if (track.trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.push(trackKey);
-      } else if (track.trackGroup !== undefined) {
-        const group = state.trackGroups[track.trackGroup];
-        if (group !== undefined) {
-          group.tracks.push(trackKey);
-        }
-      }
-    });
-  },
-
-  // Note: While this action has traditionally been omitted, with more and more
-  // dynamic tracks being added and existing ones being moved to plugins, it
-  // makes sense to have a generic "removeTracks" action which is un-opinionated
-  // about what type of tracks we are removing.
-  // E.g. Once debug tracks have been moved to a plugin, it makes no sense to
-  // keep the "removeDebugTrack()" action, as the core should have no concept of
-  // what debug tracks are.
-  removeTracks(state: StateDraft, args: {trackKeys: string[]}) {
-    for (const trackKey of args.trackKeys) {
-      removeTrack(state, trackKey);
-    }
-  },
-
-  setUtidToTrackSortKey(
-    state: StateDraft,
-    args: {threadOrderingMetadata: UtidToTrackSortKey},
-  ) {
-    state.utidToThreadSortKey = args.threadOrderingMetadata;
-  },
-
-  addTrack(state: StateDraft, args: AddTrackArgs): void {
-    this.addTracks(state, {tracks: [args]});
-  },
-
-  addTrackGroup(
-    state: StateDraft,
-    // Define ID in action so a track group can be referred to without running
-    // the reducer.
-    args: {
-      name: string;
-      key: string;
-      summaryTrackKey?: string;
-      collapsed: boolean;
-      fixedOrdering?: boolean;
-    },
-  ): void {
-    state.trackGroups[args.key] = {
-      name: args.name,
-      key: args.key,
-      collapsed: args.collapsed,
-      tracks: [],
-      summaryTrack: args.summaryTrackKey,
-      fixedOrdering: args.fixedOrdering,
-    };
-  },
-
-  maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void {
-    const trackGroups = Object.values(state.trackGroups);
-    if (trackGroups.length === 1) {
-      trackGroups[0].collapsed = false;
-    }
-  },
-
-  sortThreadTracks(state: StateDraft, _: {}) {
-    const getFullKey = (a: string) => {
-      const track = state.tracks[a];
-      const threadTrackSortKey = track.trackSortKey as ThreadTrackSortKey;
-      if (threadTrackSortKey.utid === undefined) {
-        const sortKey = track.trackSortKey as PrimaryTrackSortKey;
-        return [sortKey, 0, 0, 0];
-      }
-      const threadSortKey = state.utidToThreadSortKey[threadTrackSortKey.utid];
-      return [
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        threadSortKey
-          ? threadSortKey.sortKey
-          : PrimaryTrackSortKey.ORDINARY_THREAD,
-        threadSortKey && threadSortKey.tid !== undefined
-          ? threadSortKey.tid
-          : Number.MAX_VALUE,
-        /* eslint-enable */
-        threadTrackSortKey.utid,
-        threadTrackSortKey.priority,
-      ];
-    };
-
-    // Use a numeric collator so threads are sorted as T1, T2, ..., T10, T11,
-    // rather than T1, T10, T11, ..., T2, T20, T21 .
-    const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
-    for (const group of Object.values(state.trackGroups)) {
-      if (group.fixedOrdering) continue;
-
-      group.tracks.sort((a: string, b: string) => {
-        const aRank = getFullKey(a);
-        const bRank = getFullKey(b);
-        for (let i = 0; i < aRank.length; i++) {
-          if (aRank[i] !== bRank[i]) return aRank[i] - bRank[i];
-        }
-
-        const aName = state.tracks[a].name.toLocaleLowerCase();
-        const bName = state.tracks[b].name.toLocaleLowerCase();
-        return coll.compare(aName, bName);
-      });
-    }
-  },
-
   updateAggregateSorting(
     state: StateDraft,
     args: {id: string; column: string},
@@ -349,57 +190,6 @@
     }
   },
 
-  moveTrack(
-    state: StateDraft,
-    args: {srcId: string; op: 'before' | 'after'; dstId: string},
-  ): void {
-    const moveWithinTrackList = (trackList: string[]) => {
-      const newList: string[] = [];
-      for (let i = 0; i < trackList.length; i++) {
-        const curTrackId = trackList[i];
-        if (curTrackId === args.dstId && args.op === 'before') {
-          newList.push(args.srcId);
-        }
-        if (curTrackId !== args.srcId) {
-          newList.push(curTrackId);
-        }
-        if (curTrackId === args.dstId && args.op === 'after') {
-          newList.push(args.srcId);
-        }
-      }
-      trackList.splice(0);
-      newList.forEach((x) => {
-        trackList.push(x);
-      });
-    };
-
-    moveWithinTrackList(state.pinnedTracks);
-    moveWithinTrackList(state.scrollingTracks);
-  },
-
-  toggleTrackPinned(state: StateDraft, args: {trackKey: string}): void {
-    const key = args.trackKey;
-    const isPinned = state.pinnedTracks.includes(key);
-    const trackGroup = assertExists(state.tracks[key]).trackGroup;
-
-    if (isPinned) {
-      state.pinnedTracks.splice(state.pinnedTracks.indexOf(key), 1);
-      if (trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.unshift(key);
-      }
-    } else {
-      if (trackGroup === SCROLLING_TRACK_GROUP) {
-        state.scrollingTracks.splice(state.scrollingTracks.indexOf(key), 1);
-      }
-      state.pinnedTracks.push(key);
-    }
-  },
-
-  toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
-    const trackGroup = assertExists(state.trackGroups[args.groupKey]);
-    trackGroup.collapsed = !trackGroup.collapsed;
-  },
-
   requestTrackReload(state: StateDraft, _: {}) {
     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
     if (state.lastTrackReloadRequest) {
@@ -605,14 +395,14 @@
 
   selectSlice(
     state: StateDraft,
-    args: {id: number; trackKey: string; table?: string; scroll?: boolean},
+    args: {id: number; trackUri: string; table?: string; scroll?: boolean},
   ): void {
     state.selection = {
       kind: 'legacy',
       legacySelection: {
         kind: 'SLICE',
         id: args.id,
-        trackKey: args.trackKey,
+        trackUri: args.trackUri,
         table: args.table,
       },
     };
@@ -626,7 +416,7 @@
       sqlTableName: string;
       start: time;
       duration: duration;
-      trackKey: string;
+      trackUri: string;
       detailsPanelConfig: {
         kind: string;
         config: GenericSliceDetailsTabConfigBase;
@@ -646,7 +436,7 @@
         sqlTableName: args.sqlTableName,
         start: args.start,
         duration: args.duration,
-        trackKey: args.trackKey,
+        trackUri: args.trackUri,
         detailsPanelConfig: {
           kind: args.detailsPanelConfig.kind,
           config: detailsPanelConfig,
@@ -665,14 +455,14 @@
 
   selectThreadState(
     state: StateDraft,
-    args: {id: number; trackKey: string},
+    args: {id: number; trackUri: string},
   ): void {
     state.selection = {
       kind: 'legacy',
       legacySelection: {
         kind: 'THREAD_STATE',
         id: args.id,
-        trackKey: args.trackKey,
+        trackUri: args.trackUri,
       },
     };
   },
@@ -719,57 +509,50 @@
     state.omniboxState.mode = args.mode;
   },
 
-  selectArea(
-    state: StateDraft,
-    args: {start: time; end: time; tracks: string[]},
-  ): void {
-    const {start, end, tracks} = args;
+  selectArea(state: StateDraft, args: Area): void {
+    const {start, end} = args;
     assertTrue(start <= end);
     state.selection = {
       kind: 'area',
-      start,
-      end,
-      tracks,
+      ...args,
     };
   },
 
-  toggleTrackSelection(
-    state: StateDraft,
-    args: {key: string; isTrackGroup: boolean},
-  ) {
+  toggleTrackAreaSelection(state: StateDraft, args: {key: string}) {
     const selection = state.selection;
     if (selection.kind !== 'area') {
       return;
     }
 
-    const index = selection.tracks.indexOf(args.key);
-    if (index > -1) {
-      selection.tracks.splice(index, 1);
-      if (args.isTrackGroup) {
-        // Also remove all child tracks.
-        for (const childTrack of state.trackGroups[args.key].tracks) {
-          const childIndex = selection.tracks.indexOf(childTrack);
-          if (childIndex > -1) {
-            selection.tracks.splice(childIndex, 1);
-          }
-        }
-      }
+    if (!selection.trackUris.includes(args.key)) {
+      selection.trackUris.push(args.key);
     } else {
-      selection.tracks.push(args.key);
-      if (args.isTrackGroup) {
-        // Also add all child tracks.
-        for (const childTrack of state.trackGroups[args.key].tracks) {
-          if (!selection.tracks.includes(childTrack)) {
-            selection.tracks.push(childTrack);
-          }
-        }
-      }
+      selection.trackUris = selection.trackUris.filter((t) => t !== args.key);
     }
-    // It's super unexpected that |toggleTrackSelection| does not cause
-    // selection to be updated and this leads to bugs for people who do:
-    // if (oldSelection !== state.selection) etc.
-    // To solve this re-create the selection object here:
-    state.selection = Object.assign({}, state.selection);
+  },
+
+  toggleGroupAreaSelection(state: StateDraft, args: {trackUris: string[]}) {
+    const currentSelection = state.selection;
+    if (currentSelection.kind !== 'area') {
+      return;
+    }
+
+    const allTracksSelected = args.trackUris.every((t) =>
+      currentSelection.trackUris.includes(t),
+    );
+
+    if (allTracksSelected) {
+      // Deselect all tracks in the list
+      currentSelection.trackUris = currentSelection.trackUris.filter(
+        (t) => !args.trackUris.includes(t),
+      );
+    } else {
+      args.trackUris.forEach((t) => {
+        if (!currentSelection.trackUris.includes(t)) {
+          currentSelection.trackUris.push(t);
+        }
+      });
+    }
   },
 
   setChromeCategories(state: StateDraft, args: {categories: string[]}): void {
@@ -868,18 +651,7 @@
     }
   },
 
-  clearAllPinnedTracks(state: StateDraft, _: {}) {
-    const pinnedTracks = state.pinnedTracks.slice();
-    for (let index = pinnedTracks.length - 1; index >= 0; index--) {
-      const trackKey = pinnedTracks[index];
-      this.toggleTrackPinned(state, {trackKey});
-    }
-  },
-
-  togglePivotTable(
-    state: StateDraft,
-    args: {area?: {start: time; end: time; tracks: string[]}},
-  ) {
+  togglePivotTable(state: StateDraft, args: {area?: Area}) {
     state.nonSerializableState.pivotTable.selectionArea = args.area;
     state.nonSerializableState.pivotTable.queryResult = null;
   },
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 9289d09..26e65ae 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -15,242 +15,10 @@
 import {produce} from 'immer';
 
 import {assertExists} from '../base/logging';
-import {PrimaryTrackSortKey} from '../public';
-import {PROCESS_SCHEDULING_TRACK_KIND} from '../core_plugins/process_summary/process_scheduling_track';
 
 import {StateActions} from './actions';
 import {createEmptyState} from './empty_state';
-import {
-  InThreadTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-  State,
-  TraceUrlSource,
-  TrackSortKey,
-} from './state';
-import {
-  HEAP_PROFILE_TRACK_KIND,
-  THREAD_SLICE_TRACK_KIND,
-  THREAD_STATE_TRACK_KIND,
-} from '../core/track_kinds';
-
-function fakeTrack(
-  state: State,
-  args: {
-    key: string;
-    uri?: string;
-    trackGroup?: string;
-    trackSortKey?: TrackSortKey;
-    name?: string;
-    tid?: string;
-  },
-): State {
-  return produce(state, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: args.uri ?? 'sometrack',
-      key: args.key,
-      name: args.name ?? 'A track',
-      trackSortKey:
-        args.trackSortKey === undefined
-          ? PrimaryTrackSortKey.ORDINARY_TRACK
-          : args.trackSortKey,
-      trackGroup: args.trackGroup ?? SCROLLING_TRACK_GROUP,
-    });
-  });
-}
-
-function fakeTrackGroup(
-  state: State,
-  args: {key: string; summaryTrackKey: string},
-): State {
-  return produce(state, (draft) => {
-    StateActions.addTrackGroup(draft, {
-      name: 'A group',
-      key: args.key,
-      collapsed: false,
-      summaryTrackKey: args.summaryTrackKey,
-    });
-  });
-}
-
-function pinnedAndScrollingTracks(
-  state: State,
-  keys: string[],
-  pinnedTracks: string[],
-  scrollingTracks: string[],
-): State {
-  for (const key of keys) {
-    state = fakeTrack(state, {key});
-  }
-  state = produce(state, (draft) => {
-    draft.pinnedTracks = pinnedTracks;
-    draft.scrollingTracks = scrollingTracks;
-  });
-  return state;
-}
-
-test('add scrolling tracks', () => {
-  const once = produce(createEmptyState(), (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    });
-  });
-  const twice = produce(once, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 2',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    });
-  });
-
-  expect(Object.values(twice.tracks).length).toBe(2);
-  expect(twice.scrollingTracks.length).toBe(2);
-});
-
-test('add track to track group', () => {
-  let state = createEmptyState();
-  state = fakeTrack(state, {key: 's'});
-
-  const afterGroup = produce(state, (draft) => {
-    StateActions.addTrackGroup(draft, {
-      name: 'A track group',
-      key: '123-123-123',
-      summaryTrackKey: 's',
-      collapsed: false,
-    });
-  });
-
-  const afterTrackAdd = produce(afterGroup, (draft) => {
-    StateActions.addTrack(draft, {
-      key: '1',
-      uri: 'slices',
-      name: 'renderer 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: '123-123-123',
-    });
-  });
-
-  expect(afterTrackAdd.trackGroups['123-123-123'].summaryTrack).toBe('s');
-  expect(afterTrackAdd.trackGroups['123-123-123'].tracks[0]).toBe('1');
-});
-
-test('reorder tracks', () => {
-  const once = produce(createEmptyState(), (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 2',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  });
-
-  const firstTrackKey = once.scrollingTracks[0];
-  const secondTrackKey = once.scrollingTracks[1];
-
-  const twice = produce(once, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: `${firstTrackKey}`,
-      op: 'after',
-      dstId: `${secondTrackKey}`,
-    });
-  });
-
-  expect(twice.scrollingTracks[0]).toBe(secondTrackKey);
-  expect(twice.scrollingTracks[1]).toBe(firstTrackKey);
-});
-
-test('reorder pinned to scrolling', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'b',
-      op: 'before',
-      dstId: 'c',
-    });
-  });
-
-  expect(after.pinnedTracks).toEqual(['a']);
-  expect(after.scrollingTracks).toEqual(['b', 'c']);
-});
-
-test('reorder scrolling to pinned', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'b',
-      op: 'after',
-      dstId: 'a',
-    });
-  });
-
-  expect(after.pinnedTracks).toEqual(['a', 'b']);
-  expect(after.scrollingTracks).toEqual(['c']);
-});
-
-test('reorder clamp bottom', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'a',
-      op: 'before',
-      dstId: 'a',
-    });
-  });
-  expect(after).toEqual(state);
-});
-
-test('reorder clamp top', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.moveTrack(draft, {
-      srcId: 'c',
-      op: 'after',
-      dstId: 'c',
-    });
-  });
-  expect(after).toEqual(state);
-});
-
-test('pin', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a'], ['b', 'c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.toggleTrackPinned(draft, {
-      trackKey: 'c',
-    });
-  });
-  expect(after.pinnedTracks).toEqual(['a', 'c']);
-  expect(after.scrollingTracks).toEqual(['b']);
-});
-
-test('unpin', () => {
-  let state = createEmptyState();
-  state = pinnedAndScrollingTracks(state, ['a', 'b', 'c'], ['a', 'b'], ['c']);
-
-  const after = produce(state, (draft) => {
-    StateActions.toggleTrackPinned(draft, {
-      trackKey: 'a',
-    });
-  });
-  expect(after.pinnedTracks).toEqual(['b']);
-  expect(after.scrollingTracks).toEqual(['a', 'c']);
-});
+import {TraceUrlSource} from './state';
 
 test('open trace', () => {
   const state = createEmptyState();
@@ -275,15 +43,7 @@
     });
   });
 
-  const twice = produce(once, (draft) => {
-    StateActions.addTrack(draft, {
-      uri: 'cpu',
-      name: 'Cpu 1',
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    });
-  });
-
-  const thrice = produce(twice, (draft) => {
+  const thrice = produce(once, (draft) => {
     StateActions.openTraceFromUrl(draft, {
       url: 'https://example.com/foo',
     });
@@ -293,8 +53,6 @@
   expect((thrice.engine!!.source as TraceUrlSource).url).toBe(
     'https://example.com/foo',
   );
-  expect(thrice.pinnedTracks.length).toBe(0);
-  expect(thrice.scrollingTracks.length).toBe(0);
 });
 
 test('setEngineReady with missing engine is ignored', () => {
@@ -323,137 +81,3 @@
   });
   expect(after.engine!!.ready).toBe(true);
 });
-
-test('sortTracksByPriority', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: HEAP_PROFILE_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: PROCESS_SCHEDULING_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
-    trackGroup: 'g',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  // High Priority tracks should be sorted before Low Priority tracks:
-  // 'b' appears twice because it's the summary track
-  expect(after.trackGroups['g'].tracks).toEqual(['a', 'b']);
-});
-
-test('sortTracksByPriorityAndKindAndName', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: PROCESS_SCHEDULING_TRACK_KIND,
-    trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.MAIN_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'c',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.RENDER_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'd',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    trackSortKey: PrimaryTrackSortKey.GPU_COMPLETION_THREAD,
-  });
-  state = fakeTrack(state, {
-    key: 'e',
-    uri: HEAP_PROFILE_TRACK_KIND,
-    trackGroup: 'g',
-  });
-  state = fakeTrack(state, {
-    key: 'f',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    name: 'T2',
-  });
-  state = fakeTrack(state, {
-    key: 'g',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackGroup: 'g',
-    name: 'T10',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  // The order should be determined by:
-  // 1.High priority
-  // 2.Non ordinary track kinds
-  // 3.Low priority
-  // 4.Collated name string (ie. 'T2' will be before 'T10')
-  expect(after.trackGroups['g'].tracks).toEqual([
-    'a',
-    'b',
-    'c',
-    'd',
-    'e',
-    'f',
-    'g',
-  ]);
-});
-
-test('sortTracksByTidThenName', () => {
-  let state = createEmptyState();
-  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'a'});
-  state = fakeTrack(state, {
-    key: 'a',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackSortKey: {
-      utid: 1,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'aaa',
-    tid: '1',
-  });
-  state = fakeTrack(state, {
-    key: 'b',
-    uri: THREAD_SLICE_TRACK_KIND,
-    trackSortKey: {
-      utid: 2,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'bbb',
-    tid: '2',
-  });
-  state = fakeTrack(state, {
-    key: 'c',
-    uri: THREAD_STATE_TRACK_KIND,
-    trackSortKey: {
-      utid: 1,
-      priority: InThreadTrackSortKey.ORDINARY,
-    },
-    trackGroup: 'g',
-    name: 'ccc',
-    tid: '1',
-  });
-
-  const after = produce(state, (draft) => {
-    StateActions.sortThreadTracks(draft, {});
-  });
-
-  expect(after.trackGroups['g'].tracks).toEqual(['a', 'c', 'b']);
-});
diff --git a/ui/src/common/arg_types.ts b/ui/src/common/arg_types.ts
index 551333b..b54d2b7 100644
--- a/ui/src/common/arg_types.ts
+++ b/ui/src/common/arg_types.ts
@@ -14,5 +14,5 @@
 
 export type ArgValue =
   | string
-  | {kind: 'SCHED_SLICE'; trackId: string; sliceId: number; rawValue: string};
+  | {kind: 'SCHED_SLICE'; trackUri: string; sliceId: number; rawValue: string};
 export type Args = Map<string, ArgValue>;
diff --git a/ui/src/common/commands.ts b/ui/src/common/commands.ts
index 2375559..6465329 100644
--- a/ui/src/common/commands.ts
+++ b/ui/src/common/commands.ts
@@ -27,6 +27,10 @@
     return this.registry.get(commandId);
   }
 
+  hasCommand(commandId: string): boolean {
+    return this.registry.has(commandId);
+  }
+
   get commands(): Command[] {
     return Array.from(this.registry.values());
   }
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 1c9585d..e031148 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -94,12 +94,7 @@
     version: STATE_VERSION,
     nextId: '-1',
     newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
-    tracks: {},
-    utidToThreadSortKey: {},
     aggregatePreferences: {},
-    trackGroups: {},
-    pinnedTracks: [],
-    scrollingTracks: [],
     queries: {},
     notes: {},
 
diff --git a/ui/src/common/legacy_flamegraph_unittest.ts b/ui/src/common/legacy_flamegraph_unittest.ts
deleted file mode 100644
index 222b2e2..0000000
--- a/ui/src/common/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,1072 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {CallsiteInfo, mergeCallsites} from './legacy_flamegraph_util';
-
-test('zeroCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 2,
-      name: 'B4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('zeroCallsitesMerged2', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 6,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 2,
-      name: 'B5',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 5);
-
-  // Small callsites are not next ot each other, nothing should be changed.
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('twoCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 6);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('manyCallsitesMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: 'A36',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 5,
-      name: 'A58',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 3,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsites A36, A47
-  // and A58 should also be merged since their parents are merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('manyCallsitesMergedWithoutChildren', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 4,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 2,
-      name: 'B6',
-      depth: 1,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 3,
-      name: 'A47',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 6,
-      name: 'B68',
-      depth: 2,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotNextToEachOtherInArray', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 1,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedMergedCallsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 4,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 8,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 4);
-
-  // In this case, callsites A3, A4 and A5 should be merged since they are
-  // smaller then 4 and are on same depth with same parent. Callsite A47
-  // should not be merged with B68 althought they are small because they don't
-  // have sam parent. A47 should now have parent A3 because A4 is merged.
-  expect(mergedCallsites).toEqual(expectedMergedCallsites);
-});
-
-test('smallCallsitesNotMerged', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: 1,
-      name: 'A2',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 1);
-
-  expect(mergedCallsites).toEqual(callsites);
-});
-
-test('mergingRootCallsites', () => {
-  const callsites: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const mergedCallsites = mergeCallsites(callsites, 20);
-
-  expect(mergedCallsites).toEqual([
-    {
-      id: 1,
-      parentId: -1,
-      name: '[merged]',
-      depth: 0,
-      totalSize: 12,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-  ]);
-});
-
-test('largerFlamegraph', () => {
-  const data: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: 'A4',
-      depth: 1,
-      totalSize: 15,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 5,
-      parentId: 1,
-      name: 'A5',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 6,
-      parentId: 1,
-      name: 'A6',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: 'A410',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 11,
-      parentId: 4,
-      name: 'A411',
-      depth: 2,
-      totalSize: 3,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 12,
-      parentId: 4,
-      name: 'A412',
-      depth: 2,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 13,
-      parentId: 5,
-      name: 'A513',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 14,
-      parentId: 5,
-      name: 'A514',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: 'A715',
-      depth: 2,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 16,
-      parentId: 7,
-      name: 'A716',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 17,
-      parentId: 7,
-      name: 'A717',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 18,
-      parentId: 7,
-      name: 'A718',
-      depth: 2,
-      totalSize: 5,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 17,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  const expectedData: CallsiteInfo[] = [
-    {
-      id: 1,
-      parentId: -1,
-      name: 'A',
-      depth: 0,
-      totalSize: 60,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 2,
-      parentId: -1,
-      name: 'B',
-      depth: 0,
-      totalSize: 40,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 3,
-      parentId: 1,
-      name: 'A3',
-      depth: 1,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 4,
-      parentId: 1,
-      name: '[merged]',
-      depth: 1,
-      totalSize: 35,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 7,
-      parentId: 2,
-      name: 'B7',
-      depth: 1,
-      totalSize: 30,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 8,
-      parentId: 2,
-      name: 'B8',
-      depth: 1,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 9,
-      parentId: 3,
-      name: 'A39',
-      depth: 2,
-      totalSize: 20,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 10,
-      parentId: 4,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 15,
-      parentId: 7,
-      name: '[merged]',
-      depth: 2,
-      totalSize: 25,
-      selfSize: 0,
-      mapping: 'x',
-      merged: true,
-      highlighted: false,
-    },
-    {
-      id: 19,
-      parentId: 9,
-      name: 'A919',
-      depth: 3,
-      totalSize: 10,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-    {
-      id: 20,
-      parentId: 15,
-      name: 'A1720',
-      depth: 3,
-      totalSize: 2,
-      selfSize: 0,
-      mapping: 'x',
-      merged: false,
-      highlighted: false,
-    },
-  ];
-
-  // In this case, on depth 1, callsites A4, A5 and A6 should be merged and
-  // initiate merging of their children A410, A411, A412, A513, A514. On depth2,
-  // callsites A715, A716, A717 and A718 should be merged.
-  const actualData = mergeCallsites(data, 16);
-
-  expect(actualData).toEqual(expectedData);
-});
diff --git a/ui/src/common/legacy_flamegraph_util.ts b/ui/src/common/legacy_flamegraph_util.ts
deleted file mode 100644
index cb540dd..0000000
--- a/ui/src/common/legacy_flamegraph_util.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {featureFlags} from '../core/feature_flags';
-import {ProfileType} from './state';
-
-export enum FlamegraphViewingOption {
-  SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY = 'SPACE',
-  ALLOC_SPACE_MEMORY_ALLOCATED_KEY = 'ALLOC_SPACE',
-  OBJECTS_ALLOCATED_NOT_FREED_KEY = 'OBJECTS',
-  OBJECTS_ALLOCATED_KEY = 'ALLOC_OBJECTS',
-  PERF_SAMPLES_KEY = 'PERF_SAMPLES',
-  DOMINATOR_TREE_OBJ_SIZE_KEY = 'DOMINATED_OBJ_SIZE',
-  DOMINATOR_TREE_OBJ_COUNT_KEY = 'DOMINATED_OBJ_COUNT',
-}
-
-interface ViewingOption {
-  option: FlamegraphViewingOption;
-  name: string;
-}
-
-export interface CallsiteInfo {
-  id: number;
-  parentId: number;
-  depth: number;
-  name?: string;
-  totalSize: number;
-  selfSize: number;
-  mapping: string;
-  merged: boolean;
-  highlighted: boolean;
-  location?: string;
-}
-
-export const SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG = featureFlags.register({
-  id: 'showHeapGraphDominatorTree',
-  name: 'Show heap graph dominator tree',
-  description: 'Show dominated size and objects tabs in Java heap graph view.',
-  defaultValue: true,
-});
-
-export function viewingOptions(profileType: ProfileType): Array<ViewingOption> {
-  switch (profileType) {
-    case ProfileType.PERF_SAMPLE:
-      return [
-        {
-          option: FlamegraphViewingOption.PERF_SAMPLES_KEY,
-          name: 'Samples',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Objects',
-        },
-      ].concat(
-        SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-          ? [
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-                name: 'Dominated size',
-              },
-              {
-                option: FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-                name: 'Dominated objects',
-              },
-            ]
-          : [],
-      );
-    case ProfileType.HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total count',
-        },
-      ];
-    case ProfileType.NATIVE_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY,
-          name: 'Unreleased malloc count',
-        },
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total malloc size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total malloc count',
-        },
-      ];
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count',
-        },
-      ];
-    case ProfileType.MIXED_HEAP_PROFILE:
-      return [
-        {
-          option: FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY,
-          name: 'Total allocation size (malloc + java)',
-        },
-        {
-          option: FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY,
-          name: 'Total allocation count (malloc + java)',
-        },
-      ];
-    default:
-      const exhaustiveCheck: never = profileType;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-export function defaultViewingOption(
-  profileType: ProfileType,
-): FlamegraphViewingOption {
-  return viewingOptions(profileType)[0].option;
-}
-
-export function expandCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  clickedCallsiteIndex: number,
-): ReadonlyArray<CallsiteInfo> {
-  if (clickedCallsiteIndex === -1) return data;
-  const expandedCallsites: CallsiteInfo[] = [];
-  if (clickedCallsiteIndex >= data.length || clickedCallsiteIndex < -1) {
-    return expandedCallsites;
-  }
-  const clickedCallsite = data[clickedCallsiteIndex];
-  expandedCallsites.unshift(clickedCallsite);
-  // Adding parents
-  let parentId = clickedCallsite.parentId;
-  while (parentId > -1) {
-    expandedCallsites.unshift(data[parentId]);
-    parentId = data[parentId].parentId;
-  }
-  // Adding children
-  const parents: number[] = [];
-  parents.push(clickedCallsiteIndex);
-  for (let i = clickedCallsiteIndex + 1; i < data.length; i++) {
-    const element = data[i];
-    if (parents.includes(element.parentId)) {
-      expandedCallsites.push(element);
-      parents.push(element.id);
-    }
-  }
-  return expandedCallsites;
-}
-
-// Merge callsites that have approximately width less than
-// MIN_PIXEL_DISPLAYED. All small callsites in the same depth and with same
-// parent will be merged to one with total size of all merged callsites.
-export function mergeCallsites(
-  data: ReadonlyArray<CallsiteInfo>,
-  minSizeDisplayed: number,
-) {
-  const mergedData: CallsiteInfo[] = [];
-  const mergedCallsites: Map<number, number> = new Map();
-  for (let i = 0; i < data.length; i++) {
-    // When a small callsite is found, it will be merged with other small
-    // callsites of the same depth. So if the current callsite has already been
-    // merged we can skip it.
-    if (mergedCallsites.has(data[i].id)) {
-      continue;
-    }
-    const copiedCallsite = copyCallsite(data[i]);
-    copiedCallsite.parentId = getCallsitesParentHash(
-      copiedCallsite,
-      mergedCallsites,
-    );
-
-    let mergedAny = false;
-    // If current callsite is small, find other small callsites with same depth
-    // and parent and merge them into the current one, marking them as merged.
-    if (copiedCallsite.totalSize <= minSizeDisplayed && i + 1 < data.length) {
-      let j = i + 1;
-      let nextCallsite = data[j];
-      while (j < data.length && copiedCallsite.depth === nextCallsite.depth) {
-        if (
-          copiedCallsite.parentId ===
-            getCallsitesParentHash(nextCallsite, mergedCallsites) &&
-          nextCallsite.totalSize <= minSizeDisplayed
-        ) {
-          copiedCallsite.totalSize += nextCallsite.totalSize;
-          mergedCallsites.set(nextCallsite.id, copiedCallsite.id);
-          mergedAny = true;
-        }
-        j++;
-        nextCallsite = data[j];
-      }
-      if (mergedAny) {
-        copiedCallsite.name = '[merged]';
-        copiedCallsite.merged = true;
-      }
-    }
-    mergedData.push(copiedCallsite);
-  }
-  return mergedData;
-}
-
-function copyCallsite(callsite: CallsiteInfo): CallsiteInfo {
-  return {
-    id: callsite.id,
-    parentId: callsite.parentId,
-    depth: callsite.depth,
-    name: callsite.name,
-    totalSize: callsite.totalSize,
-    mapping: callsite.mapping,
-    selfSize: callsite.selfSize,
-    merged: callsite.merged,
-    highlighted: callsite.highlighted,
-    location: callsite.location,
-  };
-}
-
-function getCallsitesParentHash(
-  callsite: CallsiteInfo,
-  map: Map<number, number>,
-): number {
-  return map.has(callsite.parentId)
-    ? +map.get(callsite.parentId)!
-    : callsite.parentId;
-}
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index d57c6d1..e6196f3 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -12,8 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {Registry} from '../base/registry';
 import {TimeSpan, time} from '../base/time';
 import {globals} from '../frontend/globals';
@@ -26,18 +24,13 @@
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
-  PrimaryTrackSortKey,
   Store,
   TabDescriptor,
   TrackDescriptor,
-  TrackPredicate,
-  GroupPredicate,
-  TrackRef,
   SidebarMenuItem,
 } from '../public';
 import {EngineBase, Engine} from '../trace_processor/engine';
 import {Actions} from './actions';
-import {SCROLLING_TRACK_GROUP} from './state';
 import {addQueryResultsTab} from '../frontend/query_result_tab';
 import {Flag, featureFlags} from '../core/feature_flags';
 import {assertExists} from '../base/logging';
@@ -47,6 +40,7 @@
 import {horizontalScrollToTs} from '../frontend/scroll_helper';
 import {DisposableStack} from '../base/disposable_stack';
 import {TraceContext} from '../frontend/trace_context';
+import {Workspace} from '../public/workspace';
 
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
@@ -125,19 +119,15 @@
     this.trash.use(dispose);
   }
 
-  addDefaultTrack(track: TrackRef): void {
+  registerTrackAndShowOnTraceLoad(track: TrackDescriptor): void {
+    this.registerTrack(track);
+
     // Silently ignore if context is dead.
     if (!this.alive) return;
-
-    const dispose = globals.trackManager.addPotentialTrack(track);
+    const dispose = globals.trackManager.autoShowOnTraceLoad(track);
     this.trash.use(dispose);
   }
 
-  registerStaticTrack(track: TrackDescriptor & TrackRef): void {
-    this.registerTrack(track);
-    this.addDefaultTrack(track);
-  }
-
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   runCommand(id: string, ...args: any[]): any {
     return this.ctx.runCommand(id, ...args);
@@ -182,130 +172,6 @@
   }
 
   readonly timeline = {
-    // Add a new track to the timeline, returning its key.
-    addTrack(uri: string, displayName: string): string {
-      const trackKey = uuidv4();
-      globals.dispatch(
-        Actions.addTrack({
-          key: trackKey,
-          uri,
-          name: displayName,
-          trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-        }),
-      );
-      return trackKey;
-    },
-
-    removeTrack(key: string): void {
-      globals.dispatch(Actions.removeTracks({trackKeys: [key]}));
-    },
-
-    pinTrack(key: string) {
-      if (!isPinned(key)) {
-        globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
-      }
-    },
-
-    unpinTrack(key: string) {
-      if (isPinned(key)) {
-        globals.dispatch(Actions.toggleTrackPinned({trackKey: key}));
-      }
-    },
-
-    pinTracksByPredicate(predicate: TrackPredicate) {
-      const tracks = Object.values(globals.state.tracks);
-      for (const track of tracks) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackDesc && predicate(trackDesc) && !isPinned(track.key)) {
-          globals.dispatch(
-            Actions.toggleTrackPinned({
-              trackKey: track.key,
-            }),
-          );
-        }
-      }
-    },
-
-    unpinTracksByPredicate(predicate: TrackPredicate) {
-      const tracks = Object.values(globals.state.tracks);
-      for (const track of tracks) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackDesc && predicate(trackDesc) && isPinned(track.key)) {
-          globals.dispatch(
-            Actions.toggleTrackPinned({
-              trackKey: track.key,
-            }),
-          );
-        }
-      }
-    },
-
-    removeTracksByPredicate(predicate: TrackPredicate) {
-      const trackKeysToRemove = Object.values(globals.state.tracks)
-        .filter((track) => {
-          const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-          return trackDesc && predicate(trackDesc);
-        })
-        .map((trackState) => trackState.key);
-
-      globals.dispatch(Actions.removeTracks({trackKeys: trackKeysToRemove}));
-    },
-
-    expandGroupsByPredicate(predicate: GroupPredicate) {
-      const groups = globals.state.trackGroups;
-      const groupsToExpand = Object.values(groups)
-        .filter((group) => group.collapsed)
-        .filter((group) => {
-          const ref = {
-            displayName: group.name,
-            collapsed: group.collapsed,
-          };
-          return predicate(ref);
-        })
-        .map((group) => group.key);
-
-      for (const groupKey of groupsToExpand) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
-      }
-    },
-
-    collapseGroupsByPredicate(predicate: GroupPredicate) {
-      const groups = globals.state.trackGroups;
-      const groupsToCollapse = Object.values(groups)
-        .filter((group) => !group.collapsed)
-        .filter((group) => {
-          const ref = {
-            displayName: group.name,
-            collapsed: group.collapsed,
-          };
-          return predicate(ref);
-        })
-        .map((group) => group.key);
-
-      for (const groupKey of groupsToCollapse) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
-      }
-    },
-
-    get tracks(): TrackRef[] {
-      const tracks = Object.values(globals.state.tracks);
-      const pinnedTracks = globals.state.pinnedTracks;
-      const groups = globals.state.trackGroups;
-      return tracks.map((trackState) => {
-        const group = trackState.trackGroup
-          ? groups[trackState.trackGroup]
-          : undefined;
-        return {
-          title: trackState.name,
-          uri: trackState.uri,
-          key: trackState.key,
-          groupName: group?.name,
-          isPinned: pinnedTracks.includes(trackState.key),
-        };
-      });
-    },
-
     panToTimestamp(ts: time): void {
       horizontalScrollToTs(ts);
     },
@@ -317,6 +183,10 @@
     get viewport(): TimeSpan {
       return globals.timeline.visibleWindow.toTimeSpan();
     },
+
+    get workspace(): Workspace {
+      return globals.workspace;
+    },
   };
 
   [Symbol.dispose]() {
@@ -348,10 +218,6 @@
   }
 }
 
-function isPinned(trackId: string): boolean {
-  return globals.state.pinnedTracks.includes(trackId);
-}
-
 // 'Static' registry of all known plugins.
 export class PluginRegistry extends Registry<PluginDescriptor> {
   constructor() {
diff --git a/ui/src/common/search_data.ts b/ui/src/common/search_data.ts
index 7209c04..cd5856b 100644
--- a/ui/src/common/search_data.ts
+++ b/ui/src/common/search_data.ts
@@ -24,7 +24,7 @@
   eventIds: Float64Array;
   tses: BigInt64Array;
   utids: Float64Array;
-  trackKeys: string[];
+  trackUris: string[];
   sources: SearchSource[];
   totalResults: number;
 }
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 3ad7c5d..ceb26cc 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -40,36 +40,6 @@
   CpuProfileSampleSelection,
 } from '../core/selection_manager';
 
-// Tracks within track groups (usually corresponding to processes) are sorted.
-// As we want to group all tracks related to a given thread together, we use
-// two keys:
-// - Primary key corresponds to a priority of a track block (all tracks related
-//   to a given thread or a single track if it's not thread-associated).
-// - Secondary key corresponds to a priority of a given thread-associated track
-//   within its thread track block.
-// Each track will have a sort key, which either a primary sort key
-// (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
-// primary sort key is done independently).
-export enum PrimaryTrackSortKey {
-  DEBUG_TRACK,
-  NULL_TRACK,
-  PROCESS_SCHEDULING_TRACK,
-  PROCESS_SUMMARY_TRACK,
-  EXPECTED_FRAMES_SLICE_TRACK,
-  ACTUAL_FRAMES_SLICE_TRACK,
-  PERF_SAMPLES_PROFILE_TRACK,
-  HEAP_PROFILE_TRACK,
-  MAIN_THREAD,
-  RENDER_THREAD,
-  GPU_COMPLETION_THREAD,
-  CHROME_IO_THREAD,
-  CHROME_COMPOSITOR_THREAD,
-  ORDINARY_THREAD,
-  COUNTER_TRACK,
-  ASYNC_SLICE_TRACK,
-  ORDINARY_TRACK,
-}
-
 /**
  * A plain js object, holding objects of type |Class| keyed by string id.
  * We use this instead of using |Map| object since it is simpler and faster to
@@ -95,7 +65,7 @@
 export interface Area {
   start: time;
   end: time;
-  tracks: string[];
+  trackUris: string[];
 }
 
 export const MAX_TIME = 180;
@@ -175,34 +145,6 @@
 
 export type NewEngineMode = 'USE_HTTP_RPC_IF_AVAILABLE' | 'FORCE_BUILTIN_WASM';
 
-// Key that is used to sort tracks within a block of tracks associated with a
-// given thread.
-export enum InThreadTrackSortKey {
-  THREAD_COUNTER_TRACK,
-  THREAD_SCHEDULING_STATE_TRACK,
-  CPU_STACK_SAMPLES_TRACK,
-  VISUALISED_ARGS_TRACK,
-  ORDINARY,
-  DEFAULT_TRACK,
-}
-
-// Sort key used for sorting tracks associated with a thread.
-export type ThreadTrackSortKey = {
-  utid: number;
-  priority: InThreadTrackSortKey;
-};
-
-// Sort key for all tracks: both thread-associated and non-thread associated.
-export type TrackSortKey = PrimaryTrackSortKey | ThreadTrackSortKey;
-
-// Mapping which defines order for threads within a given process.
-export type UtidToTrackSortKey = {
-  [utid: number]: {
-    tid?: number;
-    sortKey: PrimaryTrackSortKey;
-  };
-};
-
 export interface TraceFileSource {
   type: 'FILE';
   file: File;
@@ -245,24 +187,6 @@
   | TraceUrlSource
   | TraceHttpRpcSource;
 
-export interface TrackState {
-  uri: string;
-  key: string;
-  name: string;
-  trackSortKey: TrackSortKey;
-  trackGroup?: string;
-  closeable?: boolean;
-}
-
-export interface TrackGroupState {
-  key: string;
-  name: string;
-  collapsed: boolean;
-  tracks: string[]; // Child track ids.
-  fixedOrdering?: boolean; // Render tracks without sorting.
-  summaryTrack: string | undefined;
-}
-
 export interface EngineConfig {
   id: string;
   mode?: EngineMode; // Is undefined until |ready| is true.
@@ -348,11 +272,7 @@
 }
 
 // Input parameters to check whether the pivot table needs to be re-queried.
-export interface PivotTableAreaState {
-  start: time;
-  end: time;
-  tracks: string[];
-}
+export type PivotTableAreaState = Area;
 
 export interface PivotTableState {
   // Currently selected area, if null, pivot table is not going to be visible.
@@ -433,12 +353,8 @@
   newEngineMode: NewEngineMode;
   engine?: EngineConfig;
   traceUuid?: string;
-  trackGroups: ObjectByKey<TrackGroupState>;
-  tracks: ObjectByKey<TrackState>;
-  utidToThreadSortKey: UtidToTrackSortKey;
+
   aggregatePreferences: ObjectById<AggregationState>;
-  scrollingTracks: string[];
-  pinnedTracks: string[];
   debugTrackId?: string;
   lastTrackReloadRequest?: number;
   queries: ObjectById<QueryConfig>;
@@ -863,21 +779,6 @@
   ];
 }
 
-export function getContainingGroupKey(
-  state: State,
-  trackKey: string,
-): null | string {
-  const track = state.tracks[trackKey];
-  if (track === undefined) {
-    return null;
-  }
-  const parentGroupKey = track.trackGroup;
-  if (!parentGroupKey) {
-    return null;
-  }
-  return parentGroupKey;
-}
-
 export function getLegacySelection(state: State): LegacySelection | null {
   return selectionToLegacySelection(state.selection);
 }
diff --git a/ui/src/common/state_serialization.ts b/ui/src/common/state_serialization.ts
index b72551c..ad7479c 100644
--- a/ui/src/common/state_serialization.ts
+++ b/ui/src/common/state_serialization.ts
@@ -81,7 +81,7 @@
   if (stateSel.kind === 'single') {
     selection.push({
       kind: 'TRACK_EVENT',
-      trackKey: stateSel.trackKey,
+      trackKey: stateSel.trackUri,
       eventId: stateSel.eventId.toString(),
     });
   } else if (stateSel.kind === 'legacy') {
@@ -123,7 +123,7 @@
 
   return {
     version: SERIALIZED_STATE_VERSION,
-    pinnedTracks: globals.state.pinnedTracks,
+    pinnedTracks: globals.workspace.pinnedTracks.map((t) => t.uri),
     viewport: {
       start: vizWindow.start,
       end: vizWindow.end,
@@ -186,16 +186,16 @@
       new TimeSpan(appState.viewport.start, appState.viewport.end),
     );
   }
-  globals.store.edit((draft) => {
-    // Restore the pinned tracks, if they exist.
-    const tracksToPin: string[] = [];
-    for (const trackKey of appState.pinnedTracks) {
-      if (trackKey in globals.state.tracks) {
-        tracksToPin.push(trackKey);
-      }
-    }
-    draft.pinnedTracks = tracksToPin;
 
+  // Restore the pinned tracks, if they exist.
+  for (const uri of appState.pinnedTracks) {
+    const track = globals.workspace.getTrackByUri(uri);
+    if (track) {
+      track.pin();
+    }
+  }
+
+  globals.store.edit((draft) => {
     // Restore notes.
     for (const note of appState.notes) {
       const commonArgs = {
@@ -226,7 +226,7 @@
         case 'TRACK_EVENT':
           draft.selection = {
             kind: 'single',
-            trackKey: sel.trackKey,
+            trackUri: sel.trackKey,
             eventId: parseInt(sel.eventId),
           };
           break;
diff --git a/ui/src/common/state_serialization_schema.ts b/ui/src/common/state_serialization_schema.ts
index 7adbecb..27737a9 100644
--- a/ui/src/common/state_serialization_schema.ts
+++ b/ui/src/common/state_serialization_schema.ts
@@ -31,6 +31,7 @@
 const SELECTION_SCHEMA = z.discriminatedUnion('kind', [
   z.object({
     kind: z.literal('TRACK_EVENT'),
+    // This is actually the track URI but let's not rename for backwards compat
     trackKey: z.string(),
     eventId: z.string(),
   }),
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 3dc65e2..af8f5f9 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -12,34 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {PrimaryTrackSortKey} from '../public';
-
 import {createEmptyState} from './empty_state';
-import {getContainingGroupKey, State} from './state';
+import {State} from './state';
 
 test('createEmptyState', () => {
   const state: State = createEmptyState();
   expect(state.engine).toEqual(undefined);
 });
-
-test('getContainingTrackId', () => {
-  const state: State = createEmptyState();
-  state.tracks['a'] = {
-    key: 'a',
-    uri: 'Foo',
-    name: 'a track',
-    trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-  };
-
-  state.tracks['b'] = {
-    key: 'b',
-    uri: 'Foo',
-    name: 'b track',
-    trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    trackGroup: 'containsB',
-  };
-
-  expect(getContainingGroupKey(state, 'z')).toEqual(null);
-  expect(getContainingGroupKey(state, 'a')).toEqual(null);
-  expect(getContainingGroupKey(state, 'b')).toEqual('containsB');
-});
diff --git a/ui/src/common/track_cache.ts b/ui/src/common/track_cache.ts
deleted file mode 100644
index 5234240..0000000
--- a/ui/src/common/track_cache.ts
+++ /dev/null
@@ -1,270 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Optional, exists} from '../base/utils';
-import {Registry} from '../base/registry';
-import {Store} from '../base/store';
-import {Track, TrackContext, TrackDescriptor, TrackRef} from '../public';
-
-import {ObjectByKey, State, TrackState} from './state';
-import {AsyncLimiter} from '../base/async_limiter';
-import {assertFalse} from '../base/logging';
-import {TrackRenderContext} from '../public/tracks';
-
-export interface TrackCacheEntry extends Disposable {
-  readonly trackKey: string;
-  readonly track: Track;
-  desc: TrackDescriptor;
-  render(ctx: TrackRenderContext): void;
-  getError(): Optional<Error>;
-}
-
-// This class is responsible for managing the lifecycle of tracks over render
-// cycles.
-
-// Example usage:
-// function render() {
-//   const trackCache = new TrackCache();
-//   const foo = trackCache.resolveTrack('foo', 'exampleURI', {});
-//   const bar = trackCache.resolveTrack('bar', 'exampleURI', {});
-//   trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
-// }
-
-// Example of how flushing works:
-// First cycle
-//   resolveTrack('foo', ...) <-- new track 'foo' created
-//   resolveTrack('bar', ...) <-- new track 'bar' created
-//   flushTracks()
-// Second cycle
-//   resolveTrack('foo', ...) <-- returns cached 'foo' track
-//   flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
-// Third cycle
-//   flushTracks() <-- 'foo' is destroyed.
-export class TrackManager {
-  private _trackKeyByTrackId = new Map<number, string>();
-  private newTracks = new Map<string, TrackCacheEntry>();
-  private currentTracks = new Map<string, TrackCacheEntry>();
-  private trackRegistry = new Registry<TrackDescriptor>(({uri}) => uri);
-  private defaultTracks = new Set<TrackRef>();
-
-  private store: Store<State>;
-  private trackState?: ObjectByKey<TrackState>;
-
-  constructor(store: Store<State>) {
-    this.store = store;
-  }
-
-  get trackKeyByTrackId() {
-    this.updateTrackKeyByTrackIdMap();
-    return this._trackKeyByTrackId;
-  }
-
-  registerTrack(trackDesc: TrackDescriptor): Disposable {
-    return this.trackRegistry.register(trackDesc);
-  }
-
-  addPotentialTrack(track: TrackRef): Disposable {
-    this.defaultTracks.add(track);
-    return {
-      [Symbol.dispose]: () => this.defaultTracks.delete(track),
-    };
-  }
-
-  findPotentialTracks(): TrackRef[] {
-    return Array.from(this.defaultTracks);
-  }
-
-  getAllTracks(): TrackDescriptor[] {
-    return Array.from(this.trackRegistry.values());
-  }
-
-  // Look up track into for a given track's URI.
-  // Returns |undefined| if no track can be found.
-  resolveTrackInfo(uri: string): TrackDescriptor | undefined {
-    return this.trackRegistry.tryGet(uri);
-  }
-
-  // Creates a new track using |uri| and |params| or retrieves a cached track if
-  // |key| exists in the cache.
-  resolveTrack(key: string, trackDesc: TrackDescriptor): TrackCacheEntry {
-    // Search for a cached version of this track,
-    const cached = this.currentTracks.get(key);
-
-    // Ensure the cached track has the same factory type as the resolved track.
-    // If this has changed, the track should be re-created.
-    if (cached && trackDesc.trackFactory === cached.desc.trackFactory) {
-      // Keep our cached track descriptor up to date, if anything's changed.
-      cached.desc = trackDesc;
-
-      // Move this track from the recycle bin to the safe cache, which means
-      // it's safe from disposal for this cycle.
-      this.newTracks.set(key, cached);
-
-      return cached;
-    } else {
-      // Cached track doesn't exist or is out of date, create a new one.
-      const trackContext: TrackContext = {
-        trackKey: key,
-      };
-      const track = trackDesc.trackFactory(trackContext);
-      const entry = new TrackFSM(key, track, trackDesc, trackContext);
-
-      // Push track into the safe cache.
-      this.newTracks.set(key, entry);
-      return entry;
-    }
-  }
-
-  // Destroys all current tracks not present in the new cache.
-  flushOldTracks() {
-    for (const [key, entry] of this.currentTracks.entries()) {
-      if (!this.newTracks.has(key)) {
-        entry[Symbol.dispose]();
-      }
-    }
-
-    this.currentTracks = this.newTracks;
-    this.newTracks = new Map<string, TrackCacheEntry>();
-  }
-
-  private updateTrackKeyByTrackIdMap() {
-    if (this.trackState === this.store.state.tracks) {
-      return;
-    }
-
-    const trackKeyByTrackId = new Map<number, string>();
-
-    const trackList = Object.entries(this.store.state.tracks);
-    trackList.forEach(([key, {uri}]) => {
-      const desc = this.trackRegistry.get(uri);
-      for (const trackId of desc?.tags?.trackIds ?? []) {
-        const existingKey = trackKeyByTrackId.get(trackId);
-        if (exists(existingKey)) {
-          throw new Error(
-            `Trying to map track id ${trackId} to UI track ${key}, already mapped to ${existingKey}`,
-          );
-        }
-        trackKeyByTrackId.set(trackId, key);
-      }
-    });
-
-    this._trackKeyByTrackId = trackKeyByTrackId;
-    this.trackState = this.store.state.tracks;
-  }
-}
-
-/**
- * This function describes the asynchronous lifecycle of a track using an async
- * generator. This saves us having to build out the state machine explicitly,
- * using conventional serial programming techniques to describe the lifecycle
- * instead, which is more natural and easier to understand.
- *
- * We expect the params to onUpdate to be passed into the generator via the
- * yield function.
- *
- * @param track The track to run the lifecycle for.
- * @param ctx The trace context, passed to various lifecycle methods.
- */
-async function* trackLifecycle(
-  track: Track,
-  ctx: TrackContext,
-): AsyncGenerator<void, void, TrackRenderContext> {
-  try {
-    // Wait for parameters to be passed in before initializing the track
-    const trackRenderCtx = yield;
-    await Promise.resolve(track.onCreate?.(ctx));
-    await Promise.resolve(track.onUpdate?.(trackRenderCtx));
-
-    // Wait for parameters to be passed in before subsequent calls to onUpdate()
-    while (true) {
-      await Promise.resolve(track.onUpdate?.(yield));
-    }
-  } finally {
-    // Ensure we always clean up, even on throw or early return
-    await Promise.resolve(track.onDestroy?.());
-  }
-}
-
-/**
- * Wrapper that manages lifecycle hooks on behalf of a track, ensuring lifecycle
- * hooks are called synchronously and in the correct order.
- */
-class TrackFSM implements TrackCacheEntry {
-  public readonly trackKey: string;
-  public readonly track: Track;
-  public readonly desc: TrackDescriptor;
-
-  private readonly limiter = new AsyncLimiter();
-  private readonly ctx: TrackContext;
-  private readonly generator: ReturnType<typeof trackLifecycle>;
-
-  private error?: Error;
-  private isDisposed = false;
-
-  constructor(
-    trackKey: string,
-    track: Track,
-    desc: TrackDescriptor,
-    ctx: TrackContext,
-  ) {
-    this.trackKey = trackKey;
-    this.track = track;
-    this.desc = desc;
-    this.ctx = ctx;
-
-    this.generator = trackLifecycle(this.track, this.ctx);
-
-    // This just starts the generator, which will pause at the first yield
-    // without doing anything - note that the parameter to the first next() call
-    // is ignored in generators
-    this.generator.next();
-  }
-
-  render(ctx: TrackRenderContext): void {
-    assertFalse(this.isDisposed);
-
-    // The generator will ensure that track lifecycle calls don't overlap, but
-    // it'll also enqueue every single call to next() which can create a large
-    // backlog of updates assuming render is called faster than updates can
-    // complete (this is usually the case), so we use an AsyncLimiter here to
-    // avoid enqueueing more than one next().
-    this.limiter
-      .schedule(async () => {
-        // Pass in the parameters to onUpdate() here (i.e. the track size)
-        await this.generator.next(ctx);
-      })
-      .catch((e) => {
-        // Errors thrown inside lifecycle hooks will bubble up through the
-        // generator and AsyncLimiter to here, where we can swallow and capture
-        // the error
-        this.error = e;
-      });
-
-    // Always call render synchronously
-    this.track.render(ctx);
-  }
-
-  [Symbol.dispose](): void {
-    assertFalse(this.isDisposed);
-    this.isDisposed = true;
-
-    // Ask the generator to stop, it'll handle any cleanup and return at the
-    // next yield
-    this.generator.return();
-  }
-
-  getError(): Optional<Error> {
-    return this.error;
-  }
-}
diff --git a/ui/src/common/track_manager.ts b/ui/src/common/track_manager.ts
new file mode 100644
index 0000000..625c732
--- /dev/null
+++ b/ui/src/common/track_manager.ts
@@ -0,0 +1,191 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Optional} from '../base/utils';
+import {Registry} from '../base/registry';
+import {Track, TrackDescriptor} from '../public';
+
+import {AsyncLimiter} from '../base/async_limiter';
+import {TrackRenderContext} from '../public/tracks';
+
+export interface TrackRenderer {
+  readonly track: Track;
+  desc: TrackDescriptor;
+  render(ctx: TrackRenderContext): void;
+  getError(): Optional<Error>;
+}
+
+/**
+ * TrackManager is responsible for managing the registry of tracks and their
+ * lifecycle of tracks over render cycles.
+ *
+ * Example usage:
+ * function render() {
+ *   const trackCache = new TrackCache();
+ *   const foo = trackCache.getTrackRenderer('foo', 'exampleURI', {});
+ *   const bar = trackCache.getTrackRenderer('bar', 'exampleURI', {});
+ *   trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
+ * }
+ *
+ * Example of how flushing works:
+ * First cycle
+ *   getTrackRenderer('foo', ...) <-- new track 'foo' created
+ *   getTrackRenderer('bar', ...) <-- new track 'bar' created
+ *   flushTracks()
+ * Second cycle
+ *   getTrackRenderer('foo', ...) <-- returns cached 'foo' track
+ *   flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
+ * Third cycle
+ *   flushTracks() <-- 'foo' is destroyed.
+ */
+export class TrackManager {
+  private tracks = new Registry<TrackFSM>((x) => x.desc.uri);
+
+  // This contains the tracks refs that plugins want to get auto-added on trace
+  // load, rather than bothering manually adding them to the workspace. They
+  // come from plugins calling registerTrackAndShowOnTraceLoad().
+  // TODO(primiano): this is going away soon.
+  private autoShowTracks = new Set<TrackDescriptor>();
+
+  registerTrack(trackDesc: TrackDescriptor): Disposable {
+    return this.tracks.register(new TrackFSM(trackDesc));
+  }
+
+  // TODO(primiano): this is going away soon.
+  autoShowOnTraceLoad(track: TrackDescriptor): Disposable {
+    this.autoShowTracks.add(track);
+    return {
+      [Symbol.dispose]: () => this.autoShowTracks.delete(track),
+    };
+  }
+
+  getAutoShowTracks(): TrackDescriptor[] {
+    return Array.from(this.autoShowTracks);
+  }
+
+  findTrack(
+    predicate: (desc: TrackDescriptor) => boolean | undefined,
+  ): TrackDescriptor | undefined {
+    for (const t of this.tracks.values()) {
+      if (predicate(t.desc)) return t.desc;
+    }
+    return undefined;
+  }
+
+  getAllTracks(): TrackDescriptor[] {
+    return Array.from(this.tracks.valuesAsArray().map((t) => t.desc));
+  }
+
+  // Look up track into for a given track's URI.
+  // Returns |undefined| if no track can be found.
+  getTrack(uri: string): TrackDescriptor | undefined {
+    return this.tracks.tryGet(uri)?.desc;
+  }
+
+  // This is only called by the viewer_page.ts.
+  getTrackRenderer(uri: string): TrackRenderer | undefined {
+    // Search for a cached version of this track,
+    const trackFsm = this.tracks.tryGet(uri);
+    trackFsm?.markUsed();
+    return trackFsm;
+  }
+
+  // Destroys all tracks that didn't recently get a getTrackRenderer() call.
+  flushOldTracks() {
+    for (const trackFsm of this.tracks.values()) {
+      trackFsm.tick();
+    }
+  }
+}
+
+const DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT = 1;
+
+/**
+ * Owns all runtime information about a track and manages its lifecycle,
+ * ensuring lifecycle hooks are called synchronously and in the correct order.
+ *
+ * There are quite some subtle properties that this class guarantees:
+ * - It make sure that lifecycle methods don't overlap with each other.
+ * - It prevents a chain of onCreate > onDestroy > onCreate if the first
+ *   onCreate() is still oustanding. This is by virtue of using AsyncLimiter
+ *   which under the hoods holds only the most recent task and skips the
+ *   intermediate ones.
+ * - Ensures that a track never sees two consecutive onCreate, or onDestroy or
+ *   an onDestroy without an onCreate.
+ * - Ensures that onUpdate never overlaps or follows with onDestroy. This is
+ *   particularly important because tracks often drop tables/views onDestroy
+ *   and they shouldn't try to fetch more data onUpdate past that point.
+ */
+class TrackFSM implements TrackRenderer {
+  public readonly desc: TrackDescriptor;
+
+  private readonly limiter = new AsyncLimiter();
+  private error?: Error;
+  private tickSinceLastUsed = 0;
+  private created = false;
+
+  constructor(desc: TrackDescriptor) {
+    this.desc = desc;
+  }
+
+  markUsed(): void {
+    this.tickSinceLastUsed = 0;
+  }
+
+  // Increment the lastUsed counter, and maybe call onDestroy().
+  tick(): void {
+    if (this.tickSinceLastUsed++ === DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT) {
+      // Schedule an onDestroy
+      this.limiter
+        .schedule(async () => {
+          if (this.created) {
+            await Promise.resolve(this.track.onDestroy?.());
+            this.created = false;
+          }
+        })
+        .catch((e) => {
+          // Errors thrown inside lifecycle hooks will bubble up through the
+          // AsyncLimiter to here, where we can swallow and capture the error.
+          this.error = e;
+        });
+    }
+  }
+
+  render(ctx: TrackRenderContext): void {
+    this.limiter
+      .schedule(async () => {
+        // Call onCreate() if we have been destroyed or were never created in
+        // the first place.
+        if (!this.created) {
+          await Promise.resolve(this.track.onCreate?.(ctx));
+          this.created = true;
+        }
+        await Promise.resolve(this.track.onUpdate?.(ctx));
+      })
+      .catch((e) => {
+        // Errors thrown inside lifecycle hooks will bubble up through the
+        // AsyncLimiter to here, where we can swallow and capture the error.
+        this.error = e;
+      });
+    this.track.render(ctx);
+  }
+
+  getError(): Optional<Error> {
+    return this.error;
+  }
+
+  get track(): Track {
+    return this.desc.track;
+  }
+}
diff --git a/ui/src/common/track_cache_unittest.ts b/ui/src/common/track_manager_unittest.ts
similarity index 78%
rename from ui/src/common/track_cache_unittest.ts
rename to ui/src/common/track_manager_unittest.ts
index c65f615..d8099bf 100644
--- a/ui/src/common/track_cache_unittest.ts
+++ b/ui/src/common/track_manager_unittest.ts
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {assertExists} from '../base/logging';
 import {Duration} from '../base/time';
 import {PxSpan, TimeScale} from '../frontend/time_scale';
-import {createStore, TrackDescriptor} from '../public';
+import {TrackDescriptor} from '../public';
 import {TrackRenderContext} from '../public/tracks';
 
-import {createEmptyState} from './empty_state';
 import {HighPrecisionTime} from './high_precision_time';
 import {HighPrecisionTimeSpan} from './high_precision_time_span';
-import {TrackManager} from './track_cache';
+import {TrackManager} from './track_manager';
 
 function makeMockTrack() {
   return {
@@ -48,7 +48,7 @@
 let trackManager: TrackManager;
 const visibleWindow = new HighPrecisionTimeSpan(HighPrecisionTime.ZERO, 0);
 const dummyCtx: TrackRenderContext = {
-  trackKey: 'foo',
+  trackUri: 'foo',
   ctx: new CanvasRenderingContext2D(),
   size: {width: 123, height: 123},
   visibleWindow,
@@ -61,29 +61,31 @@
   td = {
     uri: 'test',
     title: 'foo',
-    trackFactory: () => mockTrack,
+    track: mockTrack,
   };
-  const store = createStore(createEmptyState());
-  trackManager = new TrackManager(store);
+  trackManager = new TrackManager();
+  trackManager.registerTrack(td);
 });
 
 describe('TrackManager', () => {
   it('calls track lifecycle hooks', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
 
     entry.render(dummyCtx);
     await settle();
     expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
     expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1);
 
-    entry[Symbol.dispose]();
+    // Double flush should destroy all tracks
+    trackManager.flushOldTracks();
+    trackManager.flushOldTracks();
     await settle();
     expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
   });
 
   it('calls onCrate lazily', async () => {
     // Check we wait until the first call to render before calling onCreate
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
     await settle();
     expect(mockTrack.onCreate).not.toHaveBeenCalled();
 
@@ -93,12 +95,12 @@
   });
 
   it('reuses tracks', async () => {
-    const first = trackManager.resolveTrack('foo', td);
+    const first = assertExists(trackManager.getTrackRenderer(td.uri));
     trackManager.flushOldTracks();
     first.render(dummyCtx);
     await settle();
 
-    const second = trackManager.resolveTrack('foo', td);
+    const second = assertExists(trackManager.getTrackRenderer(td.uri));
     trackManager.flushOldTracks();
     second.render(dummyCtx);
     await settle();
@@ -109,7 +111,7 @@
   });
 
   it('destroys tracks when they are not resolved for one cycle', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
     entry.render(dummyCtx);
 
     // Double flush should destroy all tracks
@@ -121,20 +123,8 @@
     expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
   });
 
-  it('throws on render after destroy', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
-
-    // Double flush should destroy all tracks
-    trackManager.flushOldTracks();
-    trackManager.flushOldTracks();
-
-    await settle();
-
-    expect(() => entry.render(dummyCtx)).toThrow();
-  });
-
   it('contains crash inside onCreate()', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
     const e = new Error();
 
     // Mock crash inside onCreate
@@ -147,12 +137,11 @@
 
     expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
     expect(mockTrack.onUpdate).not.toHaveBeenCalled();
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
     expect(entry.getError()).toBe(e);
   });
 
   it('contains crash inside onUpdate()', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
     const e = new Error();
 
     // Mock crash inside onUpdate
@@ -164,12 +153,12 @@
     await settle();
 
     expect(mockTrack.onCreate).toHaveBeenCalledTimes(1);
-    expect(mockTrack.onDestroy).toHaveBeenCalledTimes(1);
+    expect(mockTrack.onUpdate).toHaveBeenCalledTimes(1);
     expect(entry.getError()).toBe(e);
   });
 
   it('handles dispose after crash', async () => {
-    const entry = trackManager.resolveTrack('foo', td);
+    const entry = assertExists(trackManager.getTrackRenderer(td.uri));
     const e = new Error();
 
     // Mock crash inside onUpdate
@@ -180,8 +169,8 @@
     entry.render(dummyCtx);
     await settle();
 
-    // Ensure we don't crash while disposing
-    entry[Symbol.dispose]();
+    // Ensure we don't crash during the next render cycle
+    entry.render(dummyCtx);
     await settle();
   });
 });
diff --git a/ui/src/controller/aggregation/counter_aggregation_controller.ts b/ui/src/controller/aggregation/counter_aggregation_controller.ts
index f8f25c4..b4d02cc 100644
--- a/ui/src/controller/aggregation/counter_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/counter_aggregation_controller.ts
@@ -24,13 +24,10 @@
 export class CounterAggregationController extends AggregationController {
   async createAggregateView(engine: Engine, area: Area) {
     const trackIds: (string | number)[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === COUNTER_TRACK_KIND) {
-          trackInfo.tags?.trackIds && trackIds.push(...trackInfo.tags.trackIds);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === COUNTER_TRACK_KIND) {
+        trackInfo.tags?.trackIds && trackIds.push(...trackInfo.tags.trackIds);
       }
     }
     if (trackIds.length === 0) return false;
diff --git a/ui/src/controller/aggregation/cpu_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
index 6ea3f67..085ffe2 100644
--- a/ui/src/controller/aggregation/cpu_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_aggregation_controller.ts
@@ -24,13 +24,10 @@
 export class CpuAggregationController extends AggregationController {
   async createAggregateView(engine: Engine, area: Area) {
     const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
       }
     }
     if (selectedCpus.length === 0) return false;
diff --git a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
index eeb0942..2d8baa3 100644
--- a/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/cpu_by_process_aggregation_controller.ts
@@ -24,13 +24,10 @@
 export class CpuByProcessAggregationController extends AggregationController {
   async createAggregateView(engine: Engine, area: Area) {
     const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
       }
     }
     if (selectedCpus.length === 0) return false;
diff --git a/ui/src/controller/aggregation/frame_aggregation_controller.ts b/ui/src/controller/aggregation/frame_aggregation_controller.ts
index 7c91b4b..3d25557 100644
--- a/ui/src/controller/aggregation/frame_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/frame_aggregation_controller.ts
@@ -23,15 +23,11 @@
 export class FrameAggregationController extends AggregationController {
   async createAggregateView(engine: Engine, area: Area) {
     const selectedSqlTrackIds: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      // Track will be undefined for track groups.
-      if (track?.uri !== undefined) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
-          trackInfo.tags.trackIds &&
-            selectedSqlTrackIds.push(...trackInfo.tags.trackIds);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
+        trackInfo.tags.trackIds &&
+          selectedSqlTrackIds.push(...trackInfo.tags.trackIds);
       }
     }
     if (selectedSqlTrackIds.length === 0) return false;
diff --git a/ui/src/controller/aggregation/slice_aggregation_controller.ts b/ui/src/controller/aggregation/slice_aggregation_controller.ts
index 96cb0ea..f1fdd5f 100644
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/slice_aggregation_controller.ts
@@ -25,19 +25,15 @@
 
 export function getSelectedTrackKeys(area: Area): number[] {
   const selectedTrackKeys: number[] = [];
-  for (const trackKey of area.tracks) {
-    const track = globals.state.tracks[trackKey];
-    // Track will be undefined for track groups.
-    if (track?.uri !== undefined) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === THREAD_SLICE_TRACK_KIND) {
-        trackInfo.tags.trackIds &&
-          selectedTrackKeys.push(...trackInfo.tags.trackIds);
-      }
-      if (trackInfo?.tags?.kind === ASYNC_SLICE_TRACK_KIND) {
-        trackInfo.tags.trackIds &&
-          selectedTrackKeys.push(...trackInfo.tags.trackIds);
-      }
+  for (const trackUri of area.trackUris) {
+    const trackInfo = globals.trackManager.getTrack(trackUri);
+    if (trackInfo?.tags?.kind === THREAD_SLICE_TRACK_KIND) {
+      trackInfo.tags.trackIds &&
+        selectedTrackKeys.push(...trackInfo.tags.trackIds);
+    }
+    if (trackInfo?.tags?.kind === ASYNC_SLICE_TRACK_KIND) {
+      trackInfo.tags.trackIds &&
+        selectedTrackKeys.push(...trackInfo.tags.trackIds);
     }
   }
   return selectedTrackKeys;
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
index 4f502e2..db593ad 100644
--- a/ui/src/controller/aggregation/thread_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -28,20 +28,16 @@
 
   setThreadStateUtids(tracks: string[]) {
     this.utids = [];
-    for (const trackId of tracks) {
-      const track = globals.state.tracks[trackId];
-      // Track will be undefined for track groups.
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === THREAD_STATE_TRACK_KIND) {
-          exists(trackInfo.tags.utid) && this.utids.push(trackInfo.tags.utid);
-        }
+    for (const trackUri of tracks) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === THREAD_STATE_TRACK_KIND) {
+        exists(trackInfo.tags.utid) && this.utids.push(trackInfo.tags.utid);
       }
     }
   }
 
   async createAggregateView(engine: Engine, area: Area) {
-    this.setThreadStateUtids(area.tracks);
+    this.setThreadStateUtids(area.trackUris);
     if (this.utids === undefined || this.utids.length === 0) return false;
 
     await engine.query(`
@@ -68,7 +64,7 @@
   }
 
   async getExtra(engine: Engine, area: Area): Promise<ThreadStateExtra | void> {
-    this.setThreadStateUtids(area.tracks);
+    this.setThreadStateUtids(area.trackUris);
     if (this.utids === undefined || this.utids.length === 0) return;
 
     const query = `
diff --git a/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts
index 896b136..4a17bab 100644
--- a/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/wattson/estimate_aggregation_controller.ts
@@ -29,16 +29,13 @@
     if (!(await hasWattsonSupport(engine))) return false;
 
     const estimateTracks: string[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (
-          trackInfo?.tags?.kind === CPUSS_ESTIMATE_TRACK_KIND &&
-          exists(trackInfo.tags?.wattson)
-        ) {
-          estimateTracks.push(`${trackInfo.tags.wattson}`);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (
+        trackInfo?.tags?.kind === CPUSS_ESTIMATE_TRACK_KIND &&
+        exists(trackInfo.tags?.wattson)
+      ) {
+        estimateTracks.push(`${trackInfo.tags.wattson}`);
       }
     }
     if (estimateTracks.length === 0) return false;
@@ -98,14 +95,14 @@
         columnId: 'name',
       },
       {
-        title: 'Average estimated power (mW)',
+        title: 'Average power (estimated mW)',
         kind: 'NUMBER',
         columnConstructor: Float64Array,
         columnId: 'power',
         sum: true,
       },
       {
-        title: 'Total estimated energy (mWs)',
+        title: 'Total energy (estimated mWs)',
         kind: 'NUMBER',
         columnConstructor: Float64Array,
         columnId: 'energy',
diff --git a/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts
index d43efdd..998b325 100644
--- a/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/wattson/package_aggregation_controller.ts
@@ -36,13 +36,10 @@
     if (packageInfo.firstRow({isValid: NUM}).isValid === 0) return false;
 
     const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
       }
     }
     if (selectedCpus.length === 0) return false;
@@ -88,14 +85,14 @@
         columnId: 'dur_ms',
       },
       {
-        title: 'Average estimated power (mW)',
+        title: 'Average power (estimated mW)',
         kind: 'NUMBER',
         columnConstructor: Float64Array,
         columnId: 'avg_mw',
         sum: true,
       },
       {
-        title: 'Total estimated energy (mWs)',
+        title: 'Total energy (estimated mWs)',
         kind: 'NUMBER',
         columnConstructor: Float64Array,
         columnId: 'total_mws',
diff --git a/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts
index d7675f8..e0c8631 100644
--- a/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/wattson/process_aggregation_controller.ts
@@ -29,14 +29,11 @@
     if (!(await hasWattsonSupport(engine))) return false;
 
     const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
-      }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND &&
+        exists(trackInfo.tags.cpu) &&
+        selectedCpus.push(trackInfo.tags.cpu);
     }
     if (selectedCpus.length === 0) return false;
 
diff --git a/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts b/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts
index 125731b..69c4ed5 100644
--- a/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/wattson/thread_aggregation_controller.ts
@@ -29,13 +29,10 @@
     if (!(await hasWattsonSupport(engine))) return false;
 
     const selectedCpus: number[] = [];
-    for (const trackKey of area.tracks) {
-      const track = globals.state.tracks[trackKey];
-      if (track?.uri) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-          exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
-        }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
+        exists(trackInfo.tags.cpu) && selectedCpus.push(trackInfo.tags.cpu);
       }
     }
     if (selectedCpus.length === 0) return false;
diff --git a/ui/src/controller/cpu_profile_controller.ts b/ui/src/controller/cpu_profile_controller.ts
deleted file mode 100644
index d658170..0000000
--- a/ui/src/controller/cpu_profile_controller.ts
+++ /dev/null
@@ -1,182 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {CpuProfileSampleSelection, getLegacySelection} from '../common/state';
-import {CpuProfileDetails, globals} from '../frontend/globals';
-import {publishCpuProfileDetails} from '../frontend/publish';
-import {Engine} from '../trace_processor/engine';
-import {NUM, STR} from '../trace_processor/query_result';
-
-import {Controller} from './controller';
-
-export interface CpuProfileControllerArgs {
-  engine: Engine;
-}
-
-export class CpuProfileController extends Controller<'main'> {
-  private lastSelectedSample?: CpuProfileSampleSelection;
-  private requestingData = false;
-  private queuedRunRequest = false;
-
-  constructor(private args: CpuProfileControllerArgs) {
-    super('main');
-  }
-
-  run() {
-    const selection = getLegacySelection(globals.state);
-    if (!selection || selection.kind !== 'CPU_PROFILE_SAMPLE') {
-      return;
-    }
-
-    const selectedSample = selection as CpuProfileSampleSelection;
-    if (!this.shouldRequestData(selectedSample)) {
-      return;
-    }
-
-    if (this.requestingData) {
-      this.queuedRunRequest = true;
-      return;
-    }
-
-    this.requestingData = true;
-    publishCpuProfileDetails({});
-    this.lastSelectedSample = this.copyCpuProfileSample(selection);
-
-    this.getSampleData(selectedSample.id)
-      .then((sampleData) => {
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        if (
-          sampleData !== undefined &&
-          selectedSample &&
-          /* eslint-enable */
-          this.lastSelectedSample &&
-          this.lastSelectedSample.id === selectedSample.id
-        ) {
-          const cpuProfileDetails: CpuProfileDetails = {
-            id: selectedSample.id,
-            ts: selectedSample.ts,
-            utid: selectedSample.utid,
-            stack: sampleData,
-          };
-
-          publishCpuProfileDetails(cpuProfileDetails);
-        }
-      })
-      .finally(() => {
-        this.requestingData = false;
-        if (this.queuedRunRequest) {
-          this.queuedRunRequest = false;
-          this.run();
-        }
-      });
-  }
-
-  private copyCpuProfileSample(
-    cpuProfileSample: CpuProfileSampleSelection,
-  ): CpuProfileSampleSelection {
-    return {
-      kind: cpuProfileSample.kind,
-      id: cpuProfileSample.id,
-      utid: cpuProfileSample.utid,
-      ts: cpuProfileSample.ts,
-    };
-  }
-
-  private shouldRequestData(selection: CpuProfileSampleSelection) {
-    return (
-      this.lastSelectedSample === undefined ||
-      (this.lastSelectedSample !== undefined &&
-        this.lastSelectedSample.id !== selection.id)
-    );
-  }
-
-  async getSampleData(id: number) {
-    // The goal of the query is to get all the frames of
-    // the callstack at the callsite given by |id|. To do this, it does
-    // the following:
-    // 1. Gets the leaf callsite id for the sample given by |id|.
-    // 2. For this callsite, get all the frame ids and depths
-    //    for the frame and all ancestors in the callstack.
-    // 3. For each frame, get the mapping name (i.e. library which
-    //    contains the frame).
-    // 4. Symbolize each frame using the symbol table if possible.
-    // 5. Sort the query by the depth of the callstack frames.
-    const sampleQuery = `
-      SELECT
-        samples.id as id,
-        IFNULL(
-          (
-            SELECT name
-            FROM stack_profile_symbol symbol
-            WHERE symbol.symbol_set_id = spf.symbol_set_id
-            LIMIT 1
-          ),
-          COALESCE(spf.deobfuscated_name, spf.name, "")
-        ) AS name,
-        spm.name AS mapping
-      FROM cpu_profile_stack_sample AS samples
-      LEFT JOIN (
-        SELECT
-          id,
-          frame_id,
-          depth
-        FROM stack_profile_callsite
-        UNION ALL
-        SELECT
-          leaf.id AS id,
-          callsite.frame_id AS frame_id,
-          callsite.depth AS depth
-        FROM stack_profile_callsite leaf
-        JOIN experimental_ancestor_stack_profile_callsite(leaf.id) AS callsite
-      ) AS callsites
-        ON samples.callsite_id = callsites.id
-      LEFT JOIN stack_profile_frame AS spf
-        ON callsites.frame_id = spf.id
-      LEFT JOIN stack_profile_mapping AS spm
-        ON spf.mapping = spm.id
-      WHERE samples.id = ${id}
-      ORDER BY callsites.depth;
-    `;
-
-    const callsites = await this.args.engine.query(sampleQuery);
-
-    if (callsites.numRows() === 0) {
-      return undefined;
-    }
-
-    const it = callsites.iter({
-      id: NUM,
-      name: STR,
-      mapping: STR,
-    });
-
-    const sampleData: CallsiteInfo[] = [];
-    for (; it.valid(); it.next()) {
-      sampleData.push({
-        id: it.id,
-        totalSize: 0,
-        depth: 0,
-        parentId: 0,
-        name: it.name,
-        selfSize: 0,
-        mapping: it.mapping,
-        merged: false,
-        highlighted: false,
-      });
-    }
-
-    return sampleData;
-  }
-}
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index d6e1cd5..e570b38 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -27,6 +27,7 @@
   ACTUAL_FRAMES_SLICE_TRACK_KIND,
   THREAD_SLICE_TRACK_KIND,
 } from '../core/track_kinds';
+import {TrackDescriptor} from '../public';
 
 export interface FlowEventsControllerArgs {
   engine: Engine;
@@ -190,7 +191,6 @@
     // We end up with one Info POJOs for each UI async slice track
     // which has at least  one flow {begin,end}ing in one of its slices.
     interface Info {
-      uiTrackId: string;
       siblingTrackIds: number[];
       sliceIds: number[];
       nodes: Array<{
@@ -199,11 +199,17 @@
       }>;
     }
 
-    const uiTrackIdToInfo = new Map<string, null | Info>();
+    const trackUriToInfo = new Map<string, null | Info>();
     const trackIdToInfo = new Map<number, null | Info>();
 
-    const trackIdToUiTrackId = globals.trackManager.trackKeyByTrackId;
-    const tracks = globals.state.tracks;
+    const trackIdToTrack = new Map<number, TrackDescriptor>();
+    globals.trackManager
+      .getAllTracks()
+      .forEach((trackDescriptor) =>
+        trackDescriptor.tags?.trackIds?.forEach((trackId) =>
+          trackIdToTrack.set(trackId, trackDescriptor),
+        ),
+      );
 
     const getInfo = (trackId: number): null | Info => {
       let info = trackIdToInfo.get(trackId);
@@ -211,19 +217,13 @@
         return info;
       }
 
-      const uiTrackId = trackIdToUiTrackId.get(trackId);
-      if (uiTrackId === undefined) {
+      const trackDescriptor = trackIdToTrack.get(trackId);
+      if (trackDescriptor === undefined) {
         trackIdToInfo.set(trackId, null);
         return null;
       }
 
-      const track = tracks[uiTrackId];
-      if (track === undefined) {
-        trackIdToInfo.set(trackId, null);
-        return null;
-      }
-
-      info = uiTrackIdToInfo.get(uiTrackId);
+      info = trackUriToInfo.get(trackDescriptor.uri);
       if (info !== undefined) {
         return info;
       }
@@ -233,22 +233,20 @@
       // anything if there is only one TP track in this async track. In
       // that case experimental_slice_layout is just an expensive way
       // to find out depth === layout_depth.
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      const trackIds = trackInfo?.tags?.trackIds;
+      const trackIds = trackDescriptor?.tags?.trackIds;
       if (trackIds === undefined || trackIds.length <= 1) {
-        uiTrackIdToInfo.set(uiTrackId, null);
+        trackUriToInfo.set(trackDescriptor.uri, null);
         trackIdToInfo.set(trackId, null);
         return null;
       }
 
       const newInfo = {
-        uiTrackId,
         siblingTrackIds: [...trackIds],
         sliceIds: [],
         nodes: [],
       };
 
-      uiTrackIdToInfo.set(uiTrackId, newInfo);
+      trackUriToInfo.set(trackDescriptor.uri, newInfo);
       trackIdToInfo.set(trackId, newInfo);
 
       return newInfo;
@@ -268,7 +266,7 @@
     // Second pass, for each async track:
     // - Query to find the layout_depth for each relevant sliceId
     // - Iterate through the nodes updating the depth in place
-    for (const info of uiTrackIdToInfo.values()) {
+    for (const info of trackUriToInfo.values()) {
       if (info === null) {
         continue;
       }
@@ -361,19 +359,16 @@
   private areaSelected(area: AreaSelection) {
     const trackIds: number[] = [];
 
-    for (const uiTrackId of area.tracks) {
-      const track = globals.state.tracks[uiTrackId];
-      if (track?.uri !== undefined) {
-        const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-        const kind = trackInfo?.tags?.kind;
-        if (
-          kind === THREAD_SLICE_TRACK_KIND ||
-          kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
-        ) {
-          if (trackInfo?.tags?.trackIds) {
-            for (const trackId of trackInfo.tags.trackIds) {
-              trackIds.push(trackId);
-            }
+    for (const trackUri of area.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      const kind = trackInfo?.tags?.kind;
+      if (
+        kind === THREAD_SLICE_TRACK_KIND ||
+        kind === ACTUAL_FRAMES_SLICE_TRACK_KIND
+      ) {
+        if (trackInfo?.tags?.trackIds) {
+          for (const trackId of trackInfo.tags.trackIds) {
+            trackIds.push(trackId);
           }
         }
       }
diff --git a/ui/src/controller/pivot_table_controller.ts b/ui/src/controller/pivot_table_controller.ts
index bfa46ac..036138c 100644
--- a/ui/src/controller/pivot_table_controller.ts
+++ b/ui/src/controller/pivot_table_controller.ts
@@ -217,7 +217,7 @@
       return false;
     }
 
-    const newTracks = new Set(selection.tracks);
+    const newTracks = new Set(selection.trackUris);
     if (
       this.lastQueryArea !== state.selectionArea ||
       !this.sameTracks(newTracks)
@@ -308,6 +308,6 @@
 function areasEqual(a: Area, b: Area): boolean {
   if (a.start !== b.start) return false;
   if (a.end !== b.end) return false;
-  if (!arrayEquals(a.tracks, b.tracks)) return false;
+  if (!arrayEquals(a.trackUris, b.trackUris)) return false;
   return true;
 }
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index b7d2c45..ad0031a 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -13,10 +13,13 @@
 // limitations under the License.
 
 import {sqliteString} from '../base/string_utils';
-import {exists} from '../base/utils';
+import {exists, Optional} from '../base/utils';
 import {CurrentSearchResults, SearchSource} from '../common/search_data';
 import {OmniboxState} from '../common/state';
-import {CPU_SLICE_TRACK_KIND} from '../core/track_kinds';
+import {
+  ANDROID_LOGS_TRACK_KIND,
+  CPU_SLICE_TRACK_KIND,
+} from '../core/track_kinds';
 import {globals} from '../frontend/globals';
 import {publishSearchResult} from '../frontend/publish';
 import {Engine} from '../trace_processor/engine';
@@ -65,7 +68,7 @@
         tses: new BigInt64Array(0),
         utids: new Float64Array(0),
         sources: [],
-        trackKeys: [],
+        trackUris: [],
         totalResults: 0,
       });
       return;
@@ -85,16 +88,24 @@
 
   private async specificSearch(search: string) {
     const searchLiteral = escapeSearchQuery(search);
-    // TODO(hjd): we should avoid recomputing this every time. This will be
-    // easier once the track table has entries for all the tracks.
-    const cpuToTrackId = new Map();
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        exists(trackInfo.tags.cpu) &&
-          cpuToTrackId.set(trackInfo.tags.cpu, track.key);
-      }
-    }
+
+    // TODO(stevegolton): Avoid recomputing these indexes each time.
+    const trackUrisByCpu = new Map<number, string>();
+    globals.trackManager.getAllTracks().forEach((td) => {
+      const tags = td?.tags;
+      const cpu = tags?.cpu;
+      const kind = tags?.kind;
+      exists(cpu) &&
+        kind === CPU_SLICE_TRACK_KIND &&
+        trackUrisByCpu.set(cpu, td.uri);
+    });
+
+    const trackUrisByTrackId = new Map<number, string>();
+    globals.trackManager.getAllTracks().forEach((td) => {
+      const trackIds = td?.tags?.trackIds;
+      trackIds &&
+        trackIds.forEach((trackId) => trackUrisByTrackId.set(trackId, td.uri));
+    });
 
     const utidRes = await this.query(`select utid from thread join process
     using(upid) where
@@ -155,18 +166,18 @@
       tses: new BigInt64Array(0),
       utids: new Float64Array(0),
       sources: [],
-      trackKeys: [],
+      trackUris: [],
       totalResults: 0,
     };
 
     const lowerSearch = search.toLowerCase();
-    for (const track of Object.values(globals.state.tracks)) {
-      if (track.name.toLowerCase().indexOf(lowerSearch) === -1) {
+    for (const track of globals.workspace.flatTracks) {
+      if (track.displayName.toLowerCase().indexOf(lowerSearch) === -1) {
         continue;
       }
       searchResults.totalResults++;
       searchResults.sources.push('track');
-      searchResults.trackKeys.push(track.key);
+      searchResults.trackUris.push(track.uri);
     }
 
     const rows = res.numRows();
@@ -189,30 +200,24 @@
       utid: NUM,
     });
     for (; it.valid(); it.next()) {
-      let trackId = undefined;
+      let track: Optional<string> = undefined;
       if (it.source === 'cpu') {
-        trackId = cpuToTrackId.get(it.sourceId);
+        track = trackUrisByCpu.get(it.sourceId);
       } else if (it.source === 'slice') {
-        trackId = globals.trackManager.trackKeyByTrackId.get(it.sourceId);
+        track = trackUrisByTrackId.get(it.sourceId);
       } else if (it.source === 'log') {
-        const logTracks = Object.values(globals.state.tracks).filter(
-          (track) => {
-            const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-            return trackDesc && trackDesc.tags?.kind === 'AndroidLogTrack';
-          },
-        );
-        if (logTracks.length > 0) {
-          trackId = logTracks[0].key;
-        }
+        track = globals.trackManager
+          .getAllTracks()
+          .find((td) => td.tags?.kind === ANDROID_LOGS_TRACK_KIND)?.uri;
       }
 
       // The .get() calls above could return undefined, this isn't just an else.
-      if (trackId === undefined) {
+      if (track === undefined) {
         continue;
       }
 
       const i = searchResults.totalResults++;
-      searchResults.trackKeys.push(trackId);
+      searchResults.trackUris.push(track);
       searchResults.sources.push(it.source as SearchSource);
       searchResults.eventIds[i] = it.sliceId;
       searchResults.tses[i] = it.ts;
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 4cf83b4..e68b088 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -288,10 +288,10 @@
       const name = it.name;
       const value = it.value ?? 'NULL';
       if (name === 'destination slice id' && !isNaN(Number(value))) {
-        const destTrackId = await this.getDestTrackId(value);
+        const destTrackUri = await this.getDestTrackUri(value);
         args.set('Destination Slice', {
           kind: 'SCHED_SLICE',
-          trackId: destTrackId,
+          trackUri: destTrackUri,
           sliceId: Number(value),
           rawValue: value,
         });
@@ -302,25 +302,23 @@
     return args;
   }
 
-  async getDestTrackId(sliceId: string): Promise<string> {
+  async getDestTrackUri(sliceId: string): Promise<string> {
     const trackIdQuery = `select track_id as trackId from slice
     where slice_id = ${sliceId}`;
     const result = await this.args.engine.query(trackIdQuery);
     const trackId = result.firstRow({trackId: NUM}).trackId;
     // TODO(hjd): If we had a consistent mapping from TP track_id
     // UI track id for slice tracks this would be unnecessary.
-    let trackKey = '';
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
+    for (const track of globals.workspace.flatTracks) {
+      const trackInfo = globals.trackManager.getTrack(track.uri);
       if (trackInfo?.tags?.kind === THREAD_SLICE_TRACK_KIND) {
         const trackIds = trackInfo?.tags?.trackIds;
         if (trackIds && trackIds.length > 0 && trackIds[0] === trackId) {
-          trackKey = track.key;
-          break;
+          return track.uri;
         }
       }
     }
-    return trackKey;
+    return '';
   }
 
   // TODO(altimin): We currently rely on the ThreadStateDetails for supporting
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 95b7bc5..812fd2f 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -69,10 +69,6 @@
 import {ThreadAggregationController} from './aggregation/thread_aggregation_controller';
 import {Child, Children, Controller} from './controller';
 import {
-  CpuProfileController,
-  CpuProfileControllerArgs,
-} from './cpu_profile_controller';
-import {
   FlowEventsController,
   FlowEventsControllerArgs,
 } from './flow_events_controller';
@@ -94,13 +90,12 @@
   TraceStream,
 } from '../core/trace_stream';
 import {decideTracks} from './track_decider';
-import {profileType} from '../frontend/legacy_flamegraph_panel';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
 import {
   deserializeAppStatePhase1,
   deserializeAppStatePhase2,
 } from '../common/state_serialization';
 import {TraceContext} from '../frontend/trace_context';
+import {profileType} from '../core/selection_manager';
 
 type States = 'init' | 'loading_trace' | 'ready';
 
@@ -275,11 +270,6 @@
           Child('flowEvents', FlowEventsController, flowEventsArgs),
         );
 
-        const cpuProfileArgs: CpuProfileControllerArgs = {engine};
-        childControllers.push(
-          Child('cpuProfile', CpuProfileController, cpuProfileArgs),
-        );
-
         childControllers.push(
           Child('cpu_aggregation', CpuAggregationController, {
             engine,
@@ -388,10 +378,6 @@
   onDestroy() {
     pluginManager.onTraceClose();
     globals.engines.delete(this.engineId);
-
-    // Invalidate the flamegraph cache.
-    // TODO(stevegolton): migrate this to the new system when it's ready.
-    globals.areaFlamegraphCache = new LegacyFlamegraphCache('area');
   }
 
   private async loadTrace(): Promise<EngineMode> {
@@ -548,6 +534,10 @@
 
     await defineMaxLayoutDepthSqlFunction(engine);
 
+    // Remove all workspaces, and create an empty default workspace, ready for
+    // tracks to be inserted.
+    globals.resetWorkspaces();
+
     if (globals.restoreAppStateAfterTraceLoad) {
       deserializeAppStatePhase1(globals.restoreAppStateAfterTraceLoad);
     }
@@ -557,9 +547,10 @@
     });
 
     {
-      // When we reload from a permalink don't create extra tracks:
-      const {pinnedTracks, tracks} = globals.state;
-      if (!pinnedTracks.length && !Object.keys(tracks).length) {
+      // When we reload from a permalink don't create extra tracks.
+      // TODO(stevegolton): This is a terrible way of telling whether we have
+      // loaded from a permalink or not.
+      if (globals.workspace.flatTracks.length === 0) {
         await this.listTracks();
       }
     }
@@ -583,9 +574,6 @@
       publishHasFtrace(res.numRows() > 0);
     }
 
-    globals.dispatch(Actions.sortThreadTracks({}));
-    globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
-
     await this.selectFirstHeapProfile();
     await this.selectPerfSample(traceDetails);
 
@@ -610,8 +598,6 @@
       }
     }
 
-    globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
-
     // Trace Processor doesn't support the reliable range feature for JSON
     // traces.
     if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) {
@@ -642,12 +628,14 @@
   }
 
   private async selectPerfSample(traceTime: {start: time; end: time}) {
-    const query = `select upid
-        from perf_sample
-        join thread using (utid)
-        where callsite_id is not null
-        order by ts desc limit 1`;
-    const profile = await assertExists(this.engine).query(query);
+    const profile = await assertExists(this.engine).query(`
+      select upid
+      from perf_sample
+      join thread using (utid)
+      where callsite_id is not null
+      order by ts desc
+      limit 1
+    `);
     if (profile.numRows() !== 1) return;
     const row = profile.firstRow({upid: NUM});
     const upid = row.upid;
@@ -665,28 +653,29 @@
   }
 
   private async selectFirstHeapProfile() {
-    const query = `select * from (
-      select
-        min(ts) AS ts,
-        'heap_profile:' || group_concat(distinct heap_name) AS type,
-        upid
-      from heap_profile_allocation
-      group by upid
-      union
-      select distinct graph_sample_ts as ts, 'graph' as type, upid
-      from heap_graph_object)
-      order by ts limit 1`;
+    const query = `
+      select * from (
+        select
+          min(ts) AS ts,
+          'heap_profile:' || group_concat(distinct heap_name) AS type,
+          upid
+        from heap_profile_allocation
+        group by upid
+        union
+        select distinct graph_sample_ts as ts, 'graph' as type, upid
+        from heap_graph_object
+      )
+      order by ts
+      limit 1
+    `;
     const profile = await assertExists(this.engine).query(query);
     if (profile.numRows() !== 1) return;
     const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
     const ts = Time.fromRaw(row.ts);
-    let profType = row.type;
-    if (profType == 'heap_profile:libc.malloc,com.android.art') {
-      profType = 'heap_profile:com.android.art,libc.malloc';
-    }
-    const type = profileType(profType);
     const upid = row.upid;
-    globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type}));
+    globals.dispatch(
+      Actions.selectHeapProfile({id: 0, upid, ts, type: profileType(row.type)}),
+    );
   }
 
   private async selectPendingDeeplink(link: PendingDeeplinkState) {
@@ -722,15 +711,17 @@
       });
 
       const id = row.traceProcessorTrackId;
-      const trackKey = globals.trackManager.trackKeyByTrackId.get(id);
-      if (trackKey === undefined) {
+      const track = globals.workspace.flatTracks.find((t) =>
+        globals.trackManager.getTrack(t.uri)?.tags?.trackIds?.includes(id),
+      );
+      if (track === undefined) {
         return;
       }
       globals.setLegacySelection(
         {
           kind: 'SLICE',
           id: row.id,
-          trackKey,
+          trackUri: track.uri,
           table: 'slice',
         },
         {
@@ -745,8 +736,7 @@
   private async listTracks() {
     this.updateStatus('Loading tracks');
     const engine = assertExists(this.engine);
-    const actions = await decideTracks(engine);
-    globals.dispatchMultiple(actions);
+    await decideTracks(engine);
   }
 
   // Show the list of default tabs, but don't make them active!
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index 7b6ad92..d7a9e0e 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -12,21 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {assertExists} from '../base/logging';
-import {Actions, AddTrackArgs, DeferredAction} from '../common/actions';
-import {
-  InThreadTrackSortKey,
-  SCROLLING_TRACK_GROUP,
-  TrackSortKey,
-  UtidToTrackSortKey,
-} from '../common/state';
 import {globals} from '../frontend/globals';
-import {PrimaryTrackSortKey, TrackDescriptor} from '../public';
-import {getThreadOrProcUri, getTrackName} from '../public/utils';
+import {TrackDescriptor} from '../public';
 import {Engine, EngineBase} from '../trace_processor/engine';
-import {NUM, NUM_NULL, STR_NULL} from '../trace_processor/query_result';
+import {NUM, STR, STR_NULL} from '../trace_processor/query_result';
 import {
   ACTUAL_FRAMES_SLICE_TRACK_KIND,
   ASYNC_SLICE_TRACK_KIND,
@@ -44,8 +34,8 @@
   THREAD_SLICE_TRACK_KIND,
   THREAD_STATE_TRACK_KIND,
 } from '../core/track_kinds';
-import {exists} from '../base/utils';
-import {sqliteString} from '../base/string_utils';
+import {exists, Optional} from '../base/utils';
+import {GroupNode, ContainerNode, TrackNode} from '../public/workspace';
 
 const MEM_DMA_COUNTER_NAME = 'mem.dma_heap';
 const MEM_DMA = 'mem.dma_buffer';
@@ -78,34 +68,31 @@
 const CHROME_TRACK_REGEX = new RegExp('^Chrome.*|^InputLatency::.*');
 const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
 const MISC_GROUP = 'Misc Global Tracks';
-const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
 
-export async function decideTracks(
-  engine: EngineBase,
-): Promise<DeferredAction[]> {
-  return new TrackDecider(engine).decideTracks();
+export async function decideTracks(engine: EngineBase): Promise<void> {
+  await new TrackDecider(engine).decideTracks();
 }
 
 class TrackDecider {
   private engine: EngineBase;
-  private upidToUuid = new Map<number, string>();
-  private utidToUuid = new Map<number, string>();
-  private tracksToAdd: AddTrackArgs[] = [];
-  private tracksToPin: string[] = [];
-  private addTrackGroupActions: DeferredAction[] = [];
+  private threadGroups = new Map<number, GroupNode>();
+  private processGroups = new Map<number, GroupNode>();
 
   constructor(engine: EngineBase) {
     this.engine = engine;
   }
 
   private groupGlobalIonTracks(): void {
-    const ionTracks: AddTrackArgs[] = [];
+    const ionTracks: TrackNode[] = [];
     let hasSummary = false;
-    for (const track of this.tracksToAdd) {
-      const isIon = track.name.startsWith(MEM_ION);
-      const isIonCounter = track.name === MEM_ION;
-      const isDmaHeapCounter = track.name === MEM_DMA_COUNTER_NAME;
-      const isDmaBuffferSlices = track.name === MEM_DMA;
+
+    for (const track of globals.workspace.children) {
+      if (!(track instanceof TrackNode)) continue;
+
+      const isIon = track.displayName.startsWith(MEM_ION);
+      const isIonCounter = track.displayName === MEM_ION;
+      const isDmaHeapCounter = track.displayName === MEM_DMA_COUNTER_NAME;
+      const isDmaBuffferSlices = track.displayName === MEM_DMA;
       if (isIon || isIonCounter || isDmaHeapCounter || isDmaBuffferSlices) {
         ionTracks.push(track);
       }
@@ -117,136 +104,85 @@
       return;
     }
 
-    const groupUuid = uuidv4();
-    const summaryTrackKey = uuidv4();
-    let foundSummary = false;
-
+    let group: Optional<GroupNode>;
     for (const track of ionTracks) {
-      if (
-        !foundSummary &&
-        [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.name)
-      ) {
-        foundSummary = true;
-        track.key = summaryTrackKey;
-        track.trackGroup = undefined;
+      if (!group && [MEM_DMA_COUNTER_NAME, MEM_ION].includes(track.uri)) {
+        globals.workspace.removeChild(track);
+        group = new GroupNode(track.displayName);
+        group.headerTrackUri = track.uri;
+        globals.workspace.addChild(group);
       } else {
-        track.trackGroup = groupUuid;
+        group?.addChild(track);
       }
     }
-
-    const addGroup = Actions.addTrackGroup({
-      summaryTrackKey,
-      name: MEM_DMA_COUNTER_NAME,
-      key: groupUuid,
-      collapsed: true,
-    });
-    this.addTrackGroupActions.push(addGroup);
   }
 
-  private groupGlobalIostatTracks(tag: string, group: string): void {
-    const iostatTracks: AddTrackArgs[] = [];
-    const devMap = new Map<string, string>();
+  private groupGlobalIostatTracks(tag: string, groupName: string): void {
+    const devMap = new Map<string, GroupNode>();
 
-    for (const track of this.tracksToAdd) {
-      if (track.name.startsWith(tag)) {
-        iostatTracks.push(track);
+    for (const track of globals.workspace.children) {
+      if (track instanceof TrackNode && track.displayName.startsWith(tag)) {
+        const name = track.displayName.split('.', 3);
+        const key = name[1];
+
+        let parentGroup = devMap.get(key);
+        if (!parentGroup) {
+          const group = new GroupNode(groupName);
+          globals.workspace.addChild(group);
+          devMap.set(key, group);
+          parentGroup = group;
+        }
+
+        track.displayName = name[2];
+        parentGroup.addChild(track);
       }
     }
-
-    if (iostatTracks.length === 0) {
-      return;
-    }
-
-    for (const track of iostatTracks) {
-      const name = track.name.split('.', 3);
-
-      if (!devMap.has(name[1])) {
-        devMap.set(name[1], uuidv4());
-      }
-      track.name = name[2];
-      track.trackGroup = devMap.get(name[1]);
-    }
-
-    for (const [key, value] of devMap) {
-      const groupName = group + key;
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: value,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
   }
 
   private groupGlobalBuddyInfoTracks(): void {
-    const buddyInfoTracks: AddTrackArgs[] = [];
-    const devMap = new Map<string, string>();
+    const devMap = new Map<string, GroupNode>();
 
-    for (const track of this.tracksToAdd) {
-      if (track.name.startsWith(BUDDY_INFO_TAG)) {
-        buddyInfoTracks.push(track);
+    for (const track of globals.workspace.children) {
+      if (
+        track instanceof TrackNode &&
+        track.displayName.startsWith(BUDDY_INFO_TAG)
+      ) {
+        const tokens = track.uri.split('[');
+        const node = tokens[1].slice(0, -1);
+        const zone = tokens[2].slice(0, -1);
+        const size = tokens[3].slice(0, -1);
+
+        const groupName = 'Buddyinfo:  Node: ' + node + ' Zone: ' + zone;
+        if (!devMap.has(groupName)) {
+          const group = new GroupNode(groupName);
+          devMap.set(groupName, group);
+          globals.workspace.addChild(group);
+        }
+        track.displayName = 'Chunk size: ' + size;
+        const group = devMap.get(groupName)!;
+        group.addChild(track);
       }
     }
-
-    if (buddyInfoTracks.length === 0) {
-      return;
-    }
-
-    for (const track of buddyInfoTracks) {
-      const tokens = track.name.split('[');
-      const node = tokens[1].slice(0, -1);
-      const zone = tokens[2].slice(0, -1);
-      const size = tokens[3].slice(0, -1);
-
-      const groupName = 'Buddyinfo:  Node: ' + node + ' Zone: ' + zone;
-      if (!devMap.has(groupName)) {
-        devMap.set(groupName, uuidv4());
-      }
-      track.name = 'Chunk size: ' + size;
-      track.trackGroup = devMap.get(groupName);
-    }
-
-    for (const [key, value] of devMap) {
-      const groupName = key;
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: value,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
   }
 
   private groupFrequencyTracks(groupName: string): void {
-    let groupUuid = undefined;
-    for (const track of this.tracksToAdd) {
+    const group = new GroupNode(groupName);
+
+    for (const track of globals.workspace.children) {
+      if (!(track instanceof TrackNode)) continue;
       // Group all the frequency tracks together (except the CPU and GPU
       // frequency ones).
       if (
-        track.name.endsWith('Frequency') &&
-        !track.name.startsWith('Cpu') &&
-        !track.name.startsWith('Gpu')
+        track.displayName.endsWith('Frequency') &&
+        !track.displayName.startsWith('Cpu') &&
+        !track.displayName.startsWith('Gpu')
       ) {
-        if (
-          track.trackGroup !== undefined &&
-          track.trackGroup !== SCROLLING_TRACK_GROUP
-        ) {
-          continue;
-        }
-        if (groupUuid === undefined) {
-          groupUuid = uuidv4();
-        }
-        track.trackGroup = groupUuid;
+        group.addChild(track);
       }
     }
 
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
+    if (group.children.length > 0) {
+      globals.workspace.addChild(group);
     }
   }
 
@@ -261,62 +197,35 @@
       new RegExp('^Android logs$'),
     ];
 
-    let groupUuid = undefined;
-    for (const track of this.tracksToAdd) {
-      if (
-        track.trackGroup !== undefined &&
-        track.trackGroup !== SCROLLING_TRACK_GROUP
-      ) {
-        continue;
-      }
+    const group = new GroupNode(groupName);
+    for (const track of globals.workspace.children) {
+      if (!(track instanceof TrackNode)) continue;
       let allowlisted = false;
       for (const regex of ALLOWLIST_REGEXES) {
-        allowlisted = allowlisted || regex.test(track.name);
+        allowlisted = allowlisted || regex.test(track.displayName);
       }
       if (allowlisted) {
         continue;
       }
-      if (groupUuid === undefined) {
-        groupUuid = uuidv4();
-      }
-      track.trackGroup = groupUuid;
+      group.addChild(track);
     }
 
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
+    if (group.children.length > 0) {
+      globals.workspace.addChild(group);
     }
   }
 
   private groupTracksByRegex(regex: RegExp, groupName: string): void {
-    let groupUuid = undefined;
+    const group = new GroupNode(groupName);
 
-    for (const track of this.tracksToAdd) {
-      if (regex.test(track.name)) {
-        if (
-          track.trackGroup !== undefined &&
-          track.trackGroup !== SCROLLING_TRACK_GROUP
-        ) {
-          continue;
-        }
-        if (groupUuid === undefined) {
-          groupUuid = uuidv4();
-        }
-        track.trackGroup = groupUuid;
+    for (const track of globals.workspace.children) {
+      if (track instanceof TrackNode && regex.test(track.displayName)) {
+        group.addChild(track);
       }
     }
 
-    if (groupUuid !== undefined) {
-      const addGroup = Actions.addTrackGroup({
-        name: groupName,
-        key: groupUuid,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
+    if (group.children.length > 0) {
+      globals.workspace.addChild(group);
     }
   }
 
@@ -324,13 +233,7 @@
     const annotationTracks = tracks.filter(
       ({tags}) => tags?.scope === 'annotation',
     );
-
-    interface GroupIds {
-      id: string;
-      summaryTrackKey: string;
-    }
-
-    const groupNameToKeys = new Map<string, GroupIds>();
+    const groups = new Map<string, GroupNode>();
 
     annotationTracks
       .filter(({tags}) => tags?.kind === THREAD_SLICE_TRACK_KIND)
@@ -338,60 +241,42 @@
         const upid = assertExists(td.tags?.upid);
         const groupName = td.tags?.groupName;
 
-        let summaryTrackKey = undefined;
-        let trackGroupId =
-          upid === 0 ? SCROLLING_TRACK_GROUP : this.upidToUuid.get(upid);
+        // We want to try and find a group to put this track in. If groupName is
+        // defined, create a new group or place in existing one if it already
+        // exists Otherwise, try upid to see if we can put this in a process
+        // group
 
-        if (groupName !== undefined) {
-          // If this is the first track encountered for a certain group,
-          // create an id for the group and use this track as the group's
-          // summary track.
-          const groupKeys = groupNameToKeys.get(groupName);
-          if (groupKeys) {
-            trackGroupId = groupKeys.id;
+        let container: ContainerNode;
+        if (groupName) {
+          const existingGroup = groups.get(groupName);
+          if (!existingGroup) {
+            const group = new GroupNode(groupName);
+            group.headerTrackUri = td.uri;
+            container = group;
+            groups.set(groupName, group);
+            globals.workspace.addChild(group);
           } else {
-            trackGroupId = uuidv4();
-            summaryTrackKey = uuidv4();
-            groupNameToKeys.set(groupName, {
-              id: trackGroupId,
-              summaryTrackKey,
-            });
+            container = existingGroup;
+          }
+        } else {
+          const procGroup = this.processGroups.get(upid);
+          if (upid !== 0 && procGroup) {
+            container = procGroup;
+          } else {
+            container = globals.workspace;
           }
         }
 
-        this.tracksToAdd.push({
-          uri: td.uri,
-          key: summaryTrackKey,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-          trackGroup: trackGroupId,
-        });
+        container.addChild(new TrackNode(td.uri, td.title));
       });
 
-    for (const [groupName, groupKeys] of groupNameToKeys) {
-      const addGroup = Actions.addTrackGroup({
-        summaryTrackKey: groupKeys.summaryTrackKey,
-        name: groupName,
-        key: groupKeys.id,
-        collapsed: true,
-      });
-      this.addTrackGroupActions.push(addGroup);
-    }
-
     annotationTracks
       .filter(({tags}) => tags?.kind === COUNTER_TRACK_KIND)
       .forEach((td) => {
         const upid = td.tags?.upid;
-
-        this.tracksToAdd.push({
-          uri: td.uri,
-          key: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
-          trackGroup: exists(upid)
-            ? this.upidToUuid.get(upid)
-            : SCROLLING_TRACK_GROUP,
-        });
+        const parent =
+          (exists(upid) && this.processGroups.get(upid)) || globals.workspace;
+        parent.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -402,27 +287,9 @@
           tags?.kind === THREAD_STATE_TRACK_KIND && tags?.utid !== undefined,
       )
       .forEach((td) => {
-        const upid = td.tags?.upid ?? null;
         const utid = assertExists(td.tags?.utid);
-
-        const groupId = this.getUuidUnchecked(utid, upid);
-        if (groupId === undefined) {
-          // If a thread has no scheduling activity (i.e. the sched table has zero
-          // rows for that uid) no track group will be created and we want to skip
-          // the track creation as well.
-          return;
-        }
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackGroup: groupId,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.THREAD_SCHEDULING_STATE_TRACK,
-          },
-        });
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -436,18 +303,8 @@
       )
       .forEach((td) => {
         const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.CPU_STACK_SAMPLES_TRACK,
-          },
-          trackGroup: groupId,
-        });
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -461,18 +318,8 @@
       )
       .forEach((td) => {
         const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.ORDINARY,
-          },
-          trackGroup: groupId,
-        });
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -488,14 +335,36 @@
       )
       .forEach((td) => {
         const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getProcGroup(upid);
+        group.addChild(new TrackNode(td.uri, td.title));
+      });
+  }
+
+  private addUserAsyncSliceTracks(
+    tracks: ReadonlyArray<TrackDescriptor>,
+  ): void {
+    const groupMap = new Map<string, GroupNode>();
+    tracks
+      .filter(
+        ({tags}) =>
+          tags?.kind === ASYNC_SLICE_TRACK_KIND &&
+          tags?.scope === 'user' &&
+          tags?.rawName !== undefined,
+      )
+      .forEach((td) => {
+        const rawName = td.tags?.rawName;
+        if (typeof rawName === 'string') {
+          const track = new TrackNode(td.uri, td.title);
+          const existingGroup = groupMap.get(rawName);
+          if (existingGroup) {
+            existingGroup.addChild(track);
+          } else {
+            const group = new GroupNode(rawName);
+            globals.workspace.addChild(group);
+            groupMap.set(rawName, group);
+            group.addChild(track);
+          }
+        }
       });
   }
 
@@ -508,15 +377,8 @@
       )
       .forEach((td) => {
         const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getProcGroup(upid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -531,15 +393,8 @@
       )
       .forEach((td) => {
         const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getProcGroup(upid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -551,22 +406,27 @@
       )
       .forEach((td) => {
         const utid = assertExists(td.tags?.utid);
-        const upid = td.tags?.upid ?? null;
-        const isDefaultTrackForScope = Boolean(td.tags?.isDefaultTrackForScope);
-        const groupId = this.getUuid(utid, upid);
+        // const upid = td.tags?.upid;
+        // const isDefaultTrackForScope = Boolean(td.tags?.isDefaultTrackForScope);
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
+      });
+  }
 
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackGroup: groupId,
-          trackSortKey: {
-            utid,
-            priority: isDefaultTrackForScope
-              ? InThreadTrackSortKey.DEFAULT_TRACK
-              : InThreadTrackSortKey.ORDINARY,
-          },
-        });
+  private addAsyncThreadSliceTracks(
+    tracks: ReadonlyArray<TrackDescriptor>,
+  ): void {
+    tracks
+      .filter(
+        ({tags}) =>
+          tags?.kind === ASYNC_SLICE_TRACK_KIND &&
+          tags?.utid !== undefined &&
+          tags?.scope === 'thread',
+      )
+      .forEach((td) => {
+        const utid = assertExists(td.tags?.utid);
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -582,21 +442,11 @@
 
     for (const td of processCounterTracks) {
       const upid = assertExists(td.tags?.upid);
-      const groupId = this.getUuid(null, upid);
-      const trackNameTag = td.tags?.trackName;
-      const trackName =
-        typeof trackNameTag === 'string' ? trackNameTag : undefined;
-
-      this.tracksToAdd.push({
-        key: td.uri,
-        uri: td.uri,
-        name: td.title,
-        trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
-          upid,
-          trackName,
-        ),
-        trackGroup: groupId,
-      });
+      const group = this.getProcGroup(upid);
+      // const trackNameTag = td.tags?.trackName;
+      // const trackName =
+      //   typeof trackNameTag === 'string' ? trackNameTag : undefined;
+      group.addChild(new TrackNode(td.uri, td.title));
     }
   }
 
@@ -610,14 +460,8 @@
       )
       .forEach((td) => {
         const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getProcGroup(upid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -633,14 +477,8 @@
       )
       .forEach((td) => {
         const upid = assertExists(td.tags?.upid);
-        const groupId = this.getUuid(null, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getProcGroup(upid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
@@ -654,44 +492,29 @@
           tags?.utid !== undefined,
       )
       .forEach((td) => {
-        const upid = td.tags?.upid ?? null;
+        // const upid = td.tags?.upid;
         const utid = assertExists(td.tags?.utid);
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
-          trackGroup: groupId,
-        });
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
-  private getUuidUnchecked(utid: number | null, upid: number | null) {
-    return upid === null
-      ? this.utidToUuid.get(utid!)
-      : this.upidToUuid.get(upid);
-  }
-
-  private getUuid(utid: number | null, upid: number | null) {
-    return assertExists(this.getUuidUnchecked(utid, upid));
-  }
-
-  private getOrCreateUuid(utid: number | null, upid: number | null) {
-    let uuid = this.getUuidUnchecked(utid, upid);
-    if (uuid === undefined) {
-      uuid = uuidv4();
-      if (upid === null) {
-        this.utidToUuid.set(utid!, uuid);
-      } else {
-        this.upidToUuid.set(upid, uuid);
-      }
+  private getProcGroup(upid: number): GroupNode {
+    const group = this.processGroups.get(upid);
+    if (group) {
+      return group;
+    } else {
+      throw new Error(`Unable to find proc group with upid ${upid}`);
     }
-    return uuid;
   }
 
-  private setUuidForUpid(upid: number, uuid: string) {
-    this.upidToUuid.set(upid, uuid);
+  private getThreadGroup(utid: number): GroupNode {
+    const group = this.threadGroups.get(utid);
+    if (group) {
+      return group;
+    } else {
+      throw new Error(`Unable to find thread group with utid ${utid}`);
+    }
   }
 
   private async addKernelThreadGrouping(engine: Engine): Promise<void> {
@@ -735,41 +558,25 @@
     // main track. It doesn't summarise the kernel threads within the group,
     // but creating a dedicated track type is out of scope at the time of
     // writing.
-    const kthreadGroupUuid = uuidv4();
-    const summaryTrackKey = uuidv4();
-    this.tracksToAdd.push({
-      uri: '/kernel',
-      key: summaryTrackKey,
-      trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
-      name: `Kernel thread summary`,
-    });
-    const addTrackGroup = Actions.addTrackGroup({
-      summaryTrackKey,
-      name: `Kernel threads`,
-      key: kthreadGroupUuid,
-      collapsed: true,
-    });
-    this.addTrackGroupActions.push(addTrackGroup);
+    const group = new GroupNode('Kernel threads');
+    group.headerTrackUri = '/kernel'; // Summary track
+    globals.workspace.addChild(group);
 
     // Set the group for all kernel threads (including kthreadd itself).
     for (; it.valid(); it.next()) {
-      this.setUuidForUpid(it.upid, kthreadGroupUuid);
+      const {utid} = it;
+
+      const threadGroup = new GroupNode(`Kernel Thread ${utid}`);
+      threadGroup.headless = true;
+      group.addChild(threadGroup);
+
+      this.threadGroups.set(utid, threadGroup);
     }
   }
 
-  private async addProcessTrackGroups(engine: Engine): Promise<void> {
-    // We want to create groups of tracks in a specific order.
-    // The tracks should be grouped:
-    //    by upid
-    //    or (if upid is null) by utid
-    // the groups should be sorted by:
-    //  Chrome-based process rank based on process names (e.g. Browser)
-    //  has a heap profile or not
-    //  total cpu time *for the whole parent process*
-    //  process name
-    //  upid
-    //  thread name
-    //  utid
+  // Adds top level groups for processes and thread that don't belong to a
+  // process.
+  private async addProcessGroups(engine: Engine): Promise<void> {
     const result = await engine.query(`
       with processGroups as (
         select
@@ -813,16 +620,10 @@
       select *
       from (
         select
-          upid,
-          null as utid,
-          pid,
-          null as tid,
-          processName,
-          null as threadName,
-          sumRunningDur > 0 as hasSched,
-          heapProfileAllocationCount > 0
-            or heapGraphObjectCount > 0 as hasHeapInfo,
-          ifnull(chromeProcessLabels, '') as chromeProcessLabels
+          'process' as kind,
+          upid as uid,
+          pid as id,
+          processName as name
         from processGroups
         order by
           chromeProcessRank desc,
@@ -838,15 +639,10 @@
       select *
       from (
         select
-          null,
-          utid,
-          null as pid,
-          tid,
-          null as processName,
-          threadName,
-          sumRunningDur > 0 as hasSched,
-          0 as hasHeapInfo,
-          '' as chromeProcessLabels
+          'thread' as kind,
+          utid as uid,
+          tid as id,
+          threadName as name
         from threadGroups
         order by
           perfSampleCount desc,
@@ -858,172 +654,176 @@
   `);
 
     const it = result.iter({
-      upid: NUM_NULL,
-      utid: NUM_NULL,
-      pid: NUM_NULL,
-      tid: NUM_NULL,
-      processName: STR_NULL,
-      threadName: STR_NULL,
-      hasSched: NUM_NULL,
-      hasHeapInfo: NUM_NULL,
+      kind: STR,
+      uid: NUM,
+      id: NUM,
+      name: STR_NULL,
     });
     for (; it.valid(); it.next()) {
-      const utid = it.utid;
-      const upid = it.upid;
-      const pid = it.pid;
-      const tid = it.tid;
-      const threadName = it.threadName;
-      const processName = it.processName;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const hasSched = !!it.hasSched;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const hasHeapInfo = !!it.hasHeapInfo;
+      const {kind, uid, id, name} = it;
 
-      const summaryTrackKey = uuidv4();
+      if (kind === 'process') {
+        // Ignore kernel process groups
+        if (this.processGroups.has(uid)) {
+          continue;
+        }
 
-      const uri = getThreadOrProcUri(upid, utid);
+        function getProcessDisplayName(
+          processName: string | undefined,
+          pid: number,
+        ) {
+          if (processName) {
+            return `${stripPathFromExecutable(processName)} ${pid}`;
+          } else {
+            return `Process ${pid}`;
+          }
+        }
 
-      // If previous groupings (e.g. kernel threads) picked up there tracks,
-      // don't try to regroup them.
-      const pUuid =
-        upid === null ? this.utidToUuid.get(utid!) : this.upidToUuid.get(upid);
-      if (pUuid !== undefined) {
-        continue;
+        const displayName = getProcessDisplayName(name ?? undefined, id);
+
+        const group = new GroupNode(displayName);
+        group.headerTrackUri = `/process_${uid}`; // Summary track URI
+        globals.workspace.addChild(group);
+        this.processGroups.set(uid, group);
+      } else {
+        // Ignore kernel process groups
+        if (this.threadGroups.has(uid)) {
+          continue;
+        }
+
+        function getThreadDisplayName(
+          threadName: string | undefined,
+          pid: number,
+        ) {
+          if (threadName) {
+            return `${stripPathFromExecutable(threadName)} ${pid}`;
+          } else {
+            return `Thread ${pid}`;
+          }
+        }
+
+        const displayName = getThreadDisplayName(name ?? undefined, id);
+
+        const group = new GroupNode(displayName);
+        group.headerTrackUri = `/thread_${uid}`; // Summary track URI
+        globals.workspace.addChild(group);
+        this.threadGroups.set(uid, group);
       }
-
-      this.tracksToAdd.push({
-        uri,
-        key: summaryTrackKey,
-        trackSortKey: hasSched
-          ? PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK
-          : PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
-        name: `${upid === null ? tid : pid} summary`,
-      });
-
-      const name = getTrackName({
-        utid,
-        processName,
-        pid,
-        threadName,
-        tid,
-        upid,
-      });
-
-      const addTrackGroup = Actions.addTrackGroup({
-        summaryTrackKey,
-        name: stripPathFromExecutable(name),
-        key: this.getOrCreateUuid(utid, upid),
-        // Perf profiling tracks remain collapsed, otherwise we would have too
-        // many expanded process tracks for some perf traces, leading to
-        // jankyness.
-        collapsed: !hasHeapInfo,
-      });
-      this.addTrackGroupActions.push(addTrackGroup);
     }
   }
 
-  private async computeThreadOrderingMetadata(): Promise<UtidToTrackSortKey> {
-    const result = await this.engine.query(`
-      select
-        utid,
-        tid,
-        (select pid from process p where t.upid = p.upid) as pid,
-        t.name as threadName
-      from thread t
-    `);
+  // Create all the nested & headless thread groups that live inside existing
+  // process groups.
+  private async addThreadGroups(engine: Engine): Promise<void> {
+    const result = await engine.query(`
+      with threadGroups as (
+        select
+          utid,
+          upid,
+          tid,
+          thread.name as threadName,
+          CASE
+            WHEN thread.is_main_thread = 1 THEN 10
+            WHEN thread.name = 'CrBrowserMain' THEN 10
+            WHEN thread.name = 'CrRendererMain' THEN 10
+            WHEN thread.name = 'CrGpuMain' THEN 10
+            WHEN thread.name glob '*RenderThread*' THEN 9
+            WHEN thread.name glob '*GPU completion*' THEN 8
+            WHEN thread.name = 'Chrome_ChildIOThread' THEN 7
+            WHEN thread.name = 'Chrome_IOThread' THEN 7
+            WHEN thread.name = 'Compositor' THEN 6
+            WHEN thread.name = 'VizCompositorThread' THEN 6
+            ELSE 5
+          END as priority
+        from _thread_available_info_summary
+        join thread using (utid)
+        where upid is not null
+      )
+      select *
+      from (
+        select
+          utid,
+          upid,
+          tid,
+          threadName
+        from threadGroups
+        order by
+          priority desc,
+          tid asc
+      )
+  `);
 
     const it = result.iter({
       utid: NUM,
-      tid: NUM_NULL,
-      pid: NUM_NULL,
+      tid: NUM,
+      upid: NUM,
       threadName: STR_NULL,
     });
-
-    const threadOrderingMetadata: UtidToTrackSortKey = {};
     for (; it.valid(); it.next()) {
-      threadOrderingMetadata[it.utid] = {
-        tid: it.tid === null ? undefined : it.tid,
-        sortKey: TrackDecider.getThreadSortKey(it.threadName, it.tid, it.pid),
-      };
+      const {utid, tid, upid, threadName} = it;
+
+      // Ignore kernel thread groups
+      if (this.threadGroups.has(utid)) {
+        continue;
+      }
+
+      const threadGroup = new GroupNode(threadName ?? `Thread ${tid}`);
+      this.threadGroups.set(utid, threadGroup);
+      threadGroup.headless = true;
+      threadGroup.expand();
+
+      this.processGroups.get(upid)?.addChild(threadGroup);
     }
-    return threadOrderingMetadata;
   }
 
   private addPluginTracks(): void {
-    const groupNameToUuid = new Map<string, string>();
-    const tracks = globals.trackManager.findPotentialTracks();
+    const groups = new Map<string, GroupNode>();
+    const tracks = globals.trackManager.getAutoShowTracks();
 
     for (const info of tracks) {
-      const groupName = info.groupName;
+      const groupName = info.tags?.groupName;
+      let container: ContainerNode = globals.workspace;
 
-      let groupUuid = SCROLLING_TRACK_GROUP;
       if (groupName) {
-        const uuid = groupNameToUuid.get(groupName);
-        if (uuid) {
-          groupUuid = uuid;
+        const existingGroup = groups.get(groupName);
+        if (existingGroup) {
+          container = existingGroup;
         } else {
           // Add the group
-          groupUuid = uuidv4();
-          const addGroup = Actions.addTrackGroup({
-            name: groupName,
-            key: groupUuid,
-            collapsed: true,
-            fixedOrdering: true,
-          });
-          this.addTrackGroupActions.push(addGroup);
-
-          // Add group to the map
-          groupNameToUuid.set(groupName, groupUuid);
+          const group = new GroupNode(groupName);
+          container = group;
+          globals.workspace.addChild(group);
+          groups.set(groupName, group);
         }
       }
 
-      this.tracksToAdd.push({
-        uri: info.uri,
-        key: info.uri,
-        name: info.title,
-        // TODO(hjd): Fix how sorting works. Plugins should expose
-        // 'sort keys' which the user can use to choose a sort order.
-        trackSortKey: info.sortKey ?? PrimaryTrackSortKey.ORDINARY_TRACK,
-        trackGroup: groupUuid,
-      });
-
-      if (info.isPinned) {
-        this.tracksToPin.push(info.uri);
-      }
+      const track = new TrackNode(info.uri, info.title);
+      container.addChild(track);
     }
   }
 
   private addScrollJankPluginTracks(
     tracks: ReadonlyArray<TrackDescriptor>,
   ): void {
-    let scrollTracks = this.addTracks(
-      tracks,
-      ({tags}) => tags?.kind === CHROME_TOPLEVEL_SCROLLS_KIND,
-      SCROLL_JANK_GROUP_ID,
-    );
-    scrollTracks = scrollTracks.concat(
-      this.addTracks(
-        tracks,
-        ({tags}) => tags?.kind === SCROLL_JANK_V3_TRACK_KIND,
-        SCROLL_JANK_GROUP_ID,
-      ),
-    );
-    scrollTracks = scrollTracks.concat(
-      this.addTracks(
-        tracks,
-        ({tags}) => tags?.kind === CHROME_EVENT_LATENCY_TRACK_KIND,
-        SCROLL_JANK_GROUP_ID,
-      ),
-    );
-    if (scrollTracks.length > 0) {
-      this.addTrackGroupActions.push(
-        Actions.addTrackGroup({
-          name: 'Chrome Scroll Jank',
-          key: SCROLL_JANK_GROUP_ID,
-          collapsed: false,
-          fixedOrdering: true,
-        }),
-      );
+    const group = new GroupNode('Chrome Scroll Jank');
+    group.expand();
+    tracks
+      .filter(({tags}) => tags?.kind === CHROME_TOPLEVEL_SCROLLS_KIND)
+      .forEach((td) => {
+        group.addChild(new TrackNode(td.uri, td.title));
+      });
+    tracks
+      .filter(({tags}) => tags?.kind === SCROLL_JANK_V3_TRACK_KIND)
+      .forEach((td) => {
+        group.addChild(new TrackNode(td.uri, td.title));
+      });
+    tracks
+      .filter(({tags}) => tags?.kind === CHROME_EVENT_LATENCY_TRACK_KIND)
+      .forEach((td) => {
+        group.addChild(new TrackNode(td.uri, td.title));
+      });
+    if (group.children.length) {
+      globals.workspace.addChild(group);
     }
   }
 
@@ -1033,45 +833,28 @@
     tracks
       .filter(({tags}) => tags?.kind === CHROME_SCROLL_JANK_TRACK_KIND)
       .forEach((td) => {
-        const upid = assertExists(td.tags?.upid);
         const utid = assertExists(td.tags?.utid);
-        const groupId = this.getUuid(utid, upid);
-        this.tracksToAdd.push({
-          key: td.uri,
-          uri: td.uri,
-          name: td.title,
-          trackSortKey: {
-            utid,
-            priority: InThreadTrackSortKey.ORDINARY,
-          },
-          trackGroup: groupId,
-        });
+        const group = this.getThreadGroup(utid);
+        group.addChild(new TrackNode(td.uri, td.title));
       });
   }
 
   // Add an ordinary track from a track descriptor
-  private addTrack(track: TrackDescriptor, groupId?: string): void {
-    this.tracksToAdd.push({
-      key: track.uri,
-      uri: track.uri,
-      name: track.title,
-      trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      trackGroup: groupId ?? SCROLLING_TRACK_GROUP,
-    });
+  private addTrack(track: TrackDescriptor): void {
+    globals.workspace.addChild(new TrackNode(track.uri, track.title));
   }
 
   // Add tracks that match some predicate
   private addTracks(
     source: ReadonlyArray<TrackDescriptor>,
     predicate: (td: TrackDescriptor) => boolean,
-    groupId?: string,
   ): ReadonlyArray<TrackDescriptor> {
     const filteredTracks = source.filter(predicate);
-    filteredTracks.forEach((a) => this.addTrack(a, groupId));
+    filteredTracks.forEach((a) => this.addTrack(a));
     return filteredTracks;
   }
 
-  public async decideTracks(): Promise<DeferredAction[]> {
+  public async decideTracks(): Promise<void> {
     const tracks = globals.trackManager.getAllTracks();
 
     // Add first the global tracks that don't require per-process track groups.
@@ -1125,11 +908,7 @@
     // Add user slice tracks before listing the processes. These tracks will
     // be listed with their user/package name only, and they will be grouped
     // under on their original shared track names. E.g. "GPU Work Period"
-    this.addTracks(
-      tracks,
-      ({tags}) =>
-        tags?.kind === ASYNC_SLICE_TRACK_KIND && tags?.scope === 'user',
-    );
+    this.addUserAsyncSliceTracks(tracks);
 
     // Pre-group all kernel "threads" (actually processes) if this is a linux
     // system trace. Below, addProcessTrackGroups will skip them due to an
@@ -1145,110 +924,57 @@
     // create a track per process. If a process has been completely idle and has
     // no sched events, no track group will be emitted.
     // Will populate this.addTrackGroupActions
-    await this.addProcessTrackGroups(
+    await this.addProcessGroups(
       this.engine.getProxy('TrackDecider::addProcessTrackGroups'),
     );
 
-    this.addProcessHeapProfileTracks(tracks);
-    this.addProcessPerfSamplesTracks(tracks);
-    this.addThreadPerfSamplesTracks(tracks);
-    await this.addProcessCounterTracks(tracks);
-    this.addProcessAsyncSliceTracks(tracks);
-    this.addActualFramesTracks(tracks);
     this.addExpectedFramesTracks(tracks);
-    this.addThreadCounterTracks(tracks);
+    this.addActualFramesTracks(tracks);
+    this.addProcessPerfSamplesTracks(tracks);
+    this.addProcessHeapProfileTracks(tracks);
+
+    await this.addThreadGroups(
+      this.engine.getProxy('TrackDecider::addThreadTrackGroups'),
+    );
+
+    this.addThreadPerfSamplesTracks(tracks);
+    this.addThreadCpuSampleTracks(tracks);
     this.addThreadStateTracks(tracks);
     this.addThreadSliceTracks(tracks);
-    this.addThreadCpuSampleTracks(tracks);
+    this.addThreadCounterTracks(tracks);
+
+    await this.addProcessCounterTracks(tracks);
+    this.addProcessAsyncSliceTracks(tracks);
+    this.addAsyncThreadSliceTracks(tracks);
 
     this.addChromeScrollJankTrack(tracks);
 
-    this.addTrackGroupActions.push(
-      Actions.addTracks({tracks: this.tracksToAdd}),
-    );
-
-    // Add the actions to pin any tracks we need to pin
-    for (const trackKey of this.tracksToPin) {
-      this.addTrackGroupActions.push(Actions.toggleTrackPinned({trackKey}));
-    }
-
-    const threadOrderingMetadata = await this.computeThreadOrderingMetadata();
-    this.addTrackGroupActions.push(
-      Actions.setUtidToTrackSortKey({threadOrderingMetadata}),
-    );
-
-    return this.addTrackGroupActions;
-  }
-
-  // Some process counter tracks are tied to specific threads based on their
-  // name.
-  private async resolveTrackSortKeyForProcessCounterTrack(
-    upid: number,
-    threadName?: string,
-  ): Promise<TrackSortKey> {
-    if (threadName !== 'GPU completion') {
-      return PrimaryTrackSortKey.COUNTER_TRACK;
-    }
-    const result = await this.engine.query(`
-      select utid
-      from thread
-      where upid=${upid} and name=${sqliteString(threadName)}
-    `);
-    const it = result.iter({
-      utid: NUM,
+    // Remove any empty groups
+    globals.workspace.children.forEach((n) => {
+      if (n instanceof GroupNode && n.children.length === 0) {
+        globals.workspace.removeChild(n);
+      }
     });
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    for (; it; it.next()) {
-      return {
-        utid: it.utid,
-        priority: InThreadTrackSortKey.THREAD_COUNTER_TRACK,
-      };
-    }
-    return PrimaryTrackSortKey.COUNTER_TRACK;
-  }
 
-  private static getThreadSortKey(
-    threadName?: string | null,
-    tid?: number | null,
-    pid?: number | null,
-  ): PrimaryTrackSortKey {
-    if (pid !== undefined && pid !== null && pid === tid) {
-      return PrimaryTrackSortKey.MAIN_THREAD;
-    }
-    if (threadName === undefined || threadName === null) {
-      return PrimaryTrackSortKey.ORDINARY_THREAD;
-    }
+    // Move groups underneath tracks
+    Array.from(globals.workspace.children)
+      .sort((a, b) => {
+        // Define the desired order
+        const order = [TrackNode, GroupNode];
 
-    // Chrome main threads should always come first within their process.
-    if (
-      threadName === 'CrBrowserMain' ||
-      threadName === 'CrRendererMain' ||
-      threadName === 'CrGpuMain'
-    ) {
-      return PrimaryTrackSortKey.MAIN_THREAD;
-    }
+        // Get the index in the order array
+        const indexA = order.findIndex((type) => a instanceof type);
+        const indexB = order.findIndex((type) => b instanceof type);
 
-    // Chrome IO threads should always come immediately after the main thread.
-    if (
-      threadName === 'Chrome_ChildIOThread' ||
-      threadName === 'Chrome_IOThread'
-    ) {
-      return PrimaryTrackSortKey.CHROME_IO_THREAD;
-    }
+        // Sort based on the index in the order array
+        return indexA - indexB;
+      })
+      .forEach((n) => globals.workspace.addChild(n));
 
-    // A Chrome process can have only one compositor thread, so we want to put
-    // it next to other named processes.
-    if (threadName === 'Compositor' || threadName === 'VizCompositorThread') {
-      return PrimaryTrackSortKey.CHROME_COMPOSITOR_THREAD;
-    }
-
-    switch (true) {
-      case /.*RenderThread.*/.test(threadName):
-        return PrimaryTrackSortKey.RENDER_THREAD;
-      case /.*GPU completion.*/.test(threadName):
-        return PrimaryTrackSortKey.GPU_COMPLETION_THREAD;
-      default:
-        return PrimaryTrackSortKey.ORDINARY_THREAD;
+    // If there is only one group, expand it
+    const groups = globals.workspace.children;
+    if (groups.length === 1 && groups[0] instanceof GroupNode) {
+      groups[0].expand();
     }
   }
 }
diff --git a/ui/src/core/colorizer.ts b/ui/src/core/colorizer.ts
index 3f0d1b3..ac495d3 100644
--- a/ui/src/core/colorizer.ts
+++ b/ui/src/core/colorizer.ts
@@ -238,13 +238,10 @@
   return materialColorScheme(name);
 }
 
-export function colorForSample(callsiteId: number, isHovered: boolean): string {
-  let colorScheme;
+export function getColorForSample(callsiteId: number): ColorScheme {
   if (USE_CONSISTENT_COLORS.get()) {
-    colorScheme = materialColorScheme(String(callsiteId));
+    return materialColorScheme(String(callsiteId));
   } else {
-    colorScheme = proceduralColorScheme(String(callsiteId));
+    return proceduralColorScheme(String(callsiteId));
   }
-
-  return isHovered ? colorScheme.variant.cssString : colorScheme.base.cssString;
 }
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index d057f66..1cba444 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -47,6 +47,7 @@
   'perfetto.CpuFreq',
   'perfetto.CpuProfile',
   'perfetto.CpuSlices',
+  'perfetto.CriticalPath',
   'perfetto.CriticalUserInteraction',
   'perfetto.DebugTracks',
   'perfetto.ExampleTraces',
diff --git a/ui/src/core/legacy_flamegraph_cache.ts b/ui/src/core/legacy_flamegraph_cache.ts
deleted file mode 100644
index ae37877..0000000
--- a/ui/src/core/legacy_flamegraph_cache.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {Engine} from '../trace_processor/engine';
-
-export class LegacyFlamegraphCache {
-  private cache: Map<string, string>;
-  private prefix: string;
-  private tableId: number;
-  private cacheSizeLimit: number;
-
-  constructor(prefix: string) {
-    this.cache = new Map<string, string>();
-    this.prefix = prefix;
-    this.tableId = 0;
-    this.cacheSizeLimit = 10;
-  }
-
-  async getTableName(engine: Engine, query: string): Promise<string> {
-    let tableName = this.cache.get(query);
-    if (tableName === undefined) {
-      // TODO(hjd): This should be LRU.
-      if (this.cache.size > this.cacheSizeLimit) {
-        for (const name of this.cache.values()) {
-          await engine.query(`drop table ${name}`);
-        }
-        this.cache.clear();
-      }
-      tableName = `${this.prefix}_${this.tableId++}`;
-      await engine.query(
-        `create temp table if not exists ${tableName} as ${query}`,
-      );
-      this.cache.set(query, tableName);
-    }
-    return tableName;
-  }
-
-  hasQuery(query: string): boolean {
-    return this.cache.get(query) !== undefined;
-  }
-}
diff --git a/ui/src/core/query_flamegraph.ts b/ui/src/core/query_flamegraph.ts
index 802aebc..8fdfc70 100644
--- a/ui/src/core/query_flamegraph.ts
+++ b/ui/src/core/query_flamegraph.ts
@@ -32,8 +32,6 @@
   FlamegraphView,
 } from '../widgets/flamegraph';
 
-import {featureFlags} from './feature_flags';
-
 export interface QueryFlamegraphColumn {
   // The name of the column in SQL.
   readonly name: string;
@@ -452,10 +450,3 @@
   }
   return '0';
 }
-
-export const USE_NEW_FLAMEGRAPH_IMPL = featureFlags.register({
-  id: 'useNewFlamegraphImpl',
-  name: 'Use new flamegraph implementation',
-  description: 'Use new flamgraph implementation in details panels.',
-  defaultValue: true,
-});
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 2ddb75b..84271c3 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -26,6 +26,19 @@
   PERF_SAMPLE = 'perf',
 }
 
+export function profileType(s: string): ProfileType {
+  if (s === 'heap_profile:libc.malloc,com.android.art') {
+    s = 'heap_profile:com.android.art,libc.malloc';
+  }
+  if (Object.values(ProfileType).includes(s as ProfileType)) {
+    return s as ProfileType;
+  }
+  if (s.startsWith('heap_profile')) {
+    return ProfileType.HEAP_PROFILE;
+  }
+  throw new Error('Unknown type ${s}');
+}
+
 // LEGACY Selection types:
 export interface SliceSelection {
   kind: 'SCHED_SLICE';
@@ -71,7 +84,7 @@
 export interface LogSelection {
   kind: 'LOG';
   id: number;
-  trackKey: string;
+  trackUri: string;
 }
 
 export interface GenericSliceSelection {
@@ -93,7 +106,7 @@
   | PerfSamplesSelection
   | LogSelection
   | GenericSliceSelection
-) & {trackKey?: string};
+) & {trackUri?: string};
 export type SelectionKind = LegacySelection['kind']; // 'THREAD_STATE' | 'SLICE' ...
 
 // New Selection types:
@@ -104,13 +117,13 @@
 
 export interface SingleSelection {
   kind: 'single';
-  trackKey: string;
+  trackUri: string;
   eventId: number;
 }
 
 export interface AreaSelection {
   kind: 'area';
-  tracks: string[];
+  trackUris: string[];
   start: time;
   end: time;
 }
@@ -217,22 +230,22 @@
   }
 
   setEvent(
-    trackKey: string,
+    trackUri: string,
     eventId: number,
     legacySelection?: LegacySelection,
   ) {
     this.clear();
-    this.addEvent(trackKey, eventId, legacySelection);
+    this.addEvent(trackUri, eventId, legacySelection);
   }
 
   addEvent(
-    trackKey: string,
+    trackUri: string,
     eventId: number,
     legacySelection?: LegacySelection,
   ) {
     this.addSelection({
       kind: 'single',
-      trackKey,
+      trackUri,
       eventId,
     });
     if (legacySelection) {
diff --git a/ui/src/core/track_kinds.ts b/ui/src/core/track_kinds.ts
index 93dec00..05671c9 100644
--- a/ui/src/core/track_kinds.ts
+++ b/ui/src/core/track_kinds.ts
@@ -35,3 +35,4 @@
   'org.chromium.ScrollJank.scroll_jank_v3_track';
 export const CHROME_SCROLL_JANK_TRACK_KIND =
   'org.chromium.ScrollJank.BrowserUIThreadLongTasks';
+export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
diff --git a/ui/src/core_plugins/android_log/index.ts b/ui/src/core_plugins/android_log/index.ts
index 67e840a..7ecaf75 100644
--- a/ui/src/core_plugins/android_log/index.ts
+++ b/ui/src/core_plugins/android_log/index.ts
@@ -17,6 +17,7 @@
 import {LogFilteringCriteria, LogPanel} from './logs_panel';
 import {
   PerfettoPlugin,
+  ANDROID_LOGS_TRACK_KIND,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
@@ -24,8 +25,6 @@
 import {AndroidLogTrack} from './logs_track';
 import {exists} from '../../base/utils';
 
-export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
-
 const VERSION = 1;
 
 const DEFAULT_STATE: AndroidLogPluginState = {
@@ -57,11 +56,11 @@
     );
     const logCount = result.firstRow({cnt: NUM}).cnt;
     if (logCount > 0) {
-      ctx.registerStaticTrack({
+      ctx.registerTrackAndShowOnTraceLoad({
         uri: 'perfetto.AndroidLog',
         title: 'Android logs',
         tags: {kind: ANDROID_LOGS_TRACK_KIND},
-        trackFactory: () => new AndroidLogTrack(ctx.engine),
+        track: new AndroidLogTrack(ctx.engine),
       });
     }
 
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index da0db45..0e34ca0 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -52,8 +52,9 @@
     for (; it.valid(); it.next()) {
       const {id, name, upid, groupName} = it;
 
+      const uri = `/annotation_${id}`;
       ctx.registerTrack({
-        uri: `/annotation_${id}`,
+        uri,
         title: name,
         tags: {
           kind: THREAD_SLICE_TRACK_KIND,
@@ -62,17 +63,15 @@
           ...(groupName && {groupName}),
         },
         chips: ['metric'],
-        trackFactory: ({trackKey}) => {
-          return new ThreadSliceTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            id,
-            0,
-            'annotation_slice',
-          );
-        },
+        track: new ThreadSliceTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          id,
+          0,
+          'annotation_slice',
+        ),
       });
     }
   }
@@ -99,8 +98,9 @@
     for (; counterIt.valid(); counterIt.next()) {
       const {id: trackId, name, upid} = counterIt;
 
+      const uri = `/annotation_counter_${trackId}`;
       ctx.registerTrack({
-        uri: `/annotation_counter_${trackId}`,
+        uri,
         title: name,
         tags: {
           kind: COUNTER_TRACK_KIND,
@@ -108,14 +108,12 @@
           upid,
         },
         chips: ['metric'],
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId,
-            rootTable: 'annotation_counter',
-          });
-        },
+        track: new TraceProcessorCounterTrack({
+          engine: ctx.engine,
+          uri,
+          trackId,
+          rootTable: 'annotation_counter',
+        }),
       });
     }
   }
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index d9b244e..f636191 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {removeFalsyValues} from '../../base/array_utils';
 import {ASYNC_SLICE_TRACK_KIND} from '../../public';
 import {
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
-import {getTrackName} from '../../public/utils';
+import {getThreadUriPrefix, getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
 import {AsyncSliceTrack} from './async_slice_track';
@@ -27,6 +28,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addGlobalAsyncTracks(ctx);
     await this.addProcessAsyncSliceTracks(ctx);
+    await this.addThreadAsyncSliceTracks(ctx);
     await this.addUserAsyncSliceTracks(ctx);
   }
 
@@ -68,17 +70,16 @@
       const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const maxDepth = it.maxDepth;
 
+      const uri = `/async_slices_${rawName}_${it.parentId}`;
       ctx.registerTrack({
-        uri: `/async_slices_${rawName}_${it.parentId}`,
+        uri,
         title: displayName,
         tags: {
           trackIds,
           kind: ASYNC_SLICE_TRACK_KIND,
           scope: 'global',
         },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
-        },
+        track: new AsyncSliceTrack({engine, uri}, maxDepth, trackIds),
       });
     }
   }
@@ -123,8 +124,9 @@
         kind,
       });
 
+      const uri = `/process_${upid}/async_slices_${rawTrackIds}`;
       ctx.registerTrack({
-        uri: `/process_${upid}/async_slices_${rawTrackIds}`,
+        uri,
         title: displayName,
         tags: {
           trackIds,
@@ -132,13 +134,88 @@
           scope: 'process',
           upid,
         },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack(
-            {engine: ctx.engine, trackKey},
-            maxDepth,
-            trackIds,
-          );
+        track: new AsyncSliceTrack(
+          {engine: ctx.engine, uri},
+          maxDepth,
+          trackIds,
+        ),
+      });
+    }
+  }
+
+  async addThreadAsyncSliceTracks(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+      include perfetto module viz.summary.slices;
+      include perfetto module viz.summary.threads;
+      include perfetto module viz.threads;
+
+      select
+        t.utid,
+        thread.upid,
+        t.name as trackName,
+        thread.name as threadName,
+        thread.tid as tid,
+        t.track_ids as trackIds,
+        __max_layout_depth(t.track_count, t.track_ids) as maxDepth,
+        k.is_main_thread as isMainThread,
+        k.is_kernel_thread AS isKernelThread
+      from _thread_track_summary_by_utid_and_name t
+      join _threads_with_kernel_flag k using(utid)
+      join thread using (utid)
+      where t.track_count > 1
+    `);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      trackName: STR_NULL,
+      trackIds: STR,
+      maxDepth: NUM,
+      isMainThread: NUM_NULL,
+      isKernelThread: NUM,
+      threadName: STR_NULL,
+      tid: NUM_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const {
+        utid,
+        upid,
+        trackName,
+        isMainThread,
+        isKernelThread,
+        maxDepth,
+        threadName,
+        tid,
+      } = it;
+      const rawTrackIds = it.trackIds;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const displayName = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        threadName,
+        kind: 'Slices',
+      });
+
+      const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`;
+      ctx.registerTrack({
+        uri,
+        title: displayName,
+        tags: {
+          trackIds,
+          kind: ASYNC_SLICE_TRACK_KIND,
+          scope: 'thread',
+          utid,
+          upid: upid ?? undefined,
         },
+        chips: removeFalsyValues([
+          isKernelThread === 0 && isMainThread === 1 && 'main thread',
+        ]),
+        track: new AsyncSliceTrack(
+          {engine: ctx.engine, uri},
+          maxDepth,
+          trackIds,
+        ),
       });
     }
   }
@@ -194,17 +271,17 @@
         uidTrack: true,
       });
 
+      const uri = `/async_slices_${rawName}_${uid}`;
       ctx.registerTrack({
-        uri: `/async_slices_${rawName}_${uid}`,
+        uri,
         title: displayName,
         tags: {
           trackIds,
           kind: ASYNC_SLICE_TRACK_KIND,
           scope: 'user',
+          rawName, // Defines grouping
         },
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
-        },
+        track: new AsyncSliceTrack({engine, uri}, maxDepth, trackIds),
       });
     }
   }
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
index d074b46..ce46c9f 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -139,7 +139,7 @@
         sqlTableName: this.tableName,
         start: args.slice.ts,
         duration: args.slice.dur,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
         detailsPanelConfig: {
           kind: detailsPanelConfig.kind,
           config: detailsPanelConfig.config,
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index 7ecc73d..1bb7980 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -14,36 +14,29 @@
 
 import {v4 as uuidv4} from 'uuid';
 
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
 import {
   BottomTabToSCSAdapter,
   PerfettoPlugin,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
-  PrimaryTrackSortKey,
 } from '../../public';
 
 import {PageLoadDetailsPanel} from './page_load_details_panel';
 import {StartupDetailsPanel} from './startup_details_panel';
 import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
 import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
+import {TrackNode} from '../../public/workspace';
+import {globals} from '../../frontend/globals';
 
 function addCriticalUserInteractionTrack() {
-  const trackKey = uuidv4();
-  globals.dispatchMultiple([
-    Actions.addTrack({
-      key: trackKey,
-      uri: CriticalUserInteractionTrack.kind,
-      name: `Chrome Interactions`,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    }),
-    Actions.toggleTrackPinned({trackKey}),
-  ]);
+  const track = new TrackNode(
+    CriticalUserInteractionTrack.kind,
+    'Chrome Interactions',
+  );
+  globals.workspace.addChild(track);
+  track.pin();
 }
 
 class CriticalUserInteractionPlugin implements PerfettoPlugin {
@@ -54,11 +47,10 @@
         kind: CriticalUserInteractionTrack.kind,
       },
       title: 'Chrome Interactions',
-      trackFactory: (trackCtx) =>
-        new CriticalUserInteractionTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-        }),
+      track: new CriticalUserInteractionTrack({
+        engine: ctx.engine,
+        uri: CriticalUserInteractionTrack.kind,
+      }),
     });
 
     ctx.registerDetailsPanel(
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
index cefbed8..c6232a2 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/common.ts
@@ -48,12 +48,12 @@
 
   public registerTrack(args: {
     kind: string;
-    trackKey: string;
+    trackUri: string;
     tableName: string;
     detailsPanelConfig: CustomSqlDetailsPanelConfig;
   }): void {
     this.tracks[args.kind] = {
-      key: args.trackKey,
+      key: args.trackUri,
       sqlTableName: args.tableName,
       detailsPanelConfig: args.detailsPanelConfig,
     };
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
index a170a22..88e56de 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
@@ -60,6 +60,7 @@
 } from './scroll_jank_slice';
 import {sliceRef} from '../../frontend/widgets/slice';
 import {SCROLL_JANK_V3_TRACK_KIND} from '../../public';
+import {globals} from '../../frontend/globals';
 
 // Given a node in the slice tree, return a path from root to it.
 function getPath(slice: SliceTreeNode): string[] {
@@ -132,6 +133,8 @@
   // Stages tree for the prev EventLatency.
   private prevEventLatencyBreakdown?: SliceTreeNode;
 
+  private tracksByTrackId: Map<number, string>;
+
   static create(
     args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
   ): EventLatencySliceDetailsPanel {
@@ -141,6 +144,13 @@
   constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
     super(args);
 
+    this.tracksByTrackId = new Map<number, string>();
+    globals.trackManager.getAllTracks().forEach((td) => {
+      td.tags?.trackIds?.forEach((trackId) => {
+        this.tracksByTrackId.set(trackId, td.uri);
+      });
+    });
+
     this.loadData();
   }
 
@@ -326,7 +336,7 @@
 
     const columns: ColumnDescriptor<RelevantThreadRow>[] = [
       widgetColumn<RelevantThreadRow>('Relevant Thread', (x) =>
-        getCauseLink(x.tracks, x.ts, x.dur),
+        getCauseLink(x.tracks, this.tracksByTrackId, x.ts, x.dur),
       ),
       widgetColumn<RelevantThreadRow>('Description', (x) => {
         if (x.description === '') {
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 4660e51..0dbc019 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -37,7 +37,7 @@
     super(args);
     ScrollJankPluginState.getInstance().registerTrack({
       kind: CHROME_EVENT_LATENCY_TRACK_KIND,
-      trackKey: this.trackKey,
+      trackUri: this.uri,
       tableName: this.tableName,
       detailsPanelConfig: this.getDetailsPanel(),
     });
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index bfcbd43..f24539e 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -111,20 +111,19 @@
     });
 
     const {upid, utid} = it;
+    const uri = 'perfetto.ChromeScrollJank';
     ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank',
+      uri,
       title: 'Scroll Jank causes - long tasks',
       tags: {
         kind: CHROME_SCROLL_JANK_TRACK_KIND,
         upid,
         utid,
       },
-      trackFactory: ({trackKey}) => {
-        return new ChromeTasksScrollJankTrack({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
+      track: new ChromeTasksScrollJankTrack({
+        engine: ctx.engine,
+        uri,
+      }),
     });
   }
 
@@ -134,18 +133,17 @@
       INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
     `);
 
+    const uri = 'perfetto.ChromeScrollJank#toplevelScrolls';
     ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#toplevelScrolls',
+      uri,
       title: 'Chrome Scrolls',
       tags: {
         kind: CHROME_TOPLEVEL_SCROLLS_KIND,
       },
-      trackFactory: ({trackKey}) => {
-        return new TopLevelScrollTrack({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
+      track: new TopLevelScrollTrack({
+        engine: ctx.engine,
+        uri,
+      }),
     });
 
     ctx.registerDetailsPanel(
@@ -265,15 +263,14 @@
     );
     await ctx.engine.query(tableDefSql);
 
+    const uri = 'perfetto.ChromeScrollJank#eventLatency';
     ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#eventLatency',
+      uri,
       title: 'Chrome Scroll Input Latencies',
       tags: {
         kind: CHROME_EVENT_LATENCY_TRACK_KIND,
       },
-      trackFactory: ({trackKey}) => {
-        return new EventLatencyTrack({engine: ctx.engine, trackKey}, baseTable);
-      },
+      track: new EventLatencyTrack({engine: ctx.engine, uri}, baseTable),
     });
 
     ctx.registerDetailsPanel(
@@ -304,18 +301,17 @@
       `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`,
     );
 
+    const uri = 'perfetto.ChromeScrollJank#scrollJankV3';
     ctx.registerTrack({
-      uri: 'perfetto.ChromeScrollJank#scrollJankV3',
+      uri,
       title: 'Chrome Scroll Janks',
       tags: {
         kind: SCROLL_JANK_V3_TRACK_KIND,
       },
-      trackFactory: ({trackKey}) => {
-        return new ScrollJankV3Track({
-          engine: ctx.engine,
-          trackKey,
-        });
-      },
+      track: new ScrollJankV3Track({
+        engine: ctx.engine,
+        uri,
+      }),
     });
 
     ctx.registerDetailsPanel(
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
index 6b57e46..963be02 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
@@ -179,19 +179,20 @@
 
 export function getCauseLink(
   threadTracks: EventLatencyCauseThreadTracks,
+  tracksByTrackId: Map<number, string>,
   ts: time | undefined,
   dur: duration | undefined,
 ): m.Child {
-  const trackKeys: string[] = [];
+  const trackUris: string[] = [];
   for (const trackId of threadTracks.trackIds) {
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    if (trackKey === undefined) {
+    const track = tracksByTrackId.get(trackId);
+    if (track === undefined) {
       return `Could not locate track ${trackId} for thread ${threadTracks.thread} in the global state`;
     }
-    trackKeys.push(trackKey);
+    trackUris.push(track);
   }
 
-  if (trackKeys.length == 0) {
+  if (trackUris.length == 0) {
     return `No valid tracks for thread ${threadTracks.thread}.`;
   }
 
@@ -204,16 +205,15 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          verticalScrollToTrack(trackKeys[0], true);
+          verticalScrollToTrack(trackUris[0], true);
           if (exists(ts) && exists(dur)) {
             focusHorizontalRange(ts, Time.fromRaw(ts + dur), 0.3);
-            globals.timeline.selectArea(ts, Time.fromRaw(ts + dur), trackKeys);
 
             globals.dispatch(
               Actions.selectArea({
                 start: ts,
                 end: Time.fromRaw(ts + dur),
-                tracks: trackKeys,
+                trackUris,
               }),
             );
           }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
index 34dc713..be7c837 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
@@ -181,18 +181,20 @@
             throw new Error(`${vnode.attrs.kind} track is not registered.`);
           }
 
+          const trackUri = track.key;
+
           globals.makeSelection(
             Actions.selectGenericSlice({
               id: vnode.attrs.id,
               sqlTableName: track.sqlTableName,
               start: vnode.attrs.ts,
               duration: vnode.attrs.dur,
-              trackKey: track.key,
+              trackUri,
               detailsPanelConfig: track.detailsPanelConfig,
             }),
           );
 
-          scrollToTrackAndTs(track.key, vnode.attrs.ts, true);
+          scrollToTrackAndTs(trackUri, vnode.attrs.ts, true);
         },
       },
       vnode.attrs.name,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index 65c5ade..64259c1 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -36,7 +36,7 @@
     super(args);
     ScrollJankPluginState.getInstance().registerTrack({
       kind: SCROLL_JANK_V3_TRACK_KIND,
-      trackKey: this.trackKey,
+      trackUri: this.uri,
       tableName: this.tableName,
       detailsPanelConfig: this.getDetailsPanel(),
     });
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 5819968..eb857ee 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -48,7 +48,7 @@
 
     ScrollJankPluginState.getInstance().registerTrack({
       kind: TopLevelScrollTrack.kind,
-      trackKey: this.trackKey,
+      trackUri: this.uri,
       tableName: this.tableName,
       detailsPanelConfig: this.getDetailsPanel(),
     });
diff --git a/ui/src/core_plugins/chrome_tasks/index.ts b/ui/src/core_plugins/chrome_tasks/index.ts
index 36767f5..81713ed 100644
--- a/ui/src/core_plugins/chrome_tasks/index.ts
+++ b/ui/src/core_plugins/chrome_tasks/index.ts
@@ -102,11 +102,10 @@
     for (; it.valid(); it.next()) {
       const utid = it.utid;
       const uri = `org.chromium.ChromeTasks#thread.${utid}`;
-      ctx.registerStaticTrack({
+      ctx.registerTrackAndShowOnTraceLoad({
         uri,
-        trackFactory: ({trackKey}) =>
-          new ChromeTasksThreadTrack(ctx.engine, trackKey, asUtid(utid)),
-        groupName: `Chrome Tasks`,
+        track: new ChromeTasksThreadTrack(ctx.engine, uri, asUtid(utid)),
+        tags: {groupName: `Chrome Tasks`},
         title: `${it.threadName} ${it.tid}`,
       });
     }
diff --git a/ui/src/core_plugins/chrome_tasks/track.ts b/ui/src/core_plugins/chrome_tasks/track.ts
index 6e25b03..811e6b8 100644
--- a/ui/src/core_plugins/chrome_tasks/track.ts
+++ b/ui/src/core_plugins/chrome_tasks/track.ts
@@ -25,10 +25,10 @@
 export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack {
   constructor(
     engine: Engine,
-    trackKey: string,
+    uri: string,
     private utid: Utid,
   ) {
-    super({engine, trackKey});
+    super({engine, uri});
   }
 
   getSqlDataSource(): CustomSqlTableDefConfig {
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index cc3e6a0..0430f63 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -33,6 +33,7 @@
   addSqlTableTabImpl,
   SqlTableTabConfig,
 } from '../../frontend/sql_table_tab';
+import {Workspace} from '../../public/workspace';
 
 const SQL_STATS = `
 with first as (select started as ts from sqlstats limit 1)
@@ -230,9 +231,8 @@
       id: 'perfetto.CoreCommands#UnpinAllTracks',
       name: 'Unpin all pinned tracks',
       callback: () => {
-        ctx.timeline.unpinTracksByPredicate((_) => {
-          return true;
-        });
+        const workspace = ctx.timeline.workspace;
+        workspace.pinnedTracks.forEach((t) => workspace.unpinTrack(t));
       },
     });
 
@@ -240,9 +240,7 @@
       id: 'perfetto.CoreCommands#ExpandAllGroups',
       name: 'Expand all track groups',
       callback: () => {
-        ctx.timeline.expandGroupsByPredicate((_) => {
-          return true;
-        });
+        ctx.timeline.workspace.flatGroups.forEach((g) => g.expand());
       },
     });
 
@@ -250,9 +248,7 @@
       id: 'perfetto.CoreCommands#CollapseAllGroups',
       name: 'Collapse all track groups',
       callback: () => {
-        ctx.timeline.collapseGroupsByPredicate((_) => {
-          return true;
-        });
+        ctx.timeline.workspace.flatGroups.forEach((g) => g.collapse());
       },
     });
 
@@ -298,6 +294,43 @@
         addSqlTableTabImpl(args as SqlTableTabConfig);
       },
     });
+
+    ctx.registerCommand({
+      id: 'createNewEmptyWorkspace',
+      name: 'Create new empty workspace',
+      callback: async () => {
+        try {
+          const name = await ctx.prompt('Give it a name...');
+          const newWorkspace = new Workspace(name);
+          globals.workspaces.push(newWorkspace);
+          globals.switchWorkspace(newWorkspace);
+        } finally {
+        }
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'switchWorkspace',
+      name: 'Switch workspace',
+      callback: async () => {
+        try {
+          const options = globals.workspaces.map((ws) => {
+            return {key: ws.uuid, displayName: ws.displayName};
+          });
+          const workspaceUuid = await ctx.prompt(
+            'Choose a workspace...',
+            options,
+          );
+          const workspace = globals.workspaces.find(
+            (ws) => ws.uuid === workspaceUuid,
+          );
+          if (workspace) {
+            globals.switchWorkspace(workspace);
+          }
+        } finally {
+        }
+      },
+    });
   }
 
   onDeactivate(_: PluginContext): void {
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 3f9b612..3b08e7e 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -20,7 +20,6 @@
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
-  PrimaryTrackSortKey,
   STR,
   LONG,
   Engine,
@@ -176,25 +175,23 @@
       const trackId = it.id;
       const displayName = it.name;
       const unit = it.unit ?? undefined;
-      ctx.registerStaticTrack({
-        uri: `/counter_${trackId}`,
+      const uri = `/counter_${trackId}`;
+      ctx.registerTrackAndShowOnTraceLoad({
+        uri,
         title: displayName,
         tags: {
           kind: COUNTER_TRACK_KIND,
           trackIds: [trackId],
         },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId,
-            options: {
-              ...getDefaultCounterOptions(displayName),
-              unit,
-            },
-          });
-        },
-        sortKey: PrimaryTrackSortKey.COUNTER_TRACK,
+        track: new TraceProcessorCounterTrack({
+          engine: ctx.engine,
+          uri,
+          trackId,
+          options: {
+            ...getDefaultCounterOptions(displayName),
+            unit,
+          },
+        }),
         detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, displayName),
         getEventBounds: async (id) => {
           return await getCounterEventBounds(ctx.engine, trackId, id);
@@ -246,22 +243,21 @@
     for (; it.valid(); it.next()) {
       const name = it.name;
       const trackId = it.id;
+      const uri = `counter.cpu.${trackId}`;
       ctx.registerTrack({
-        uri: `counter.cpu.${trackId}`,
+        uri,
         title: name,
         tags: {
           kind: COUNTER_TRACK_KIND,
           trackIds: [trackId],
           scope,
         },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
+        track: new TraceProcessorCounterTrack({
+          engine: ctx.engine,
+          uri,
+          trackId: trackId,
+          options: getDefaultCounterOptions(name),
+        }),
         detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
         getEventBounds: async (id) => {
           return await getCounterEventBounds(ctx.engine, trackId, id);
@@ -313,8 +309,9 @@
         threadName,
         threadTrack: true,
       });
+      const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
       ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`,
+        uri,
         title: name,
         tags: {
           kind,
@@ -323,14 +320,12 @@
           upid: upid ?? undefined,
           scope: 'thread',
         },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
+        track: new TraceProcessorCounterTrack({
+          engine: ctx.engine,
+          uri,
+          trackId: trackId,
+          options: getDefaultCounterOptions(name),
+        }),
         detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
         getEventBounds: async (id) => {
           return await getCounterEventBounds(ctx.engine, trackId, id);
@@ -373,8 +368,9 @@
         processName,
         ...(exists(trackName) && {trackName}),
       });
+      const uri = `/process_${upid}/counter_${trackId}`;
       ctx.registerTrack({
-        uri: `/process_${upid}/counter_${trackId}`,
+        uri,
         title: name,
         tags: {
           kind,
@@ -382,14 +378,12 @@
           upid,
           scope: 'process',
         },
-        trackFactory: (trackCtx) => {
-          return new TraceProcessorCounterTrack({
-            engine: ctx.engine,
-            trackKey: trackCtx.trackKey,
-            trackId: trackId,
-            options: getDefaultCounterOptions(name),
-          });
-        },
+        track: new TraceProcessorCounterTrack({
+          engine: ctx.engine,
+          uri,
+          trackId: trackId,
+          options: getDefaultCounterOptions(name),
+        }),
         detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
         getEventBounds: async (id) => {
           return await getCounterEventBounds(ctx.engine, trackId, id);
@@ -424,14 +418,12 @@
             trackIds: [trackId],
             scope: 'gpuFreq',
           },
-          trackFactory: (trackCtx) => {
-            return new TraceProcessorCounterTrack({
-              engine: ctx.engine,
-              trackKey: trackCtx.trackKey,
-              trackId: trackId,
-              options: getDefaultCounterOptions(name),
-            });
-          },
+          track: new TraceProcessorCounterTrack({
+            engine: ctx.engine,
+            uri,
+            trackId: trackId,
+            options: getDefaultCounterOptions(name),
+          }),
           detailsPanel: new CounterDetailsPanel(ctx.engine, trackId, name),
           getEventBounds: async (id) => {
             return await getCounterEventBounds(ctx.engine, trackId, id);
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
index b02278b..407a9d7 100644
--- a/ui/src/core_plugins/counter/trace_processor_counter_track.ts
+++ b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
@@ -79,7 +79,7 @@
         return;
       }
       const id = it.id;
-      globals.selectSingleEvent(this.trackKey, id);
+      globals.selectSingleEvent(this.uri, id);
     });
 
     return true;
diff --git a/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts b/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts
new file mode 100644
index 0000000..49e5b20
--- /dev/null
+++ b/ui/src/core_plugins/cpu_freq/cpu_freq_track.ts
@@ -0,0 +1,428 @@
+// Copyright (C) 2021 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {searchSegment} from '../../base/binary_search';
+import {assertTrue} from '../../base/logging';
+import {duration, time, Time} from '../../base/time';
+import {drawTrackHoverTooltip} from '../../common/canvas_utils';
+import {colorForCpu} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
+import {Vector} from '../../base/geom';
+import {createView, createVirtualTable} from '../../trace_processor/sql_utils';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+
+export interface Data extends TrackData {
+  timestamps: BigInt64Array;
+  minFreqKHz: Uint32Array;
+  maxFreqKHz: Uint32Array;
+  lastFreqKHz: Uint32Array;
+  lastIdleValues: Int8Array;
+}
+
+interface Config {
+  cpu: number;
+  freqTrackId: number;
+  idleTrackId?: number;
+  maximumValue: number;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 20;
+
+export class CpuFreqTrack implements Track {
+  private mousePos: Vector = {x: 0, y: 0};
+  private hoveredValue: number | undefined = undefined;
+  private hoveredTs: time | undefined = undefined;
+  private hoveredTsEnd: time | undefined = undefined;
+  private hoveredIdle: number | undefined = undefined;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private engine: Engine;
+  private config: Config;
+  private trackUuid = uuidv4Sql();
+
+  private trash!: AsyncDisposableStack;
+
+  constructor(config: Config, engine: Engine) {
+    this.config = config;
+    this.engine = engine;
+  }
+
+  async onCreate() {
+    this.trash = new AsyncDisposableStack();
+    if (this.config.idleTrackId === undefined) {
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_freq_idle_${this.trackUuid}`,
+          `
+            select ts, dur, value as freqValue, -1 as idleValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.freqTrackId}
+          `,
+        ),
+      );
+    } else {
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_freq_${this.trackUuid}`,
+          `
+            select ts, dur, value as freqValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.freqTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createView(
+          this.engine,
+          `raw_idle_${this.trackUuid}`,
+          `
+            select
+              ts,
+              dur,
+              iif(value = 4294967295, -1, cast(value as int)) as idleValue
+            from experimental_counter_dur c
+            where track_id = ${this.config.idleTrackId}
+          `,
+        ),
+      );
+
+      this.trash.use(
+        await createVirtualTable(
+          this.engine,
+          `raw_freq_idle_${this.trackUuid}`,
+          `span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid})`,
+        ),
+      );
+    }
+
+    this.trash.use(
+      await createVirtualTable(
+        this.engine,
+        `cpu_freq_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, freqValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+
+    this.trash.use(
+      await createVirtualTable(
+        this.engine,
+        `cpu_idle_${this.trackUuid}`,
+        `
+          __intrinsic_counter_mipmap((
+            select ts, idleValue as value
+            from raw_freq_idle_${this.trackUuid}
+          ))
+        `,
+      ),
+    );
+  }
+
+  async onUpdate({
+    visibleWindow,
+    resolution,
+  }: TrackRenderContext): Promise<void> {
+    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  }
+
+  async onDestroy(): Promise<void> {
+    await this.trash.asyncDispose();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    // The resolution should always be a power of two for the logic of this
+    // function to make sense.
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const freqResult = await this.engine.query(`
+      SELECT
+        min_value as minFreq,
+        max_value as maxFreq,
+        last_ts as ts,
+        last_value as lastFreq
+      FROM cpu_freq_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+    const idleResult = await this.engine.query(`
+      SELECT last_value as lastIdle
+      FROM cpu_idle_${this.trackUuid}(
+        ${start},
+        ${end},
+        ${resolution}
+      );
+    `);
+
+    const freqRows = freqResult.numRows();
+    const idleRows = idleResult.numRows();
+    assertTrue(freqRows == idleRows);
+
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: freqRows,
+      timestamps: new BigInt64Array(freqRows),
+      minFreqKHz: new Uint32Array(freqRows),
+      maxFreqKHz: new Uint32Array(freqRows),
+      lastFreqKHz: new Uint32Array(freqRows),
+      lastIdleValues: new Int8Array(freqRows),
+    };
+
+    const freqIt = freqResult.iter({
+      ts: LONG,
+      minFreq: NUM,
+      maxFreq: NUM,
+      lastFreq: NUM,
+    });
+    const idleIt = idleResult.iter({
+      lastIdle: NUM,
+    });
+    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
+      data.timestamps[i] = freqIt.ts;
+      data.minFreqKHz[i] = freqIt.minFreq;
+      data.maxFreqKHz[i] = freqIt.maxFreq;
+      data.lastFreqKHz[i] = freqIt.lastFreq;
+      data.lastIdleValues[i] = idleIt.lastIdle;
+    }
+    return data;
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT;
+  }
+
+  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.fetcher.data;
+
+    if (data === undefined || data.timestamps.length === 0) {
+      // Can't possibly draw anything.
+      return;
+    }
+
+    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
+    assertTrue(data.timestamps.length === data.minFreqKHz.length);
+    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
+    assertTrue(data.timestamps.length === data.lastIdleValues.length);
+
+    const endPx = size.width;
+    const zeroY = MARGIN_TOP + RECT_HEIGHT;
+
+    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
+    let yMax = this.config.maximumValue;
+    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
+    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
+    const pow10 = Math.pow(10, exp);
+    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
+    const unitGroup = Math.floor(exp / 3);
+    const num = yMax / Math.pow(10, unitGroup * 3);
+    // The values we have for cpufreq are in kHz so +1 to unitGroup.
+    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
+
+    const color = colorForCpu(this.config.cpu);
+    let saturation = 45;
+    if (globals.state.hoveredUtid !== -1) {
+      saturation = 0;
+    }
+
+    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
+    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
+
+    const calculateX = (timestamp: time) => {
+      return Math.floor(timescale.timeToPx(timestamp));
+    };
+    const calculateY = (value: number) => {
+      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
+    };
+
+    const timespan = visibleWindow.toTimeSpan();
+    const start = timespan.start;
+    const end = timespan.end;
+
+    const [rawStartIdx] = searchSegment(data.timestamps, start);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.timestamps, end);
+    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
+
+    // Draw the CPU frequency graph.
+    {
+      ctx.beginPath();
+      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
+      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
+
+      let lastDrawnY = zeroY;
+      for (let i = startIdx; i < endIdx; i++) {
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = Math.max(0, calculateX(timestamp));
+        const minY = calculateY(data.minFreqKHz[i]);
+        const maxY = calculateY(data.maxFreqKHz[i]);
+        const lastY = calculateY(data.lastFreqKHz[i]);
+
+        ctx.lineTo(x, lastDrawnY);
+        if (minY === maxY) {
+          assertTrue(lastY === minY);
+          ctx.lineTo(x, lastY);
+        } else {
+          ctx.lineTo(x, minY);
+          ctx.lineTo(x, maxY);
+          ctx.lineTo(x, lastY);
+        }
+        lastDrawnY = lastY;
+      }
+      ctx.lineTo(endPx, lastDrawnY);
+      ctx.lineTo(endPx, zeroY);
+      ctx.closePath();
+      ctx.fill();
+      ctx.stroke();
+    }
+
+    // Draw CPU idle rectangles that overlay the CPU freq graph.
+    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
+    {
+      for (let i = startIdx; i < endIdx; i++) {
+        if (data.lastIdleValues[i] < 0) {
+          continue;
+        }
+
+        // We intentionally don't use the floor function here when computing x
+        // coordinates. Instead we use floating point which prevents flickering as
+        // we pan and zoom; this relies on the browser anti-aliasing pixels
+        // correctly.
+        const timestamp = Time.fromRaw(data.timestamps[i]);
+        const x = timescale.timeToPx(timestamp);
+        const xEnd =
+          i === data.lastIdleValues.length - 1
+            ? endPx
+            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
+
+        const width = xEnd - x;
+        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
+
+        ctx.fillRect(x, zeroY, width, height);
+      }
+    }
+
+    ctx.font = '10px Roboto Condensed';
+
+    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
+      let text = `${this.hoveredValue.toLocaleString()}kHz`;
+
+      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
+      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
+
+      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
+      const xEnd =
+        this.hoveredTsEnd === undefined
+          ? endPx
+          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
+      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
+
+      // Highlight line.
+      ctx.beginPath();
+      ctx.moveTo(xStart, y);
+      ctx.lineTo(xEnd, y);
+      ctx.lineWidth = 3;
+      ctx.stroke();
+      ctx.lineWidth = 1;
+
+      // Draw change marker.
+      ctx.beginPath();
+      ctx.arc(
+        xStart,
+        y,
+        3 /* r*/,
+        0 /* start angle*/,
+        2 * Math.PI /* end angle*/,
+      );
+      ctx.fill();
+      ctx.stroke();
+
+      // Display idle value if current hover is idle.
+      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
+        // Display the idle value +1 to be consistent with catapult.
+        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
+      }
+
+      // Draw the tooltip.
+      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
+    }
+
+    // Write the Y scale on the top left corner.
+    ctx.textBaseline = 'alphabetic';
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+    ctx.fillRect(0, 0, 42, 18);
+    ctx.fillStyle = '#666';
+    ctx.textAlign = 'left';
+    ctx.fillText(`${yLabel}`, 4, 14);
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      timescale.timeToPx(data.start),
+      timescale.timeToPx(data.end),
+    );
+  }
+
+  onMouseMove({x, y, timescale}: TrackMouseEvent) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    this.mousePos = {x, y};
+    const time = timescale.pxToHpTime(x);
+
+    const [left, right] = searchSegment(data.timestamps, time.toTime());
+
+    this.hoveredTs =
+      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
+    this.hoveredTsEnd =
+      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
+    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
+    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
+  }
+
+  onMouseOut() {
+    this.hoveredValue = undefined;
+    this.hoveredTs = undefined;
+    this.hoveredTsEnd = undefined;
+    this.hoveredIdle = undefined;
+  }
+}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index 0945223..0dd4218 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -12,396 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {searchSegment} from '../../base/binary_search';
-import {assertTrue} from '../../base/logging';
-import {duration, time, Time} from '../../base/time';
-import {drawTrackHoverTooltip} from '../../common/canvas_utils';
-import {colorForCpu} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
 import {
   CPU_FREQ_TRACK_KIND,
-  Engine,
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
-  Track,
 } from '../../public';
-import {LONG, NUM, NUM_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
-import {Vector} from '../../base/geom';
-
-export interface Data extends TrackData {
-  timestamps: BigInt64Array;
-  minFreqKHz: Uint32Array;
-  maxFreqKHz: Uint32Array;
-  lastFreqKHz: Uint32Array;
-  lastIdleValues: Int8Array;
-}
-
-interface Config {
-  cpu: number;
-  freqTrackId: number;
-  idleTrackId?: number;
-  maximumValue: number;
-}
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 20;
-
-class CpuFreqTrack implements Track {
-  private mousePos: Vector = {x: 0, y: 0};
-  private hoveredValue: number | undefined = undefined;
-  private hoveredTs: time | undefined = undefined;
-  private hoveredTsEnd: time | undefined = undefined;
-  private hoveredIdle: number | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private engine: Engine;
-  private config: Config;
-  private trackUuid = uuidv4Sql();
-
-  constructor(config: Config, engine: Engine) {
-    this.config = config;
-    this.engine = engine;
-  }
-
-  async onCreate() {
-    if (this.config.idleTrackId === undefined) {
-      await this.engine.query(`
-        create view raw_freq_idle_${this.trackUuid} as
-        select ts, dur, value as freqValue, -1 as idleValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.freqTrackId}
-      `);
-    } else {
-      await this.engine.query(`
-        create view raw_freq_${this.trackUuid} as
-        select ts, dur, value as freqValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.freqTrackId};
-
-        create view raw_idle_${this.trackUuid} as
-        select
-          ts,
-          dur,
-          iif(value = 4294967295, -1, cast(value as int)) as idleValue
-        from experimental_counter_dur c
-        where track_id = ${this.config.idleTrackId};
-
-        create virtual table raw_freq_idle_${this.trackUuid}
-        using span_join(raw_freq_${this.trackUuid}, raw_idle_${this.trackUuid});
-      `);
-    }
-
-    await this.engine.query(`
-      create virtual table cpu_freq_${this.trackUuid}
-      using __intrinsic_counter_mipmap((
-        select ts, freqValue as value
-        from raw_freq_idle_${this.trackUuid}
-      ));
-
-      create virtual table cpu_idle_${this.trackUuid}
-      using __intrinsic_counter_mipmap((
-        select ts, idleValue as value
-        from raw_freq_idle_${this.trackUuid}
-      ));
-    `);
-  }
-
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
-  }
-
-  async onDestroy(): Promise<void> {
-    await this.engine.tryQuery(`drop table cpu_freq_${this.trackUuid}`);
-    await this.engine.tryQuery(`drop table cpu_idle_${this.trackUuid}`);
-    await this.engine.tryQuery(`drop table raw_freq_idle_${this.trackUuid}`);
-    await this.engine.tryQuery(
-      `drop view if exists raw_freq_${this.trackUuid}`,
-    );
-    await this.engine.tryQuery(
-      `drop view if exists raw_idle_${this.trackUuid}`,
-    );
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    // The resolution should always be a power of two for the logic of this
-    // function to make sense.
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const freqResult = await this.engine.query(`
-      SELECT
-        min_value as minFreq,
-        max_value as maxFreq,
-        last_ts as ts,
-        last_value as lastFreq
-      FROM cpu_freq_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-    const idleResult = await this.engine.query(`
-      SELECT last_value as lastIdle
-      FROM cpu_idle_${this.trackUuid}(
-        ${start},
-        ${end},
-        ${resolution}
-      );
-    `);
-
-    const freqRows = freqResult.numRows();
-    const idleRows = idleResult.numRows();
-    assertTrue(freqRows == idleRows);
-
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: freqRows,
-      timestamps: new BigInt64Array(freqRows),
-      minFreqKHz: new Uint32Array(freqRows),
-      maxFreqKHz: new Uint32Array(freqRows),
-      lastFreqKHz: new Uint32Array(freqRows),
-      lastIdleValues: new Int8Array(freqRows),
-    };
-
-    const freqIt = freqResult.iter({
-      ts: LONG,
-      minFreq: NUM,
-      maxFreq: NUM,
-      lastFreq: NUM,
-    });
-    const idleIt = idleResult.iter({
-      lastIdle: NUM,
-    });
-    for (let i = 0; freqIt.valid(); ++i, freqIt.next(), idleIt.next()) {
-      data.timestamps[i] = freqIt.ts;
-      data.minFreqKHz[i] = freqIt.minFreq;
-      data.maxFreqKHz[i] = freqIt.maxFreq;
-      data.lastFreqKHz[i] = freqIt.lastFreq;
-      data.lastIdleValues[i] = idleIt.lastIdle;
-    }
-    return data;
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT;
-  }
-
-  render({ctx, size, timescale, visibleWindow}: TrackRenderContext): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.fetcher.data;
-
-    if (data === undefined || data.timestamps.length === 0) {
-      // Can't possibly draw anything.
-      return;
-    }
-
-    assertTrue(data.timestamps.length === data.lastFreqKHz.length);
-    assertTrue(data.timestamps.length === data.minFreqKHz.length);
-    assertTrue(data.timestamps.length === data.maxFreqKHz.length);
-    assertTrue(data.timestamps.length === data.lastIdleValues.length);
-
-    const endPx = size.width;
-    const zeroY = MARGIN_TOP + RECT_HEIGHT;
-
-    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
-    let yMax = this.config.maximumValue;
-    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
-    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
-    const pow10 = Math.pow(10, exp);
-    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
-    const unitGroup = Math.floor(exp / 3);
-    const num = yMax / Math.pow(10, unitGroup * 3);
-    // The values we have for cpufreq are in kHz so +1 to unitGroup.
-    const yLabel = `${num} ${kUnits[unitGroup + 1]}Hz`;
-
-    const color = colorForCpu(this.config.cpu);
-    let saturation = 45;
-    if (globals.state.hoveredUtid !== -1) {
-      saturation = 0;
-    }
-
-    ctx.fillStyle = color.setHSL({s: saturation, l: 70}).cssString;
-    ctx.strokeStyle = color.setHSL({s: saturation, l: 55}).cssString;
-
-    const calculateX = (timestamp: time) => {
-      return Math.floor(timescale.timeToPx(timestamp));
-    };
-    const calculateY = (value: number) => {
-      return zeroY - Math.round((value / yMax) * RECT_HEIGHT);
-    };
-
-    const timespan = visibleWindow.toTimeSpan();
-    const start = timespan.start;
-    const end = timespan.end;
-
-    const [rawStartIdx] = searchSegment(data.timestamps, start);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.timestamps, end);
-    const endIdx = rawEndIdx === -1 ? data.timestamps.length : rawEndIdx;
-
-    // Draw the CPU frequency graph.
-    {
-      ctx.beginPath();
-      const timestamp = Time.fromRaw(data.timestamps[startIdx]);
-      ctx.moveTo(Math.max(calculateX(timestamp), 0), zeroY);
-
-      let lastDrawnY = zeroY;
-      for (let i = startIdx; i < endIdx; i++) {
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = Math.max(0, calculateX(timestamp));
-        const minY = calculateY(data.minFreqKHz[i]);
-        const maxY = calculateY(data.maxFreqKHz[i]);
-        const lastY = calculateY(data.lastFreqKHz[i]);
-
-        ctx.lineTo(x, lastDrawnY);
-        if (minY === maxY) {
-          assertTrue(lastY === minY);
-          ctx.lineTo(x, lastY);
-        } else {
-          ctx.lineTo(x, minY);
-          ctx.lineTo(x, maxY);
-          ctx.lineTo(x, lastY);
-        }
-        lastDrawnY = lastY;
-      }
-      ctx.lineTo(endPx, lastDrawnY);
-      ctx.lineTo(endPx, zeroY);
-      ctx.closePath();
-      ctx.fill();
-      ctx.stroke();
-    }
-
-    // Draw CPU idle rectangles that overlay the CPU freq graph.
-    ctx.fillStyle = `rgba(240, 240, 240, 1)`;
-    {
-      for (let i = startIdx; i < endIdx; i++) {
-        if (data.lastIdleValues[i] < 0) {
-          continue;
-        }
-
-        // We intentionally don't use the floor function here when computing x
-        // coordinates. Instead we use floating point which prevents flickering as
-        // we pan and zoom; this relies on the browser anti-aliasing pixels
-        // correctly.
-        const timestamp = Time.fromRaw(data.timestamps[i]);
-        const x = timescale.timeToPx(timestamp);
-        const xEnd =
-          i === data.lastIdleValues.length - 1
-            ? endPx
-            : timescale.timeToPx(Time.fromRaw(data.timestamps[i + 1]));
-
-        const width = xEnd - x;
-        const height = calculateY(data.lastFreqKHz[i]) - zeroY;
-
-        ctx.fillRect(x, zeroY, width, height);
-      }
-    }
-
-    ctx.font = '10px Roboto Condensed';
-
-    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
-      let text = `${this.hoveredValue.toLocaleString()}kHz`;
-
-      ctx.fillStyle = color.setHSL({s: 45, l: 75}).cssString;
-      ctx.strokeStyle = color.setHSL({s: 45, l: 45}).cssString;
-
-      const xStart = Math.floor(timescale.timeToPx(this.hoveredTs));
-      const xEnd =
-        this.hoveredTsEnd === undefined
-          ? endPx
-          : Math.floor(timescale.timeToPx(this.hoveredTsEnd));
-      const y = zeroY - Math.round((this.hoveredValue / yMax) * RECT_HEIGHT);
-
-      // Highlight line.
-      ctx.beginPath();
-      ctx.moveTo(xStart, y);
-      ctx.lineTo(xEnd, y);
-      ctx.lineWidth = 3;
-      ctx.stroke();
-      ctx.lineWidth = 1;
-
-      // Draw change marker.
-      ctx.beginPath();
-      ctx.arc(
-        xStart,
-        y,
-        3 /* r*/,
-        0 /* start angle*/,
-        2 * Math.PI /* end angle*/,
-      );
-      ctx.fill();
-      ctx.stroke();
-
-      // Display idle value if current hover is idle.
-      if (this.hoveredIdle !== undefined && this.hoveredIdle !== -1) {
-        // Display the idle value +1 to be consistent with catapult.
-        text += ` (Idle: ${(this.hoveredIdle + 1).toLocaleString()})`;
-      }
-
-      // Draw the tooltip.
-      drawTrackHoverTooltip(ctx, this.mousePos, size, text);
-    }
-
-    // Write the Y scale on the top left corner.
-    ctx.textBaseline = 'alphabetic';
-    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
-    ctx.fillRect(0, 0, 42, 18);
-    ctx.fillStyle = '#666';
-    ctx.textAlign = 'left';
-    ctx.fillText(`${yLabel}`, 4, 14);
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      timescale.timeToPx(data.start),
-      timescale.timeToPx(data.end),
-    );
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    this.mousePos = {x, y};
-    const time = timescale.pxToHpTime(x);
-
-    const [left, right] = searchSegment(data.timestamps, time.toTime());
-
-    this.hoveredTs =
-      left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
-    this.hoveredTsEnd =
-      right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
-    this.hoveredValue = left === -1 ? undefined : data.lastFreqKHz[left];
-    this.hoveredIdle = left === -1 ? undefined : data.lastIdleValues[left];
-  }
-
-  onMouseOut() {
-    this.hoveredValue = undefined;
-    this.hoveredTs = undefined;
-    this.hoveredTsEnd = undefined;
-    this.hoveredIdle = undefined;
-  }
-}
+import {NUM, NUM_NULL} from '../../trace_processor/query_result';
+import {CpuFreqTrack} from './cpu_freq_track';
 
 class CpuFreq implements PerfettoPlugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -451,14 +69,15 @@
           idleTrackId,
         };
 
+        const uri = `/cpu_freq_cpu${cpu}`;
         ctx.registerTrack({
-          uri: `/cpu_freq_cpu${cpu}`,
+          uri,
           title: `Cpu ${cpu} Frequency`,
           tags: {
             kind: CPU_FREQ_TRACK_KIND,
             cpu,
           },
-          trackFactory: () => new CpuFreqTrack(config, ctx.engine),
+          track: new CpuFreqTrack(config, ctx.engine),
         });
       }
     }
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
index 5c283db..2e70b2f 100644
--- a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
+++ b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
@@ -12,237 +12,75 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {getLegacySelection} from '../../common/state';
+import {assertExists} from '../../base/logging';
+import {Time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {colorForSample} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
+import {LegacySelection} from '../../common/state';
+import {getColorForSample} from '../../core/colorizer';
+import {
+  BaseSliceTrack,
+  OnSliceClickArgs,
+} from '../../frontend/base_slice_track';
 import {globals} from '../../frontend/globals';
-import {TimeScale} from '../../frontend/time_scale';
-import {Engine, Track} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {TrackMouseEvent, TrackRenderContext} from '../../public/tracks';
+import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
+import {NewTrackArgs} from '../../frontend/track';
+import {NUM, Slice} from '../../public';
 
-const BAR_HEIGHT = 3;
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
-
-interface Data extends TrackData {
-  ids: Float64Array;
-  tsStarts: BigInt64Array;
-  callsiteId: Uint32Array;
+interface CpuProfileRow extends NamedRow {
+  callsiteId: number;
 }
 
-export class CpuProfileTrack implements Track {
-  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
-  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private utid: number;
-
-  constructor(engine: Engine, utid: number) {
-    this.engine = engine;
-    this.utid = utid;
+export class CpuProfileTrack extends BaseSliceTrack<Slice, CpuProfileRow> {
+  constructor(
+    args: NewTrackArgs,
+    private utid: number,
+  ) {
+    super(args);
   }
 
-  async onUpdate({
-    visibleWindow,
-    resolution,
-  }: TrackRenderContext): Promise<void> {
-    await this.fetcher.requestData(visibleWindow.toTimeSpan(), resolution);
+  protected getRowSpec(): CpuProfileRow {
+    return {...NAMED_ROW, callsiteId: NUM};
   }
 
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const query = `select
-        id,
+  protected rowToSlice(row: CpuProfileRow): Slice {
+    const baseSlice = super.rowToSliceBase(row);
+    const name = assertExists(row.name);
+    const colorScheme = getColorForSample(row.callsiteId);
+    return {...baseSlice, title: name, colorScheme};
+  }
+
+  isSelectionHandled(selection: LegacySelection): boolean {
+    return selection.kind === 'CPU_PROFILE_SAMPLE';
+  }
+
+  onUpdatedSlices(slices: Slice[]) {
+    for (const slice of slices) {
+      slice.isHighlighted = slice === this.hoveredSlice;
+    }
+  }
+
+  getSqlSource(): string {
+    return `
+      select
+        p.id,
         ts,
+        0 as dur,
+        0 as depth,
+        'CPU Sample' as name,
         callsite_id as callsiteId
-      from cpu_profile_stack_sample
+      from cpu_profile_stack_sample p
       where utid = ${this.utid}
-      order by ts`;
-
-    const result = await this.engine.query(query);
-    const numRows = result.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      ids: new Float64Array(numRows),
-      tsStarts: new BigInt64Array(numRows),
-      callsiteId: new Uint32Array(numRows),
-    };
-
-    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
-    for (let row = 0; it.valid(); it.next(), ++row) {
-      data.ids[row] = it.id;
-      data.tsStarts[row] = it.ts;
-      data.callsiteId[row] = it.callsiteId;
-    }
-
-    return data;
+      order by ts
+    `;
   }
 
-  async onDestroy(): Promise<void> {
-    this.fetcher[Symbol.dispose]();
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render({ctx, timescale: timeScale}: TrackRenderContext): void {
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'CPU_PROFILE_SAMPLE' &&
-        selection.ts === centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        timeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-        data.callsiteId[i],
-      );
-    }
-
-    // Group together identical identical CPU profile samples by connecting them
-    // with an horizontal bar.
-    let clusterStartIndex = 0;
-    while (clusterStartIndex < data.tsStarts.length) {
-      const callsiteId = data.callsiteId[clusterStartIndex];
-
-      // Find the end of the cluster by searching for the next different CPU
-      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
-      // inclusive and within array bounds.
-      let clusterEndIndex = clusterStartIndex;
-      while (
-        clusterEndIndex + 1 < data.tsStarts.length &&
-        data.callsiteId[clusterEndIndex + 1] === callsiteId
-      ) {
-        clusterEndIndex++;
-      }
-
-      // If there are multiple CPU samples in the cluster, draw a line.
-      if (clusterStartIndex !== clusterEndIndex) {
-        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
-        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
-        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
-        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
-        const width = rightPx - leftPx;
-        ctx.fillStyle = colorForSample(callsiteId, false);
-        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
-      }
-
-      // Move to the next cluster.
-      clusterStartIndex = clusterEndIndex + 1;
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-    callsiteId: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = colorForSample(callsiteId, isHovered);
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = colorForSample(callsiteId, false);
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const time = timescale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(left, timescale, data, x, y, right);
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y, timescale}: TrackMouseEvent) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-
-    const time = timescale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(left, timescale, data, x, y, right);
-
-    if (index !== -1) {
-      const id = data.ids[index];
-      const ts = Time.fromRaw(data.tsStarts[index]);
-
-      globals.makeSelection(
-        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+  onSliceClick({slice}: OnSliceClickArgs<Slice>) {
+    globals.makeSelection(
+      Actions.selectCpuProfileSample({
+        id: slice.id,
+        utid: this.utid,
+        ts: Time.fromRaw(slice.ts),
+      }),
     );
   }
 }
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 95350a5..08c68ea 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -14,9 +14,10 @@
 
 import m from 'mithril';
 
-import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
 import {
   CPU_PROFILE_TRACK_KIND,
+  Engine,
+  LegacyDetailsPanel,
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -25,6 +26,16 @@
 import {CpuProfileTrack} from './cpu_profile_track';
 import {getThreadUriPrefix} from '../../public/utils';
 import {exists} from '../../base/utils';
+import {Monitor} from '../../base/monitor';
+import {
+  metricsFromTableOrSubquery,
+  QueryFlamegraph,
+  QueryFlamegraphAttrs,
+} from '../../core/query_flamegraph';
+import {Timestamp} from '../../frontend/widgets/timestamp';
+import {assertExists} from '../../base/logging';
+import {DetailsShell} from '../../widgets/details_shell';
+import {CpuProfileSampleSelection, LegacySelection} from '../../common/state';
 
 class CpuProfile implements PerfettoPlugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -40,7 +51,8 @@
         upid,
         thread.name as threadName
       from thread_cpu_sample
-      join thread using(utid)`);
+      join thread using(utid)
+    `);
 
     const it = result.iter({
       utid: NUM,
@@ -52,27 +64,98 @@
       const utid = it.utid;
       const upid = it.upid;
       const threadName = it.threadName;
+      const uri = `${getThreadUriPrefix(upid, utid)}_cpu_samples`;
       ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_cpu_samples`,
+        uri,
         title: `${threadName} (CPU Stack Samples)`,
         tags: {
           kind: CPU_PROFILE_TRACK_KIND,
           utid,
           ...(exists(upid) && {upid}),
         },
-        trackFactory: () => new CpuProfileTrack(ctx.engine, utid),
+        track: new CpuProfileTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          utid,
+        ),
       });
     }
+    ctx.registerDetailsPanel(
+      new CpuProfileSampleFlamegraphDetailsPanel(ctx.engine),
+    );
+  }
+}
 
-    ctx.registerDetailsPanel({
-      render: (sel) => {
-        if (sel.kind === 'CPU_PROFILE_SAMPLE') {
-          return m(CpuProfileDetailsPanel);
-        } else {
-          return undefined;
-        }
-      },
-    });
+class CpuProfileSampleFlamegraphDetailsPanel implements LegacyDetailsPanel {
+  private sel?: CpuProfileSampleSelection;
+  private selMonitor = new Monitor([() => this.sel?.ts, () => this.sel?.utid]);
+  private flamegraphAttrs?: QueryFlamegraphAttrs;
+
+  constructor(private engine: Engine) {}
+
+  render(sel: LegacySelection) {
+    if (sel.kind !== 'CPU_PROFILE_SAMPLE') {
+      this.sel = undefined;
+      return undefined;
+    }
+    const {ts, utid} = sel;
+    this.sel = sel;
+    if (this.selMonitor.ifStateChanged()) {
+      this.flamegraphAttrs = {
+        engine: this.engine,
+        metrics: [
+          ...metricsFromTableOrSubquery(
+            `
+              (
+                select
+                  id,
+                  parent_id as parentId,
+                  name,
+                  mapping_name,
+                  source_file,
+                  cast(line_number AS text) as line_number,
+                  self_count
+                from _callstacks_for_cpu_profile_stack_samples!((
+                  select p.callsite_id
+                  from cpu_profile_stack_sample p
+                  where p.ts = ${ts} and p.utid = ${utid}
+                ))
+              )
+            `,
+            [
+              {
+                name: 'CPU Profile Samples',
+                unit: '',
+                columnName: 'self_count',
+              },
+            ],
+            'include perfetto module callstacks.stack_profile',
+            [{name: 'mapping_name', displayName: 'Mapping'}],
+            [
+              {name: 'source_file', displayName: 'Source File'},
+              {name: 'line_number', displayName: 'Line Number'},
+            ],
+          ),
+        ],
+      };
+    }
+    return m(
+      '.flamegraph-profile',
+      m(
+        DetailsShell,
+        {
+          fillParent: true,
+          title: m('.title', 'CPU Profile Samples'),
+          description: [],
+          buttons: [
+            m('div.time', `Timestamp: `, m(Timestamp, {ts: this.sel.ts})),
+          ],
+        },
+        m(QueryFlamegraph, assertExists(this.flamegraphAttrs)),
+      ),
+    );
   }
 }
 
diff --git a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
index 7f1b619..c37cd9b 100644
--- a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
+++ b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
@@ -61,12 +61,12 @@
   private lastRowId = -1;
   private engine: Engine;
   private cpu: number;
-  private trackKey: string;
+  private uri: string;
   private trackUuid = uuidv4Sql();
 
-  constructor(engine: Engine, trackKey: string, cpu: number) {
+  constructor(engine: Engine, uri: string, cpu: number) {
     this.engine = engine;
-    this.trackKey = trackKey;
+    this.uri = uri;
     this.cpu = cpu;
   }
 
@@ -438,7 +438,7 @@
       {
         kind: 'SCHED_SLICE',
         id,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
       },
       {
         clearSearch: true,
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index a377639..8d13c9c 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -42,9 +42,7 @@
           kind: CPU_SLICE_TRACK_KIND,
           cpu,
         },
-        trackFactory: ({trackKey}) => {
-          return new CpuSliceTrack(ctx.engine, trackKey, cpu);
-        },
+        track: new CpuSliceTrack(ctx.engine, uri, cpu),
       });
     }
 
diff --git a/ui/src/core_plugins/critical_path/OWNERS b/ui/src/core_plugins/critical_path/OWNERS
new file mode 100644
index 0000000..cd23fd9
--- /dev/null
+++ b/ui/src/core_plugins/critical_path/OWNERS
@@ -0,0 +1,2 @@
+zezeozue@google.com
+lalitm@google.com
\ No newline at end of file
diff --git a/ui/src/core_plugins/critical_path/index.ts b/ui/src/core_plugins/critical_path/index.ts
new file mode 100644
index 0000000..f16dd65
--- /dev/null
+++ b/ui/src/core_plugins/critical_path/index.ts
@@ -0,0 +1,332 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  getThreadInfo,
+  ThreadInfo,
+} from '../../trace_processor/sql_utils/thread';
+
+import {
+  addDebugSliceTrack,
+  Engine,
+  PerfettoPlugin,
+  PluginContextTrace,
+  PluginDescriptor,
+  THREAD_STATE_TRACK_KIND,
+} from '../../public';
+import {
+  getTimeSpanOfSelectionOrVisibleWindow,
+  globals,
+} from '../../frontend/globals';
+import {asUtid, Utid} from '../../trace_processor/sql_utils/core_types';
+import {addQueryResultsTab} from '../../frontend/query_result_tab';
+import {showModal} from '../../widgets/modal';
+import {Optional} from '../../base/utils';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../../public/exposed_commands';
+
+const criticalPathSliceColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'name',
+};
+
+const criticalPathsliceColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'name',
+  'table_name',
+];
+
+const criticalPathsliceLiteColumns = {
+  ts: 'ts',
+  dur: 'dur',
+  name: 'thread_name',
+};
+
+const criticalPathsliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
+
+const sliceLiteColumnNames = [
+  'id',
+  'utid',
+  'ts',
+  'dur',
+  'thread_name',
+  'process_name',
+  'table_name',
+];
+
+const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
+
+const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
+
+function getFirstUtidOfSelectionOrVisibleWindow(): number {
+  const selection = globals.state.selection;
+  if (selection.kind === 'area') {
+    for (const trackUri of selection.trackUris) {
+      const trackDesc = globals.trackManager.getTrack(trackUri);
+      if (
+        trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
+        trackDesc?.tags?.utid !== undefined
+      ) {
+        return trackDesc.tags.utid;
+      }
+    }
+  }
+
+  return 0;
+}
+
+function showModalErrorAreaSelectionRequired() {
+  showModal({
+    title: 'Error: range selection required',
+    content:
+      'This command requires an area selection over a thread state track.',
+  });
+}
+
+function showModalErrorThreadStateRequired() {
+  showModal({
+    title: 'Error: thread state selection required',
+    content: 'This command requires a thread state slice to be selected.',
+  });
+}
+
+// If utid is undefined, returns the utid for the selected thread state track,
+// if any. If it's defined, looks up the info about that specific utid.
+async function getThreadInfoForUtidOrSelection(
+  engine: Engine,
+  utid?: Utid,
+): Promise<Optional<ThreadInfo>> {
+  if (utid === undefined) {
+    if (
+      globals.state.selection.kind !== 'legacy' ||
+      globals.state.selection.legacySelection.kind !== 'THREAD_STATE'
+    ) {
+      return undefined;
+    }
+    const trackUri = globals.state.selection.legacySelection.trackUri;
+    if (trackUri === undefined) return undefined;
+    const track = globals.trackManager.getTrack(trackUri);
+    utid = asUtid(track?.tags?.utid);
+    if (utid === undefined) return undefined;
+  }
+  return getThreadInfo(engine, utid);
+}
+
+class CriticalPath implements PerfettoPlugin {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    // The 3 commands below are used in two contextes:
+    // 1. By clicking a slice and using the command palette. In this case the
+    //    utid argument is undefined and we need to look at the selection.
+    // 2. Invoked via runCommand(...) by thread_state_tab.ts when the user
+    //    clicks on the buttons in the details panel. In this case the details
+    //    panel passes the utid explicitly.
+    ctx.registerCommand({
+      id: CRITICAL_PATH_LITE_CMD,
+      name: 'Critical path lite (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx.engine, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+          .then(() =>
+            addDebugSliceTrack(
+              ctx,
+              {
+                sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                    ${thdInfo.utid},
+                    trace_bounds.start_ts,
+                    trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                  trace_bounds
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+              `,
+                columns: sliceLiteColumnNames,
+              },
+              `${thdInfo.name}`,
+              sliceLiteColumns,
+              sliceLiteColumnNames,
+            ),
+          );
+      },
+    });
+
+    ctx.registerCommand({
+      id: CRITICAL_PATH_CMD,
+      name: 'Critical path (selected thread state slice)',
+      callback: async (utid?: Utid) => {
+        const thdInfo = await getThreadInfoForUtidOrSelection(ctx.engine, utid);
+        if (thdInfo === undefined) {
+          return showModalErrorThreadStateRequired();
+        }
+        ctx.engine
+          .query(
+            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+          )
+          .then(() =>
+            addDebugSliceTrack(
+              ctx,
+              {
+                sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                  FROM
+                    _thread_executing_span_critical_path_stack(
+                      ${thdInfo.utid},
+                      trace_bounds.start_ts,
+                      trace_bounds.end_ts - trace_bounds.start_ts) cr,
+                    trace_bounds WHERE name IS NOT NULL
+              `,
+                columns: sliceColumnNames,
+              },
+              `${thdInfo.name}`,
+              sliceColumns,
+              sliceColumnNames,
+            ),
+          );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPathLite_AreaSelection',
+      name: 'Critical path lite (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
+        );
+        await addDebugSliceTrack(
+          ctx,
+          {
+            sqlSource: `
+                SELECT
+                  cr.id,
+                  cr.utid,
+                  cr.ts,
+                  cr.dur,
+                  thread.name AS thread_name,
+                  process.name AS process_name,
+                  'thread_state' AS table_name
+                FROM
+                  _thread_executing_span_critical_path(
+                      ${trackUtid},
+                      ${window.start},
+                      ${window.end} - ${window.start}) cr
+                JOIN thread USING(utid)
+                JOIN process USING(upid)
+                `,
+            columns: criticalPathsliceLiteColumnNames,
+          },
+          (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          criticalPathsliceLiteColumns,
+          criticalPathsliceLiteColumnNames,
+        );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPath_AreaSelection',
+      name: 'Critical path  (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        await ctx.engine.query(
+          `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+        );
+        await addDebugSliceTrack(
+          ctx,
+          {
+            sqlSource: `
+                SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
+                FROM
+                _critical_path_stack(
+                  ${trackUtid},
+                  ${window.start},
+                  ${window.end} - ${window.start}, 1, 1, 1, 1) cr
+                WHERE name IS NOT NULL
+                `,
+            columns: criticalPathsliceColumnNames,
+          },
+          (await getThreadInfo(ctx.engine, trackUtid as Utid)).name ??
+            '<thread name>',
+          criticalPathSliceColumns,
+          criticalPathsliceColumnNames,
+        );
+      },
+    });
+
+    ctx.registerCommand({
+      id: 'perfetto.CriticalPathPprof_AreaSelection',
+      name: 'Critical path pprof (over area selection)',
+      callback: async () => {
+        const trackUtid = getFirstUtidOfSelectionOrVisibleWindow();
+        const window = await getTimeSpanOfSelectionOrVisibleWindow();
+        if (trackUtid === 0) {
+          return showModalErrorAreaSelectionRequired();
+        }
+        addQueryResultsTab({
+          query: `
+              INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
+              SELECT *
+                FROM
+                  _thread_executing_span_critical_path_graph(
+                  "criical_path",
+                    ${trackUtid},
+                    ${window.start},
+                    ${window.end} - ${window.start}) cr`,
+          title: 'Critical path',
+        });
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.CriticalPath',
+  plugin: CriticalPath,
+};
diff --git a/ui/src/core_plugins/frames/actual_frames_track.ts b/ui/src/core_plugins/frames/actual_frames_track.ts
index 1f3fe4c..5f4cdaf 100644
--- a/ui/src/core_plugins/frames/actual_frames_track.ts
+++ b/ui/src/core_plugins/frames/actual_frames_track.ts
@@ -48,10 +48,10 @@
   constructor(
     engine: Engine,
     maxDepth: number,
-    trackKey: string,
+    uri: string,
     private trackIds: number[],
   ) {
-    super({engine, trackKey});
+    super({engine, uri});
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
diff --git a/ui/src/core_plugins/frames/expected_frames_track.ts b/ui/src/core_plugins/frames/expected_frames_track.ts
index ee51060..c3e110e 100644
--- a/ui/src/core_plugins/frames/expected_frames_track.ts
+++ b/ui/src/core_plugins/frames/expected_frames_track.ts
@@ -28,10 +28,10 @@
   constructor(
     engine: Engine,
     maxDepth: number,
-    trackKey: string,
+    uri: string,
     private trackIds: number[],
   ) {
-    super({engine, trackKey});
+    super({engine, uri});
     this.sliceLayout = {
       ...SLICE_LAYOUT_FIT_CONTENT_DEFAULTS,
       depthGuess: maxDepth,
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 45e2e2e..9c1447d 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -74,12 +74,11 @@
         kind: 'ExpectedFrames',
       });
 
+      const uri = `/process_${upid}/expected_frames`;
       ctx.registerTrack({
         uri: `/process_${upid}/expected_frames`,
         title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ExpectedFramesTrack(engine, maxDepth, trackKey, trackIds);
-        },
+        track: new ExpectedFramesTrack(engine, maxDepth, uri, trackIds),
         tags: {
           trackIds,
           upid,
@@ -135,12 +134,11 @@
         kind,
       });
 
+      const uri = `/process_${upid}/actual_frames`;
       ctx.registerTrack({
-        uri: `/process_${upid}/actual_frames`,
+        uri,
         title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ActualFramesTrack(engine, maxDepth, trackKey, trackIds);
-        },
+        track: new ActualFramesTrack(engine, maxDepth, uri, trackIds),
         tags: {
           upid,
           trackIds,
diff --git a/ui/src/core_plugins/ftrace/index.ts b/ui/src/core_plugins/ftrace/index.ts
index b1486ee..3f9e88e 100644
--- a/ui/src/core_plugins/ftrace/index.ts
+++ b/ui/src/core_plugins/ftrace/index.ts
@@ -64,16 +64,14 @@
     for (const cpuNum of cpus) {
       const uri = `/ftrace/cpu${cpuNum}`;
 
-      ctx.registerStaticTrack({
+      ctx.registerTrackAndShowOnTraceLoad({
         uri,
-        groupName: 'Ftrace Events',
         title: `Ftrace Track for CPU ${cpuNum}`,
         tags: {
           cpu: cpuNum,
+          groupName: 'Ftrace Events',
         },
-        trackFactory: () => {
-          return new FtraceRawTrack(ctx.engine, cpuNum, filterStore);
-        },
+        track: new FtraceRawTrack(ctx.engine, cpuNum, filterStore),
       });
     }
 
diff --git a/ui/src/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
index 12311dc..82c1f1f 100644
--- a/ui/src/core_plugins/heap_profile/heap_profile_track.ts
+++ b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
@@ -14,6 +14,7 @@
 
 import {Actions} from '../../common/actions';
 import {LegacySelection, ProfileType} from '../../common/state';
+import {profileType} from '../../core/selection_manager';
 import {
   BASE_ROW,
   BaseSliceTrack,
@@ -21,7 +22,6 @@
   OnSliceOverArgs,
 } from '../../frontend/base_slice_track';
 import {globals} from '../../frontend/globals';
-import {profileType} from '../../frontend/legacy_flamegraph_panel';
 import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public';
 import {STR} from '../../trace_processor/query_result';
@@ -99,13 +99,9 @@
 
   rowToSlice(row: HeapProfileRow): HeapProfileSlice {
     const slice = this.rowToSliceBase(row);
-    let type = row.type;
-    if (type === 'heap_profile:libc.malloc,com.android.art') {
-      type = 'heap_profile:com.android.art,libc.malloc';
-    }
     return {
       ...slice,
-      type: profileType(type),
+      type: profileType(row.type),
     };
   }
 
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 2133173..c99e8bf 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -16,16 +16,11 @@
 
 import {assertExists, assertFalse} from '../../base/logging';
 import {Monitor} from '../../base/monitor';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
 import {
   HeapProfileSelection,
   LegacySelection,
   ProfileType,
 } from '../../core/selection_manager';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
 import {Timestamp} from '../../frontend/widgets/timestamp';
 import {
   Engine,
@@ -42,7 +37,6 @@
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../../core/query_flamegraph';
 import {time} from '../../base/time';
@@ -57,7 +51,6 @@
 import {Modal} from '../../widgets/modal';
 import {Router} from '../../frontend/router';
 import {Actions} from '../../common/actions';
-import {SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG} from '../../common/legacy_flamegraph_util';
 
 class HeapProfilePlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
@@ -68,22 +61,21 @@
     `);
     for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
       const upid = it.upid;
+      const uri = `/process_${upid}/heap_profile`;
       ctx.registerTrack({
-        uri: `/process_${upid}/heap_profile`,
+        uri,
         title: 'Heap Profile',
         tags: {
           kind: HEAP_PROFILE_TRACK_KIND,
           upid,
         },
-        trackFactory: ({trackKey}) => {
-          return new HeapProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            upid,
-          );
-        },
+        track: new HeapProfileTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          upid,
+        ),
       });
     }
     const it = await ctx.engine.query(`
@@ -105,7 +97,6 @@
     () => this.sel?.type,
   ]);
   private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('heap_profile');
 
   constructor(
     private engine: Engine,
@@ -117,18 +108,6 @@
       this.sel = undefined;
       return undefined;
     }
-    if (!USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.ts,
-          end: sel.ts,
-          upids: [sel.upid],
-        },
-      });
-    }
 
     const {ts, upid, type} = sel;
     this.sel = sel;
@@ -319,37 +298,6 @@
 }
 
 function flamegraphAttrsForHeapGraph(engine: Engine, ts: time, upid: number) {
-  const dominator = SHOW_HEAP_GRAPH_DOMINATOR_TREE_FLAG.get()
-    ? metricsFromTableOrSubquery(
-        `
-          (
-            select
-              id,
-              parent_id as parentId,
-              name,
-              root_type,
-              self_size,
-              self_count
-            from _heap_graph_dominator_class_tree
-            where graph_sample_ts = ${ts} and upid = ${upid}
-          )
-        `,
-        [
-          {
-            name: 'Dominated Object Size',
-            unit: 'B',
-            columnName: 'self_size',
-          },
-          {
-            name: 'Dominated Object Count',
-            unit: '',
-            columnName: 'self_count',
-          },
-        ],
-        'include perfetto module android.memory.heap_graph.dominator_class_tree;',
-        [{name: 'root_type', displayName: 'Root Type'}],
-      )
-    : [];
   return {
     engine,
     metrics: [
@@ -382,7 +330,35 @@
         'include perfetto module android.memory.heap_graph.class_tree;',
         [{name: 'root_type', displayName: 'Root Type'}],
       ),
-      ...dominator,
+      ...metricsFromTableOrSubquery(
+        `
+          (
+            select
+              id,
+              parent_id as parentId,
+              name,
+              root_type,
+              self_size,
+              self_count
+            from _heap_graph_dominator_class_tree
+            where graph_sample_ts = ${ts} and upid = ${upid}
+          )
+        `,
+        [
+          {
+            name: 'Dominated Object Size',
+            unit: 'B',
+            columnName: 'self_size',
+          },
+          {
+            name: 'Dominated Object Count',
+            unit: '',
+            columnName: 'self_count',
+          },
+        ],
+        'include perfetto module android.memory.heap_graph.dominator_class_tree;',
+        [{name: 'root_type', displayName: 'Root Type'}],
+      ),
     ],
   };
 }
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 1390688..3a9092d 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -20,11 +20,6 @@
   LegacyDetailsPanel,
   PERF_SAMPLES_PROFILE_TRACK_KIND,
 } from '../../public';
-import {LegacyFlamegraphCache} from '../../core/legacy_flamegraph_cache';
-import {
-  LegacyFlamegraphDetailsPanel,
-  profileType,
-} from '../../frontend/legacy_flamegraph_panel';
 import {
   PerfettoPlugin,
   PluginContextTrace,
@@ -38,7 +33,6 @@
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../../core/query_flamegraph';
 import {Monitor} from '../../base/monitor';
@@ -65,21 +59,21 @@
     `);
     for (const it = pResult.iter({upid: NUM}); it.valid(); it.next()) {
       const upid = it.upid;
+      const uri = `/process_${upid}/perf_samples_profile`;
       ctx.registerTrack({
-        uri: `/process_${upid}/perf_samples_profile`,
+        uri,
         title: `Process Callstacks`,
         tags: {
           kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
           upid,
         },
-        trackFactory: ({trackKey}) =>
-          new ProcessPerfSamplesProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            upid,
-          ),
+        track: new ProcessPerfSamplesProfileTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          upid,
+        ),
       });
     }
     const tResult = await ctx.engine.query(`
@@ -107,22 +101,22 @@
         threadName === null
           ? `Thread Callstacks ${tid}`
           : `${threadName} Callstacks ${tid}`;
+      const uri = `${getThreadUriPrefix(upid, utid)}_perf_samples_profile`;
       ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_perf_samples_profile`,
+        uri,
         title: displayName,
         tags: {
           kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
           utid,
           upid: upid ?? undefined,
         },
-        trackFactory: ({trackKey}) =>
-          new ThreadPerfSamplesProfileTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            utid,
-          ),
+        track: new ThreadPerfSamplesProfileTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          utid,
+        ),
       });
     }
     ctx.registerDetailsPanel(new PerfSamplesFlamegraphDetailsPanel(ctx.engine));
@@ -139,7 +133,6 @@
     () => this.sel?.type,
   ]);
   private flamegraphAttrs?: QueryFlamegraphAttrs;
-  private cache = new LegacyFlamegraphCache('perf_samples');
 
   constructor(private engine: Engine) {}
 
@@ -148,22 +141,6 @@
       this.sel = undefined;
       return undefined;
     }
-    if (
-      !USE_NEW_FLAMEGRAPH_IMPL.get() &&
-      sel.utid === undefined &&
-      sel.upid !== undefined
-    ) {
-      this.sel = undefined;
-      return m(LegacyFlamegraphDetailsPanel, {
-        cache: this.cache,
-        selection: {
-          profileType: profileType(sel.type),
-          start: sel.leftTs,
-          end: sel.rightTs,
-          upids: [sel.upid],
-        },
-      });
-    }
 
     const {leftTs, rightTs, upid, utid} = sel;
     this.sel = sel;
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
index 0e43da4..b8b9d0c 100644
--- a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -12,35 +12,40 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Slice} from '../../public';
+import {NUM, Slice} from '../../public';
 import {
   BaseSliceTrack,
   OnSliceClickArgs,
 } from '../../frontend/base_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
 import {NAMED_ROW, NamedRow} from '../../frontend/named_slice_track';
-import {getColorForSlice} from '../../core/colorizer';
+import {getColorForSample} from '../../core/colorizer';
 import {Time} from '../../base/time';
 import {globals} from '../../frontend/globals';
 import {Actions} from '../../common/actions';
 import {LegacySelection, ProfileType} from '../../core/selection_manager';
+import {assertExists} from '../../base/logging';
+
+interface PerfSampleRow extends NamedRow {
+  callsiteId: number;
+}
 
 abstract class BasePerfSamplesProfileTrack extends BaseSliceTrack<
   Slice,
-  NamedRow
+  PerfSampleRow
 > {
   constructor(args: NewTrackArgs) {
     super(args);
   }
 
-  protected getRowSpec(): NamedRow {
-    return NAMED_ROW;
+  protected getRowSpec(): PerfSampleRow {
+    return {...NAMED_ROW, callsiteId: NUM};
   }
 
-  protected rowToSlice(row: NamedRow): Slice {
+  protected rowToSlice(row: PerfSampleRow): Slice {
     const baseSlice = super.rowToSliceBase(row);
-    const name = row.name ?? '';
-    const colorScheme = getColorForSlice(name);
+    const name = assertExists(row.name);
+    const colorScheme = getColorForSample(row.callsiteId);
     return {...baseSlice, title: name, colorScheme};
   }
 
@@ -65,11 +70,16 @@
 
   getSqlSource(): string {
     return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
       from perf_sample p
       join thread using (utid)
-      where upid = ${this.upid}
-        and callsite_id is not null
+      where upid = ${this.upid} and callsite_id is not null
       order by ts
     `;
   }
@@ -97,10 +107,15 @@
 
   getSqlSource(): string {
     return `
-      select p.id, ts, 0 as dur, 0 as depth, 'Perf Sample' as name
+      select
+        p.id,
+        ts,
+        0 as dur,
+        0 as depth,
+        'Perf Sample' as name,
+        callsite_id as callsiteId
       from perf_sample p
-      where utid = ${this.utid}
-        and callsite_id is not null
+      where utid = ${this.utid} and callsite_id is not null
       order by ts
     `;
   }
diff --git a/ui/src/core_plugins/process/table.ts b/ui/src/core_plugins/process/table.ts
index 9d5f046..bb18e50 100644
--- a/ui/src/core_plugins/process/table.ts
+++ b/ui/src/core_plugins/process/table.ts
@@ -12,6 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import m from 'mithril';
+
+import {
+  AggregationConfig,
+  SourceTable,
+  SqlColumn,
+  TableColumn,
+  TableManager,
+} from '../../frontend/widgets/sql/table/column';
+import {getStandardContextMenuItems} from '../../frontend/widgets/sql/table/render_cell_utils';
 import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
 import {
   ArgSetColumnSet,
@@ -19,21 +29,95 @@
   StandardColumn,
   TimestampColumn,
 } from '../../frontend/widgets/sql/table/well_known_columns';
+import {SqlValue} from '../../trace_processor/sql_utils';
+import {PopupMenu2} from '../../widgets/menu';
+import {Anchor} from '../../widgets/anchor';
+import {showProcessDetailsMenuItem} from '../../frontend/widgets/process';
+import {asUpid} from '../../trace_processor/sql_utils/core_types';
+
+// ProcessIdColumn is a column type for displaying primary key of the `process` table.
+// All other references (foreign keys) should use `ProcessColumn` instead.
+class ProcessIdColumn extends TableColumn {
+  private columns: {pid: SqlColumn};
+
+  constructor(private upid: SqlColumn) {
+    super({});
+
+    const processTable: SourceTable = {
+      table: 'process',
+      joinOn: {id: this.upid},
+      innerJoin: true,
+    };
+
+    this.columns = {
+      pid: {
+        column: 'pid',
+        source: processTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.upid;
+  }
+
+  getTitle() {
+    return 'upid';
+  }
+
+  dependentColumns() {
+    return {
+      pid: this.columns.pid,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const upid = value;
+    const rawPid = data['pid'];
+
+    if (typeof upid !== 'bigint') {
+      throw new Error(
+        `process.upid is expected to be bigint, got ${typeof upid}`,
+      );
+    }
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, `${upid}`),
+      },
+
+      showProcessDetailsMenuItem(
+        asUpid(Number(upid)),
+        rawPid === null ? undefined : Number(rawPid),
+      ),
+      getStandardContextMenuItems(upid, this.upid, manager),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
+}
 
 export function getProcessTable(): SqlTableDescription {
   return {
     name: 'process',
     columns: [
-      new StandardColumn('upid'),
-      new StandardColumn('pid'),
+      new ProcessIdColumn('upid'),
+      new StandardColumn('pid', {aggregationType: 'nominal'}),
       new StandardColumn('name'),
       new TimestampColumn('start_ts'),
       new TimestampColumn('end_ts'),
       new ProcessColumn('parent_upid'),
-      new StandardColumn('uid'),
-      new StandardColumn('android_appid'),
+      new StandardColumn('uid', {aggregationType: 'nominal'}),
+      new StandardColumn('android_appid', {aggregationType: 'nominal'}),
       new StandardColumn('cmdline', {startsHidden: true}),
-      new StandardColumn('machine_id'),
+      new StandardColumn('machine_id', {aggregationType: 'nominal'}),
       new ArgSetColumnSet('arg_set_id'),
     ],
   };
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
index a6c39a0..096045f 100644
--- a/ui/src/core_plugins/process_summary/index.ts
+++ b/ui/src/core_plugins/process_summary/index.ts
@@ -125,9 +125,7 @@
             kind: PROCESS_SCHEDULING_TRACK_KIND,
           },
           chips,
-          trackFactory: () => {
-            return new ProcessSchedulingTrack(ctx.engine, config, cpuCount);
-          },
+          track: new ProcessSchedulingTrack(ctx.engine, config, cpuCount),
           subtitle,
         });
       } else {
@@ -144,7 +142,7 @@
             kind: PROCESS_SUMMARY_TRACK,
           },
           chips,
-          trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
+          track: new ProcessSummaryTrack(ctx.engine, config),
           subtitle,
         });
       }
@@ -202,7 +200,7 @@
       tags: {
         kind: PROCESS_SUMMARY_TRACK,
       },
-      trackFactory: () => new ProcessSummaryTrack(ctx.engine, config),
+      track: new ProcessSummaryTrack(ctx.engine, config),
     });
   }
 }
diff --git a/ui/src/core_plugins/sched/active_cpu_count.ts b/ui/src/core_plugins/sched/active_cpu_count.ts
index 794cd62..3ee99aa 100644
--- a/ui/src/core_plugins/sched/active_cpu_count.ts
+++ b/ui/src/core_plugins/sched/active_cpu_count.ts
@@ -13,14 +13,15 @@
 // limitations under the License.
 
 import m from 'mithril';
-
+import {Icons} from '../../base/semantic_icons';
 import {sqliteString} from '../../base/string_utils';
 import {
   BaseCounterTrack,
   CounterOptions,
 } from '../../frontend/base_counter_track';
-import {CloseTrackButton} from '../../frontend/close_track_button';
+import {globals} from '../../frontend/globals';
 import {Engine, TrackContext} from '../../public';
+import {Button} from '../../widgets/button';
 
 export enum CPUType {
   Big = 'big',
@@ -34,14 +35,19 @@
   constructor(ctx: TrackContext, engine: Engine, cpuType?: CPUType) {
     super({
       engine,
-      trackKey: ctx.trackKey,
+      uri: ctx.trackUri,
     });
     this.cpuType = cpuType;
   }
 
   getTrackShellButtons(): m.Children {
-    return m(CloseTrackButton, {
-      trackKey: this.trackKey,
+    return m(Button, {
+      onclick: () => {
+        globals.workspace.getTrackByUri(this.uri)?.remove();
+      },
+      icon: Icons.Close,
+      title: 'Close',
+      compact: true,
     });
   }
 
diff --git a/ui/src/core_plugins/sched/index.ts b/ui/src/core_plugins/sched/index.ts
index 10cf5d7..b35638b 100644
--- a/ui/src/core_plugins/sched/index.ts
+++ b/ui/src/core_plugins/sched/index.ts
@@ -12,17 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {uuidv4} from '../../base/uuid';
-import {Actions} from '../../common/actions';
-import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {globals} from '../../frontend/globals';
 import {addSqlTableTab} from '../../frontend/sql_table_tab_command';
 import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
+import {TrackNode} from '../../public/workspace';
 import {
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
-  PrimaryTrackSortKey,
 } from '../../public';
 
 import {ActiveCPUCountTrack, CPUType} from './active_cpu_count';
@@ -35,17 +31,16 @@
     ctx.registerTrack({
       uri: runnableThreadCountUri,
       title: 'Runnable thread count',
-      trackFactory: (trackCtx) =>
-        new RunnableThreadCountTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-        }),
+      track: new RunnableThreadCountTrack({
+        engine: ctx.engine,
+        uri: runnableThreadCountUri,
+      }),
     });
     ctx.registerCommand({
       id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
       name: 'Add track: runnable thread count',
       callback: () =>
-        addPinnedTrack(runnableThreadCountUri, 'Runnable thread count'),
+        addPinnedTrack(ctx, runnableThreadCountUri, 'Runnable thread count'),
     });
 
     const uri = uriForActiveCPUCountTrack();
@@ -53,12 +48,12 @@
     ctx.registerTrack({
       uri,
       title: title,
-      trackFactory: (trackCtx) => new ActiveCPUCountTrack(trackCtx, ctx.engine),
+      track: new ActiveCPUCountTrack({trackUri: uri}, ctx.engine),
     });
     ctx.registerCommand({
       id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
       name: 'Add track: active CPU count',
-      callback: () => addPinnedTrack(uri, title),
+      callback: () => addPinnedTrack(ctx, uri, title),
     });
 
     for (const cpuType of Object.values(CPUType)) {
@@ -67,14 +62,13 @@
       ctx.registerTrack({
         uri,
         title: title,
-        trackFactory: (trackCtx) =>
-          new ActiveCPUCountTrack(trackCtx, ctx.engine, cpuType),
+        track: new ActiveCPUCountTrack({trackUri: uri}, ctx.engine, cpuType),
       });
 
       ctx.registerCommand({
         id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
         name: `Add track: active ${cpuType} CPU count`,
-        callback: () => addPinnedTrack(uri, title),
+        callback: () => addPinnedTrack(ctx, uri, title),
       });
     }
 
@@ -100,18 +94,11 @@
   }
 }
 
-function addPinnedTrack(uri: string, title: string) {
-  const key = uuidv4();
-  globals.dispatchMultiple([
-    Actions.addTrack({
-      key,
-      uri,
-      name: title,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-    }),
-    Actions.toggleTrackPinned({trackKey: key}),
-  ]);
+function addPinnedTrack(ctx: PluginContextTrace, uri: string, title: string) {
+  const track = new TrackNode(uri, title);
+  // Add track to the top of the stack
+  ctx.timeline.workspace.prependChild(track);
+  track.pin();
 }
 
 export const plugin: PluginDescriptor = {
diff --git a/ui/src/core_plugins/sched/runnable_thread_count.ts b/ui/src/core_plugins/sched/runnable_thread_count.ts
index 3908572..9b5e9c5 100644
--- a/ui/src/core_plugins/sched/runnable_thread_count.ts
+++ b/ui/src/core_plugins/sched/runnable_thread_count.ts
@@ -12,13 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import m from 'mithril';
-
 import {
   BaseCounterTrack,
   CounterOptions,
 } from '../../frontend/base_counter_track';
-import {CloseTrackButton} from '../../frontend/close_track_button';
 import {NewTrackArgs} from '../../frontend/track';
 
 export class RunnableThreadCountTrack extends BaseCounterTrack {
@@ -26,12 +23,6 @@
     super(args);
   }
 
-  getTrackShellButtons(): m.Children {
-    return m(CloseTrackButton, {
-      trackKey: this.trackKey,
-    });
-  }
-
   protected getDefaultCounterOptions(): CounterOptions {
     const options = super.getDefaultCounterOptions();
     options.yRangeRounding = 'strict';
diff --git a/ui/src/core_plugins/sched/table.ts b/ui/src/core_plugins/sched/table.ts
index 0f0b274..105c80b 100644
--- a/ui/src/core_plugins/sched/table.ts
+++ b/ui/src/core_plugins/sched/table.ts
@@ -29,9 +29,9 @@
       new SchedIdColumn('id'),
       new TimestampColumn('ts'),
       new DurationColumn('dur'),
-      new StandardColumn('cpu'),
-      new StandardColumn('priority'),
-      new ThreadColumn('utid', {title: 'Thread'}),
+      new StandardColumn('cpu', {aggregationType: 'nominal'}),
+      new StandardColumn('priority', {aggregationType: 'nominal'}),
+      new ThreadColumn('utid', {title: 'Thread', notNull: true}),
       new ProcessColumn(
         {
           column: 'upid',
@@ -40,12 +40,16 @@
             joinOn: {
               utid: 'utid',
             },
+            innerJoin: true,
           },
         },
-        {title: 'Process'},
+        {title: 'Process', notNull: true},
       ),
       new StandardColumn('end_state'),
-      new StandardColumn('ucpu'),
+      new StandardColumn('ucpu', {
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
     ],
   };
 }
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index a1bacde..1a0320d 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -41,12 +41,10 @@
       ctx.registerTrack({
         uri,
         title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new ScreenshotsTrack({
-            engine: ctx.engine,
-            trackKey,
-          });
-        },
+        track: new ScreenshotsTrack({
+          engine: ctx.engine,
+          uri,
+        }),
         tags: {
           kind: ScreenshotsTrack.kind,
         },
diff --git a/ui/src/core_plugins/slice/table.ts b/ui/src/core_plugins/slice/table.ts
index fa800d3..29b453c 100644
--- a/ui/src/core_plugins/slice/table.ts
+++ b/ui/src/core_plugins/slice/table.ts
@@ -29,13 +29,17 @@
     name: '_slice_with_thread_and_process_info',
     displayName: 'slice',
     columns: [
-      new SliceIdColumn('id'),
+      new SliceIdColumn('id', {notNull: true}),
       new TimestampColumn('ts', {title: 'Timestamp'}),
       new DurationColumn('dur', {title: 'Duration'}),
       new DurationColumn('thread_dur', {title: 'Thread duration'}),
       new StandardColumn('category', {title: 'Category'}),
       new StandardColumn('name', {title: 'Name'}),
-      new StandardColumn('track_id', {title: 'Track ID', startsHidden: true}),
+      new StandardColumn('track_id', {
+        title: 'Track ID',
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
       new ThreadColumn('utid', {title: 'Thread'}),
       new ProcessColumn('upid', {title: 'Process'}),
       new StandardColumn('depth', {title: 'Depth', startsHidden: true}),
diff --git a/ui/src/core_plugins/thread/table.ts b/ui/src/core_plugins/thread/table.ts
index 920039f..6deb63e 100644
--- a/ui/src/core_plugins/thread/table.ts
+++ b/ui/src/core_plugins/thread/table.ts
@@ -12,24 +12,110 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import m from 'mithril';
+
+import {
+  AggregationConfig,
+  SourceTable,
+  SqlColumn,
+  TableColumn,
+  TableManager,
+} from '../../frontend/widgets/sql/table/column';
 import {SqlTableDescription} from '../../frontend/widgets/sql/table/table_description';
+import {SqlValue} from '../../trace_processor/query_result';
+import {PopupMenu2} from '../../widgets/menu';
+import {Anchor} from '../../widgets/anchor';
+import {getStandardContextMenuItems} from '../../frontend/widgets/sql/table/render_cell_utils';
 import {
   ProcessColumn,
   StandardColumn,
   TimestampColumn,
 } from '../../frontend/widgets/sql/table/well_known_columns';
+import {showThreadDetailsMenuItem} from '../../frontend/widgets/thread';
+import {asUtid} from '../../trace_processor/sql_utils/core_types';
+
+// ThreadIdColumn is a column type for displaying primary key of the `thread` table.
+// All other references (foreign keys) should use `ThreadColumn` instead.
+class ThreadIdColumn extends TableColumn {
+  private columns: {tid: SqlColumn};
+
+  constructor(private utid: SqlColumn) {
+    super({});
+
+    const threadTable: SourceTable = {
+      table: 'thread',
+      joinOn: {id: this.utid},
+      innerJoin: true,
+    };
+
+    this.columns = {
+      tid: {
+        column: 'tid',
+        source: threadTable,
+      },
+    };
+  }
+
+  primaryColumn(): SqlColumn {
+    return this.utid;
+  }
+
+  getTitle() {
+    return 'utid';
+  }
+
+  dependentColumns() {
+    return {
+      tid: this.columns.tid,
+    };
+  }
+
+  renderCell(
+    value: SqlValue,
+    manager: TableManager,
+    data: {[key: string]: SqlValue},
+  ): m.Children {
+    const utid = value;
+    const rawTid = data['tid'];
+
+    if (typeof utid !== 'bigint') {
+      throw new Error(
+        `thread.upid is expected to be bigint, got ${typeof utid}`,
+      );
+    }
+
+    return m(
+      PopupMenu2,
+      {
+        trigger: m(Anchor, `${utid}`),
+      },
+
+      showThreadDetailsMenuItem(
+        asUtid(Number(utid)),
+        rawTid === null ? undefined : Number(rawTid),
+      ),
+      getStandardContextMenuItems(utid, this.utid, manager),
+    );
+  }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
+}
 
 export function getThreadTable(): SqlTableDescription {
   return {
     name: 'thread',
     columns: [
-      new StandardColumn('utid'),
-      new StandardColumn('tid'),
+      new ThreadIdColumn('utid'),
+      new StandardColumn('tid', {aggregationType: 'nominal'}),
       new StandardColumn('name'),
       new TimestampColumn('start_ts'),
       new TimestampColumn('end_ts'),
-      new ProcessColumn('upid'),
-      new StandardColumn('is_main_thread'),
+      new ProcessColumn('upid', {notNull: true}),
+      new StandardColumn('is_main_thread', {
+        aggregationType: 'nominal',
+      }),
     ],
   };
 }
diff --git a/ui/src/core_plugins/thread_slice/index.ts b/ui/src/core_plugins/thread_slice/index.ts
index 74eebc2..e2c4395 100644
--- a/ui/src/core_plugins/thread_slice/index.ts
+++ b/ui/src/core_plugins/thread_slice/index.ts
@@ -36,22 +36,24 @@
       include perfetto module viz.threads;
 
       select
-        thread_track.utid as utid,
-        thread_track.id as trackId,
-        thread_track.name as trackName,
-        EXTRACT_ARG(thread_track.source_arg_set_id,
-                    'is_root_in_scope') as isDefaultTrackForScope,
-        tid,
+        tt.utid as utid,
+        tt.id as trackId,
+        tt.name as trackName,
+        extract_arg(
+          tt.source_arg_set_id,
+          'is_root_in_scope'
+        ) as isDefaultTrackForScope,
+        t.tid,
         t.name as threadName,
-        max_depth as maxDepth,
+        s.max_depth as maxDepth,
         t.upid as upid,
-        is_main_thread as isMainThread,
-        is_kernel_thread AS isKernelThread
-      from thread_track
+        t.is_main_thread as isMainThread,
+        t.is_kernel_thread AS isKernelThread
+      from _thread_track_summary_by_utid_and_name s
       join _threads_with_kernel_flag t using(utid)
-      join _slice_track_summary sts on sts.id = thread_track.id
+      join thread_track tt on s.track_id = tt.id
+      where s.track_count = 1
   `);
-
     const it = result.iter({
       utid: NUM,
       trackId: NUM,
@@ -64,7 +66,6 @@
       isMainThread: NUM_NULL,
       isKernelThread: NUM,
     });
-
     for (; it.valid(); it.next()) {
       const {
         upid,
@@ -86,8 +87,9 @@
         kind: 'Slices',
       });
 
+      const uri = `${getThreadUriPrefix(upid, utid)}_slice_${trackId}`;
       ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_slice_${trackId}`,
+        uri,
         title: displayName,
         tags: {
           trackIds: [trackId],
@@ -99,13 +101,14 @@
         chips: removeFalsyValues([
           isKernelThread === 0 && isMainThread === 1 && 'main thread',
         ]),
-        trackFactory: ({trackKey}) => {
-          const newTrackArgs = {
+        track: new ThreadSliceTrack(
+          {
             engine: ctx.engine,
-            trackKey,
-          };
-          return new ThreadSliceTrack(newTrackArgs, trackId, maxDepth);
-        },
+            uri,
+          },
+          trackId,
+          maxDepth,
+        ),
       });
     }
 
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index 6a91252..617da03 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -66,8 +66,9 @@
         kind: THREAD_STATE_TRACK_KIND,
       });
 
+      const uri = `${getThreadUriPrefix(upid, utid)}_state`;
       ctx.registerTrack({
-        uri: `${getThreadUriPrefix(upid, utid)}_state`,
+        uri,
         title: displayName,
         tags: {
           kind: THREAD_STATE_TRACK_KIND,
@@ -77,15 +78,13 @@
         chips: removeFalsyValues([
           isKernelThread === 0 && isMainThread === 1 && 'main thread',
         ]),
-        trackFactory: ({trackKey}) => {
-          return new ThreadStateTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            utid,
-          );
-        },
+        track: new ThreadStateTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          utid,
+        ),
       });
     }
 
diff --git a/ui/src/core_plugins/thread_state/table.ts b/ui/src/core_plugins/thread_state/table.ts
index 3372665..7897127 100644
--- a/ui/src/core_plugins/thread_state/table.ts
+++ b/ui/src/core_plugins/thread_state/table.ts
@@ -26,12 +26,12 @@
   return {
     name: 'thread_state',
     columns: [
-      new ThreadStateIdColumn('id'),
+      new ThreadStateIdColumn('id', {notNull: true}),
       new TimestampColumn('ts'),
       new DurationColumn('dur'),
       new StandardColumn('state'),
-      new StandardColumn('cpu'),
-      new ThreadColumn('utid', {title: 'Thread'}),
+      new StandardColumn('cpu', {aggregationType: 'nominal'}),
+      new ThreadColumn('utid', {title: 'Thread', notNull: true}),
       new ProcessColumn(
         {
           column: 'upid',
@@ -40,16 +40,20 @@
             joinOn: {
               utid: 'utid',
             },
+            innerJoin: true,
           },
         },
-        {title: 'Process'},
+        {title: 'Process', notNull: true},
       ),
-      new StandardColumn('io_wait'),
+      new StandardColumn('io_wait', {aggregationType: 'nominal'}),
       new StandardColumn('blocked_function'),
       new ThreadColumn('waker_utid', {title: 'Waker thread'}),
       new ThreadStateIdColumn('waker_id'),
-      new StandardColumn('irq_context'),
-      new StandardColumn('ucpu'),
+      new StandardColumn('irq_context', {aggregationType: 'nominal'}),
+      new StandardColumn('ucpu', {
+        aggregationType: 'nominal',
+        startsHidden: true,
+      }),
     ],
   };
 }
diff --git a/ui/src/core_plugins/thread_state/thread_state_track.ts b/ui/src/core_plugins/thread_state/thread_state_track.ts
index 660700e..150ebb4 100644
--- a/ui/src/core_plugins/thread_state/thread_state_track.ts
+++ b/ui/src/core_plugins/thread_state/thread_state_track.ts
@@ -89,7 +89,7 @@
     globals.makeSelection(
       Actions.selectThreadState({
         id: args.slice.id,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
       }),
     );
   }
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index 5c46f06..a51a170 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -66,24 +66,15 @@
             sortedOptions,
           );
 
-          // Find the first track with this URI
-          const firstTrack = Object.values(globals.state.tracks).find(
-            ({uri}) => uri === selectedUri,
+          verticalScrollToTrack(selectedUri, true);
+          const traceTime = globals.traceContext;
+          globals.makeSelection(
+            Actions.selectArea({
+              start: traceTime.start,
+              end: traceTime.end,
+              trackUris: [selectedUri],
+            }),
           );
-          if (firstTrack) {
-            console.log(firstTrack);
-            verticalScrollToTrack(firstTrack.key, true);
-            const traceTime = globals.traceContext;
-            globals.makeSelection(
-              Actions.selectArea({
-                start: traceTime.start,
-                end: traceTime.end,
-                tracks: [firstTrack.key],
-              }),
-            );
-          } else {
-            alert(`No tracks with uri ${selectedUri} on the timeline`);
-          }
         } catch {
           // Prompt was probably cancelled - do nothing.
         }
diff --git a/ui/src/core_plugins/wattson/index.ts b/ui/src/core_plugins/wattson/index.ts
index 8fa20ee..54841fb 100644
--- a/ui/src/core_plugins/wattson/index.ts
+++ b/ui/src/core_plugins/wattson/index.ts
@@ -37,28 +37,29 @@
     const cpus = globals.traceContext.cpus;
     for (const cpu of cpus) {
       const queryKey = `cpu${cpu}_curve`;
-      ctx.registerStaticTrack({
-        uri: `/wattson/cpu_subsystem_estimate_cpu${cpu}`,
+      const uri = `/wattson/cpu_subsystem_estimate_cpu${cpu}`;
+      ctx.registerTrackAndShowOnTraceLoad({
+        uri,
         title: `Cpu${cpu} Estimate`,
-        trackFactory: ({trackKey}) =>
-          new CpuSubsystemEstimateTrack(ctx.engine, trackKey, queryKey),
-        groupName: `Wattson`,
+        track: new CpuSubsystemEstimateTrack(ctx.engine, uri, queryKey),
+
         tags: {
           kind: CPUSS_ESTIMATE_TRACK_KIND,
           wattson: `CPU${cpu}`,
+          groupName: `Wattson`,
         },
       });
     }
 
-    ctx.registerStaticTrack({
-      uri: `/wattson/cpu_subsystem_estimate_dsu_scu`,
+    const uri = `/wattson/cpu_subsystem_estimate_dsu_scu`;
+    ctx.registerTrackAndShowOnTraceLoad({
+      uri,
       title: `DSU/SCU Estimate`,
-      trackFactory: ({trackKey}) =>
-        new CpuSubsystemEstimateTrack(ctx.engine, trackKey, `dsu_scu`),
-      groupName: `Wattson`,
+      track: new CpuSubsystemEstimateTrack(ctx.engine, uri, `dsu_scu`),
       tags: {
         kind: CPUSS_ESTIMATE_TRACK_KIND,
         wattson: 'Dsu_Scu',
+        groupName: `Wattson`,
       },
     });
   }
@@ -68,10 +69,10 @@
   readonly engine: Engine;
   readonly queryKey: string;
 
-  constructor(engine: Engine, trackKey: string, queryKey: string) {
+  constructor(engine: Engine, uri: string, queryKey: string) {
     super({
-      engine: engine,
-      trackKey: trackKey,
+      engine,
+      uri,
     });
     this.engine = engine;
     this.queryKey = queryKey;
diff --git a/ui/src/frontend/aggregation_tab.ts b/ui/src/frontend/aggregation_tab.ts
index 8abaa06..aefc40b 100644
--- a/ui/src/frontend/aggregation_tab.ts
+++ b/ui/src/frontend/aggregation_tab.ts
@@ -23,24 +23,20 @@
 import {EmptyState} from '../widgets/empty_state';
 import {FlowEventsAreaSelectedPanel} from './flow_events_panel';
 import {PivotTable} from './pivot_table';
-import {
-  LegacyFlamegraphDetailsPanel,
-  FlamegraphSelectionParams,
-} from './legacy_flamegraph_panel';
-import {AreaSelection, ProfileType, TrackState} from '../common/state';
-import {assertExists} from '../base/logging';
+import {AreaSelection} from '../common/state';
 import {Monitor} from '../base/monitor';
 import {
+  CPU_PROFILE_TRACK_KIND,
   PERF_SAMPLES_PROFILE_TRACK_KIND,
   THREAD_SLICE_TRACK_KIND,
 } from '../core/track_kinds';
 import {
   QueryFlamegraph,
   QueryFlamegraphAttrs,
-  USE_NEW_FLAMEGRAPH_IMPL,
   metricsFromTableOrSubquery,
 } from '../core/query_flamegraph';
 import {DisposableStack} from '../base/disposable_stack';
+import {assertExists} from '../base/logging';
 
 interface View {
   key: string;
@@ -51,9 +47,9 @@
 class AreaDetailsPanel implements m.ClassComponent {
   private readonly monitor = new Monitor([() => globals.state.selection]);
   private currentTab: string | undefined = undefined;
+  private cpuProfileFlamegraphAttrs?: QueryFlamegraphAttrs;
   private perfSampleFlamegraphAttrs?: QueryFlamegraphAttrs;
   private sliceFlamegraphAttrs?: QueryFlamegraphAttrs;
-  private legacyFlamegraphSelection?: FlamegraphSelectionParams;
 
   private getCurrentView(): string | undefined {
     const types = this.getViews().map(({key}) => key);
@@ -87,7 +83,11 @@
     }
 
     const pivotTableState = globals.state.nonSerializableState.pivotTable;
-    if (pivotTableState.selectionArea !== undefined) {
+    const tree = pivotTableState.queryResult?.tree;
+    if (
+      pivotTableState.selectionArea != undefined &&
+      (tree === undefined || tree.children.size > 0 || tree?.rows.length > 0)
+    ) {
       views.push({
         key: 'pivot_table',
         name: 'Pivot Table',
@@ -97,12 +97,7 @@
       });
     }
 
-    const isChanged = this.monitor.ifStateChanged();
-    if (USE_NEW_FLAMEGRAPH_IMPL.get()) {
-      this.addFlamegraphView(isChanged, views);
-    } else {
-      this.addLegacyFlamegraphView(isChanged, views);
-    }
+    this.addFlamegraphView(this.monitor.ifStateChanged(), views);
 
     // Add this after all aggregation panels, to make it appear after 'Slices'
     if (globals.selectedFlows.length > 0) {
@@ -163,6 +158,15 @@
   }
 
   private addFlamegraphView(isChanged: boolean, views: View[]) {
+    this.cpuProfileFlamegraphAttrs =
+      this.computeCpuProfileFlamegraphAttrs(isChanged);
+    if (this.cpuProfileFlamegraphAttrs !== undefined) {
+      views.push({
+        key: 'cpu_profile_flamegraph_selection',
+        name: 'CPU Profile Sample Flamegraph',
+        content: m(QueryFlamegraph, this.cpuProfileFlamegraphAttrs),
+      });
+    }
     this.perfSampleFlamegraphAttrs =
       this.computePerfSampleFlamegraphAttrs(isChanged);
     if (this.perfSampleFlamegraphAttrs !== undefined) {
@@ -182,6 +186,67 @@
     }
   }
 
+  private computeCpuProfileFlamegraphAttrs(isChanged: boolean) {
+    const currentSelection = globals.state.selection;
+    if (currentSelection.kind !== 'area') {
+      return undefined;
+    }
+    if (!isChanged) {
+      // If the selection has not changed, just return a copy of the last seen
+      // attrs.
+      return this.cpuProfileFlamegraphAttrs;
+    }
+    const utids = [];
+    for (const trackUri of currentSelection.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
+      if (trackInfo?.tags?.kind === CPU_PROFILE_TRACK_KIND) {
+        utids.push(trackInfo.tags?.utid);
+      }
+    }
+    if (utids.length === 0) {
+      return undefined;
+    }
+    return {
+      engine: assertExists(this.getCurrentEngine()),
+      metrics: [
+        ...metricsFromTableOrSubquery(
+          `
+            (
+              select
+                id,
+                parent_id as parentId,
+                name,
+                mapping_name,
+                source_file,
+                cast(line_number AS text) as line_number,
+                self_count
+              from _callstacks_for_cpu_profile_stack_samples!((
+                select p.callsite_id
+                from cpu_profile_stack_sample p
+                where p.ts >= ${currentSelection.start}
+                  and p.ts <= ${currentSelection.end}
+                  and p.utid in (${utids.join(',')})
+              ))
+            )
+          `,
+          [
+            {
+              name: 'CPU Profile Samples',
+              unit: '',
+              columnName: 'self_count',
+            },
+          ],
+          'include perfetto module callstacks.stack_profile',
+          [{name: 'mapping_name', displayName: 'Mapping'}],
+          [
+            {name: 'source_file', displayName: 'Source File'},
+            {name: 'line_number', displayName: 'Line Number'},
+          ],
+        ),
+      ],
+    };
+  }
+
   private computePerfSampleFlamegraphAttrs(isChanged: boolean) {
     const currentSelection = globals.state.selection;
     if (currentSelection.kind !== 'area') {
@@ -194,6 +259,9 @@
     }
     const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
     const utids = getUtidsFromPerfSampleAreaSelection(currentSelection);
+    if (utids.length === 0 && upids.length === 0) {
+      return undefined;
+    }
     return {
       engine: assertExists(this.getCurrentEngine()),
       metrics: [
@@ -238,9 +306,8 @@
       return this.sliceFlamegraphAttrs;
     }
     const trackIds = [];
-    for (const trackId of currentSelection.tracks) {
-      const track: TrackState | undefined = globals.state.tracks[trackId];
-      const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
+    for (const trackUri of currentSelection.trackUris) {
+      const trackInfo = globals.trackManager.getTrack(trackUri);
       if (trackInfo?.tags?.kind !== THREAD_SLICE_TRACK_KIND) {
         continue;
       }
@@ -286,44 +353,6 @@
     };
   }
 
-  private addLegacyFlamegraphView(isChanged: boolean, views: View[]) {
-    this.legacyFlamegraphSelection =
-      this.computeLegacyFlamegraphSelection(isChanged);
-    if (this.legacyFlamegraphSelection === undefined) {
-      return;
-    }
-    views.push({
-      key: 'flamegraph_selection',
-      name: 'Flamegraph Selection',
-      content: m(LegacyFlamegraphDetailsPanel, {
-        cache: globals.areaFlamegraphCache,
-        selection: this.legacyFlamegraphSelection,
-      }),
-    });
-  }
-
-  private computeLegacyFlamegraphSelection(isChanged: boolean) {
-    const currentSelection = globals.state.selection;
-    if (currentSelection.kind !== 'area') {
-      return undefined;
-    }
-    if (!isChanged) {
-      // If the selection has not changed, just return a copy of the last seen
-      // selection.
-      return this.legacyFlamegraphSelection;
-    }
-    const upids = getUpidsFromPerfSampleAreaSelection(currentSelection);
-    if (upids.length === 0) {
-      return undefined;
-    }
-    return {
-      profileType: ProfileType.PERF_SAMPLE,
-      start: currentSelection.start,
-      end: currentSelection.end,
-      upids,
-    };
-  }
-
   private getCurrentEngine() {
     const engineId = globals.getCurrentEngine()?.id;
     if (engineId === undefined) return undefined;
@@ -355,9 +384,8 @@
 
 function getUpidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
   const upids = [];
-  for (const trackId of currentSelection.tracks) {
-    const track: TrackState | undefined = globals.state.tracks[trackId];
-    const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
+  for (const trackUri of currentSelection.trackUris) {
+    const trackInfo = globals.trackManager.getTrack(trackUri);
     if (
       trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
       trackInfo.tags?.utid === undefined
@@ -370,9 +398,8 @@
 
 function getUtidsFromPerfSampleAreaSelection(currentSelection: AreaSelection) {
   const utids = [];
-  for (const trackId of currentSelection.tracks) {
-    const track: TrackState | undefined = globals.state.tracks[trackId];
-    const trackInfo = globals.trackManager.resolveTrackInfo(track?.uri);
+  for (const trackUri of currentSelection.trackUris) {
+    const trackInfo = globals.trackManager.getTrack(trackUri);
     if (
       trackInfo?.tags?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND &&
       trackInfo.tags?.utid !== undefined
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index bafeade..63931e2 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -189,7 +189,7 @@
 
 export abstract class BaseCounterTrack implements Track {
   protected engine: Engine;
-  protected trackKey: string;
+  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -249,7 +249,7 @@
 
   constructor(args: BaseCounterTrackArgs) {
     this.engine = args.engine;
-    this.trackKey = args.trackKey;
+    this.uri = args.uri;
     this.defaultOptions = args.options ?? {};
     this.trash = new AsyncDisposableStack();
   }
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 772aa22..eafdeba 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -171,7 +171,7 @@
 {
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
   protected engine: Engine;
-  protected trackKey: string;
+  protected uri: string;
   protected trackUuid = uuidv4Sql();
 
   // This is the over-skirted cached bounds:
@@ -249,7 +249,7 @@
 
   constructor(args: NewTrackArgs) {
     this.engine = args.engine;
-    this.trackKey = args.trackKey;
+    this.uri = args.uri;
     // Work out the extra columns.
     // This is the union of the embedder-defined columns and the base columns
     // we know about (ts, dur, ...).
diff --git a/ui/src/frontend/charts/histogram/state.ts b/ui/src/frontend/charts/histogram/state.ts
index b07b9a9..23cf8a6 100644
--- a/ui/src/frontend/charts/histogram/state.ts
+++ b/ui/src/frontend/charts/histogram/state.ts
@@ -16,7 +16,7 @@
 import {Row} from '../../../trace_processor/query_result';
 
 interface ChartConfig {
-  binAxisType?: 'nominal' | 'quantitative';
+  binAxisType: 'nominal' | 'quantitative';
   binAxis: 'x' | 'y';
   countAxis: 'x' | 'y';
   sort: string;
@@ -24,36 +24,53 @@
   labelLimit?: number;
 }
 
-export class HistogramState {
-  private readonly sqlColumn: string;
-  private readonly engine: Engine;
-  private readonly query: string;
+interface HistogramData {
+  readonly rows: Row[];
+  readonly error?: string;
+  readonly chartConfig: ChartConfig;
+}
 
-  data?: Row[];
-  chartConfig: ChartConfig;
-
-  get isLoading() {
-    return this.data === undefined;
-  }
-
-  constructor(engine: Engine, query: string, column: string) {
-    this.engine = engine;
-    this.query = query;
-    this.sqlColumn = column;
-
-    this.chartConfig = {
+function getHistogramConfig(
+  aggregationType: 'nominal' | 'quantitative',
+): ChartConfig {
+  const labelLimit = 500;
+  if (aggregationType === 'nominal') {
+    return {
+      binAxisType: aggregationType,
+      binAxis: 'y',
+      countAxis: 'x',
+      sort: `{
+        "op": "count",
+        "order": "descending"
+      }`,
+      isBinned: false,
+      labelLimit,
+    };
+  } else {
+    return {
+      binAxisType: aggregationType,
       binAxis: 'x',
-      binAxisType: 'nominal',
       countAxis: 'y',
       sort: 'false',
       isBinned: true,
-      labelLimit: 500,
+      labelLimit,
     };
+  }
+}
 
-    this.getData();
+export class HistogramState {
+  data?: HistogramData;
+
+  constructor(
+    private readonly engine: Engine,
+    private readonly query: string,
+    private readonly sqlColumn: string,
+    private readonly aggregationType?: 'nominal' | 'quantitative',
+  ) {
+    this.loadData();
   }
 
-  async getData() {
+  private async loadData() {
     const res = await this.engine.query(`
       SELECT ${this.sqlColumn}
       FROM (
@@ -63,14 +80,12 @@
 
     const rows: Row[] = [];
 
+    let hasQuantitativeData = false;
+
     for (const it = res.iter({}); it.valid(); it.next()) {
       const rowVal = it.get(this.sqlColumn);
-
-      if (
-        this.chartConfig.binAxisType === 'nominal' &&
-        typeof rowVal === 'bigint'
-      ) {
-        this.chartConfig.binAxisType = 'quantitative';
+      if (typeof rowVal === 'bigint') {
+        hasQuantitativeData = true;
       }
 
       rows.push({
@@ -78,17 +93,17 @@
       });
     }
 
-    this.data = rows;
+    const aggregationType =
+      this.aggregationType !== undefined
+        ? this.aggregationType
+        : hasQuantitativeData
+          ? 'quantitative'
+          : 'nominal';
 
-    if (this.chartConfig.binAxisType === 'nominal') {
-      this.chartConfig.binAxis = 'y';
-      this.chartConfig.countAxis = 'x';
-      this.chartConfig.sort = `{
-          "op": "count",
-          "order": "descending"
-        }`;
-      this.chartConfig.isBinned = false;
-    }
+    this.data = {
+      rows,
+      chartConfig: getHistogramConfig(aggregationType),
+    };
 
     raf.scheduleFullRedraw();
   }
diff --git a/ui/src/frontend/charts/histogram/tab.ts b/ui/src/frontend/charts/histogram/tab.ts
index 329e6f6..f732932 100644
--- a/ui/src/frontend/charts/histogram/tab.ts
+++ b/ui/src/frontend/charts/histogram/tab.ts
@@ -19,6 +19,7 @@
 import {addBottomTab} from '../../../common/addEphemeralTab';
 import {Engine} from '../../../public';
 import {DetailsShell} from '../../../widgets/details_shell';
+import {Spinner} from '../../../widgets/spinner';
 import {VegaView} from '../../../widgets/vega_view';
 import {BottomTab, NewBottomTabArgs} from '../../bottom_tab';
 import {Filter, filterTitle} from '../../widgets/sql/table/column';
@@ -31,6 +32,7 @@
   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 function addHistogramTab(
@@ -58,6 +60,7 @@
       this.engine,
       this.config.query,
       this.config.sqlColumn,
+      this.config.aggregationType,
     );
   }
 
@@ -66,6 +69,10 @@
   }
 
   viewTab() {
+    const data = this.state.data;
+    if (data === undefined) {
+      return m(Spinner);
+    }
     return m(
       DetailsShell,
       {
@@ -80,24 +87,20 @@
               "$schema": "https://vega.github.io/schema/vega-lite/v5.json",
               "mark": "bar",
               "data": {
-                "values": ${
-                  this.state.data
-                    ? stringifyJsonWithBigints(this.state.data)
-                    : []
-                }
+                "values": ${stringifyJsonWithBigints(data.rows)}
               },
               "encoding": {
-                "${this.state.chartConfig.binAxis}": {
-                  "bin": ${this.state.chartConfig.isBinned},
+                "${data.chartConfig.binAxis}": {
+                  "bin": ${data.chartConfig.isBinned},
                   "field": "${this.config.sqlColumn}",
-                  "type": "${this.state.chartConfig.binAxisType}",
+                  "type": "${data.chartConfig.binAxisType}",
                   "title": "${this.config.columnTitle}",
-                  "sort": ${this.state.chartConfig.sort},
+                  "sort": ${data.chartConfig.sort},
                   "axis": {
-                    "labelLimit": ${this.state.chartConfig.labelLimit}
+                    "labelLimit": ${data.chartConfig.labelLimit}
                   }
                 },
-                "${this.state.chartConfig.countAxis}": {
+                "${data.chartConfig.countAxis}": {
                   "aggregate": "count",
                   "title": "Count"
                 }
@@ -112,7 +115,7 @@
 
   getTitle(): string {
     return `${this.toTitleCase(this.config.columnTitle)} ${
-      this.state.chartConfig.binAxisType === 'quantitative'
+      this.state.data?.chartConfig.binAxisType === 'quantitative'
         ? 'Histogram'
         : 'Counts'
     }`;
@@ -140,6 +143,6 @@
   }
 
   isLoading(): boolean {
-    return this.state.isLoading;
+    return this.state.data === undefined;
   }
 }
diff --git a/ui/src/frontend/close_track_button.ts b/ui/src/frontend/close_track_button.ts
deleted file mode 100644
index 1b0fd91..0000000
--- a/ui/src/frontend/close_track_button.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {Icons} from '../base/semantic_icons';
-import {Actions} from '../common/actions';
-
-import {globals} from './globals';
-import {Button} from '../widgets/button';
-
-export interface CloseTrackButtonAttrs {
-  trackKey: string;
-}
-
-export class CloseTrackButton
-  implements m.ClassComponent<CloseTrackButtonAttrs>
-{
-  view({attrs}: m.CVnode<CloseTrackButtonAttrs>) {
-    return m(Button, {
-      onclick: () => {
-        globals.dispatch(Actions.removeTracks({trackKeys: [attrs.trackKey]}));
-      },
-      icon: Icons.Close,
-      title: 'Close',
-      compact: true,
-    });
-  }
-}
diff --git a/ui/src/frontend/cpu_profile_panel.ts b/ui/src/frontend/cpu_profile_panel.ts
deleted file mode 100644
index 0987d50..0000000
--- a/ui/src/frontend/cpu_profile_panel.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright (C) 2020 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-
-import {globals} from './globals';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-
-interface CpuProfileDetailsPanelAttrs {}
-
-export class CpuProfileDetailsPanel
-  implements m.ClassComponent<CpuProfileDetailsPanelAttrs>
-{
-  view() {
-    const sampleDetails = globals.cpuProfileDetails;
-    const header = m(
-      '.details-panel-heading',
-      m('h2', `CPU Profile Sample Details`),
-    );
-    if (sampleDetails.id === undefined) {
-      return m('.details-panel', header);
-    }
-
-    return m(
-      '.details-panel',
-      header,
-      m('table', this.getStackText(sampleDetails.stack)),
-    );
-  }
-
-  getStackText(stack?: CallsiteInfo[]): m.Vnode[] {
-    if (!stack) return [];
-
-    const result = [];
-    for (let i = stack.length - 1; i >= 0; --i) {
-      result.push(m('tr', m('td', stack[i].name), m('td', stack[i].mapping)));
-    }
-
-    return result;
-  }
-}
diff --git a/ui/src/frontend/debug_tracks/counter_track.ts b/ui/src/frontend/debug_tracks/counter_track.ts
index db00e87..a8d55f9 100644
--- a/ui/src/frontend/debug_tracks/counter_track.ts
+++ b/ui/src/frontend/debug_tracks/counter_track.ts
@@ -12,9 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import m from 'mithril';
 import {BaseCounterTrack} from '../../frontend/base_counter_track';
 import {TrackContext} from '../../public';
 import {Engine} from '../../trace_processor/engine';
+import {Button} from '../../widgets/button';
+import {globals} from '../globals';
+import {Icons} from '../../base/semantic_icons';
 
 export class DebugCounterTrack extends BaseCounterTrack {
   private readonly sqlTableName: string;
@@ -22,7 +26,7 @@
   constructor(engine: Engine, ctx: TrackContext, tableName: string) {
     super({
       engine,
-      trackKey: ctx.trackKey,
+      uri: ctx.trackUri,
     });
     this.sqlTableName = tableName;
   }
@@ -30,4 +34,15 @@
   getSqlSource(): string {
     return `select * from ${this.sqlTableName}`;
   }
+
+  getTrackShellButtons(): m.Children {
+    return m(Button, {
+      onclick: () => {
+        globals.workspace.getTrackByUri(this.uri)?.remove();
+      },
+      icon: Icons.Close,
+      title: 'Close',
+      compact: true,
+    });
+  }
 }
diff --git a/ui/src/frontend/debug_tracks/debug_tracks.ts b/ui/src/frontend/debug_tracks/debug_tracks.ts
index a2dbe3b..ee7ef32 100644
--- a/ui/src/frontend/debug_tracks/debug_tracks.ts
+++ b/ui/src/frontend/debug_tracks/debug_tracks.ts
@@ -13,8 +13,6 @@
 // limitations under the License.
 
 import {uuidv4, uuidv4Sql} from '../../base/uuid';
-import {Actions, DeferredAction} from '../../common/actions';
-import {PrimaryTrackSortKey, SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../globals';
 import {TrackDescriptor} from '../../public';
 import {DebugSliceTrack} from './slice_track';
@@ -26,6 +24,8 @@
 import {Engine} from '../../trace_processor/engine';
 import {DebugCounterTrack} from './counter_track';
 import {ARG_PREFIX} from './details_tab';
+import {TrackNode} from '../../public/workspace';
+import {raf} from '../../core/raf_scheduler';
 
 // We need to add debug tracks from the core and from plugins. In order to add a
 // debug track we need to pass a context through with we can add the track. This
@@ -67,26 +67,13 @@
 // have an effect. Use this variant if you want to create many tracks at
 // once or want to tweak the actions once produced. Otherwise, use
 // addDebugSliceTrack().
-function createAddDebugTrackActions(
-  trackName: string,
-  uri: string,
-): DeferredAction<{}>[] {
+function addDebugTrack(trackName: string, uri: string): void {
   const debugTrackId = ++debugTrackCount;
-  const trackKey = uuidv4();
-
-  const actions: DeferredAction<{}>[] = [
-    Actions.addTrack({
-      key: trackKey,
-      name: trackName.trim() || `Debug Track ${debugTrackId}`,
-      uri,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-      closeable: true,
-    }),
-    Actions.toggleTrackPinned({trackKey}),
-  ];
-
-  return actions;
+  const displayName = trackName.trim() || `Debug Track ${debugTrackId}`;
+  const track = new TrackNode(uri, displayName);
+  globals.workspace.prependChild(track);
+  track.pin();
+  raf.scheduleFullRedraw();
 }
 
 export async function addPivotedTracks(
@@ -152,14 +139,11 @@
   ctx.registerTrack({
     uri,
     title: trackName,
-    trackFactory: (trackCtx) => {
-      return new DebugSliceTrack(ctx.engine, trackCtx, tableName);
-    },
+    track: new DebugSliceTrack(ctx.engine, {trackUri: uri}, tableName),
   });
 
   // Create the actions to add this track to the tracklist
-  const actions = await createAddDebugTrackActions(trackName, uri);
-  globals.dispatchMultiple(actions);
+  addDebugTrack(trackName, uri);
 }
 
 function createDebugSliceTrackTableExpr(
@@ -237,14 +221,11 @@
   ctx.registerTrack({
     uri,
     title: trackName,
-    trackFactory: (trackCtx) => {
-      return new DebugCounterTrack(ctx.engine, trackCtx, tableName);
-    },
+    track: new DebugCounterTrack(ctx.engine, {trackUri: uri}, tableName),
   });
 
   // Create the actions to add this track to the tracklist
-  const actions = await createAddDebugTrackActions(trackName, uri);
-  globals.dispatchMultiple(actions);
+  addDebugTrack(trackName, uri);
 }
 
 function createDebugCounterTrackTableExpr(
diff --git a/ui/src/frontend/debug_tracks/slice_track.ts b/ui/src/frontend/debug_tracks/slice_track.ts
index 51458b2..86a44b7 100644
--- a/ui/src/frontend/debug_tracks/slice_track.ts
+++ b/ui/src/frontend/debug_tracks/slice_track.ts
@@ -12,6 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import m from 'mithril';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -20,6 +21,9 @@
 import {TrackContext} from '../../public';
 import {Engine} from '../../trace_processor/engine';
 import {DebugSliceDetailsTab} from './details_tab';
+import {Button} from '../../widgets/button';
+import {Icons} from '../../base/semantic_icons';
+import {globals} from '../globals';
 
 export class DebugSliceTrack extends CustomSqlTableSliceTrack {
   private readonly sqlTableName: string;
@@ -27,7 +31,7 @@
   constructor(engine: Engine, ctx: TrackContext, tableName: string) {
     super({
       engine,
-      trackKey: ctx.trackKey,
+      uri: ctx.trackUri,
     });
     this.sqlTableName = tableName;
   }
@@ -47,4 +51,15 @@
       },
     };
   }
+
+  getTrackShellButtons(): m.Children {
+    return m(Button, {
+      onclick: () => {
+        globals.workspace.getTrackByUri(this.uri)?.remove();
+      },
+      icon: Icons.Close,
+      title: 'Close',
+      compact: true,
+    });
+  }
 }
diff --git a/ui/src/frontend/error_dialog.ts b/ui/src/frontend/error_dialog.ts
index 143a53f..d5bef7e 100644
--- a/ui/src/frontend/error_dialog.ts
+++ b/ui/src/frontend/error_dialog.ts
@@ -75,6 +75,11 @@
     return;
   }
 
+  if (err.message.includes('(ERR:ws)')) {
+    showWebsocketConnectionIssue(err.message);
+    return;
+  }
+
   // This is only for older version of the UI and for ease of tracking across
   // cherry-picks. Newer versions don't have this exception anymore.
   if (err.message.includes('State hash does not match')) {
@@ -443,7 +448,11 @@
 export function showWebsocketConnectionIssue(message: string): void {
   showModal({
     title: 'Unable to connect to the device via websocket',
-    content: m('div', m('span', message), m('br')),
+    content: m(
+      'div',
+      m('div', 'trace_processor_shell --httpd is unreachable or crashed.'),
+      m('pre', message),
+    ),
   });
 }
 
diff --git a/ui/src/frontend/flow_events_panel.ts b/ui/src/frontend/flow_events_panel.ts
index 83e848b..bccbd90 100644
--- a/ui/src/frontend/flow_events_panel.ts
+++ b/ui/src/frontend/flow_events_panel.ts
@@ -65,13 +65,15 @@
     }
 
     const flowClickHandler = (sliceId: number, trackId: number) => {
-      const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-      if (trackKey) {
+      const track = globals.trackManager.findTrack((td) =>
+        td.tags?.trackIds?.includes(trackId),
+      );
+      if (track) {
         globals.setLegacySelection(
           {
             kind: 'SLICE',
             id: sliceId,
-            trackKey,
+            trackUri: track.uri,
             table: 'slice',
           },
           {
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index b62efd9..1e8b3d6 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -15,11 +15,12 @@
 import {ArrowHeadStyle, drawBezierArrow} from '../base/canvas/bezier_arrow';
 import {Size, Vector} from '../base/geom';
 import {Optional} from '../base/utils';
-import {SCROLLING_TRACK_GROUP, TrackState} from '../common/state';
+
 import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
 import {Flow, globals} from './globals';
 import {RenderedPanelInfo} from './panel_container';
 import {PxSpan, TimeScale} from './time_scale';
+import {TrackNode} from '../public/workspace';
 
 const TRACK_GROUP_CONNECTION_OFFSET = 5;
 const TRIANGLE_SIZE = 5;
@@ -63,10 +64,23 @@
 
   // Create indexes for the tracks and groups by key for quick access
   const trackPanelsByKey = new Map(
-    panels.map((panel) => [panel.panel.trackKey, panel]),
+    panels.map((panel) => [panel.panel.trackUri, panel]),
   );
   const groupPanelsByKey = new Map(
-    panels.map((panel) => [panel.panel.groupKey, panel]),
+    panels.map((panel) => [panel.panel.groupUri, panel]),
+  );
+
+  // Build a track index on trackIds. Note: We need to find the track nodes
+  // specifically here (not just the URIs) because we might need to navigate up
+  // the tree to find containing groups.
+
+  const trackIdToTrack = new Map<number, TrackNode>();
+  globals.workspace.flatTracks.forEach((track) =>
+    globals.trackManager
+      .getTrack(track.uri)
+      ?.tags?.trackIds?.forEach((trackId) =>
+        trackIdToTrack.set(trackId, track),
+      ),
   );
 
   const drawFlow = (flow: Flow, hue: number) => {
@@ -122,12 +136,12 @@
     depth: number,
     x: number,
   ): Optional<VerticalEdgeOrPoint> => {
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    if (!trackKey) {
+    const track = trackIdToTrack.get(trackId);
+    if (!track) {
       return undefined;
     }
 
-    const trackPanel = trackPanelsByKey.get(trackKey);
+    const trackPanel = trackPanelsByKey.get(track.uri);
     if (trackPanel) {
       const trackRect = trackPanel.rect;
       const sliceRectRaw = trackPanel.panel.getSliceVerticalBounds?.(depth);
@@ -151,21 +165,15 @@
         };
       }
     } else {
-      // If we didn't find a panel for this track, it might inside a group, so
-      // try to place the target on the group instead
-      const trackState = globals.state.tracks[trackKey] as Optional<TrackState>;
-      if (trackState) {
-        if (trackState.trackGroup !== SCROLLING_TRACK_GROUP) {
-          const groupKey = trackState.trackGroup;
-          const groupPanel = groupPanelsByKey.get(groupKey);
-          if (groupPanel) {
-            return {
-              kind: 'point',
-              x,
-              y: groupPanel.rect.bottom - TRACK_GROUP_CONNECTION_OFFSET,
-            };
-          }
-        }
+      // If we didn't find a track, it might inside a group, so check for the group
+      const group = track.closestVisibleAncestor;
+      const groupPanel = group && groupPanelsByKey.get(group.uri);
+      if (groupPanel) {
+        return {
+          kind: 'point',
+          x,
+          y: groupPanel.rect.bottom - TRACK_GROUP_CONNECTION_OFFSET,
+        };
       }
     }
 
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 94da791..b889596 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -29,7 +29,7 @@
 import {EngineConfig, State, getLegacySelection} from '../common/state';
 import {TabManager} from '../common/tab_registry';
 import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
-import {TrackManager} from '../common/track_cache';
+import {TrackManager} from '../common/track_manager';
 import {setPerfHooks} from '../core/perf';
 import {raf} from '../core/raf_scheduler';
 import {ServiceWorkerController} from './service_worker_controller';
@@ -41,8 +41,6 @@
 import {SelectionManager, LegacySelection} from '../core/selection_manager';
 import {Optional, exists} from '../base/utils';
 import {OmniboxManager} from './omnibox_manager';
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
 import {SerializedAppState} from '../common/state_serialization_schema';
 import {getServingRoot} from '../base/http_utils';
 import {
@@ -53,6 +51,7 @@
 import {TraceContext} from './trace_context';
 import {Registry} from '../base/registry';
 import {SidebarMenuItem} from '../public';
+import {Workspace} from '../public/workspace';
 
 const INSTANT_FOCUS_DURATION = 1n;
 const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -133,13 +132,6 @@
   dur?: duration;
 }
 
-export interface CpuProfileDetails {
-  id?: number;
-  ts?: time;
-  utid?: number;
-  stack?: CallsiteInfo[];
-}
-
 export interface QuantizedLoad {
   start: time;
   end: time;
@@ -196,6 +188,8 @@
   readonly modules: SqlModule[];
 }
 
+const DEFAULT_WORKSPACE_NAME = 'Default Workspace';
+
 /**
  * Global accessors for state/dispatch in the frontend.
  */
@@ -221,7 +215,6 @@
   private _connectedFlows?: Flow[] = undefined;
   private _selectedFlows?: Flow[] = undefined;
   private _visibleFlowCategories?: Map<string, boolean> = undefined;
-  private _cpuProfileDetails?: CpuProfileDetails = undefined;
   private _numQueriesQueued = 0;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
@@ -233,15 +226,16 @@
   private _hideSidebar?: boolean = undefined;
   private _cmdManager = new CommandManager();
   private _tabManager = new TabManager();
-  private _trackManager = new TrackManager(this._store);
+  private _trackManager = new TrackManager();
   private _selectionManager = new SelectionManager(this._store);
   private _hasFtrace: boolean = false;
   private _searchOverviewTrack?: SearchOverviewTrack;
+  readonly workspaces: Workspace[] = [];
+  private _currentWorkspace: Workspace;
 
   omnibox = new OmniboxManager();
-  areaFlamegraphCache = new LegacyFlamegraphCache('area');
 
-  scrollToTrackKey?: string | number;
+  scrollToTrackUri?: string;
   httpRpcState: HttpRpcState = {connected: false};
   showPanningHint = false;
   permalinkHash?: string;
@@ -252,6 +246,21 @@
 
   readonly sidebarMenuItems = new Registry<SidebarMenuItem>((m) => m.commandId);
 
+  get workspace(): Workspace {
+    return this._currentWorkspace;
+  }
+
+  resetWorkspaces(): void {
+    this.workspaces.length = 0;
+    const defaultWorkspace = new Workspace(DEFAULT_WORKSPACE_NAME);
+    this.workspaces.push(defaultWorkspace);
+    this._currentWorkspace = defaultWorkspace;
+  }
+
+  switchWorkspace(workspace: Workspace): void {
+    this._currentWorkspace = workspace;
+  }
+
   // This is the app's equivalent of a plugin's onTraceLoad() function.
   // TODO(stevegolton): Eventually initialization that should be done on trace
   // load should be moved into here, and then we can remove TraceController
@@ -282,6 +291,10 @@
     // Alternatively we could decide that we don't want to support switching
     // traces at all, in which case we can ignore tear down entirely.
     this._searchOverviewTrack = await createSearchOverviewTrack(engine, this);
+
+    // Reset the trackManager - this clears out the cache and any registered
+    // tracks
+    this._trackManager = new TrackManager();
   }
 
   // Used for permalink load by trace_controller.ts.
@@ -294,7 +307,7 @@
     eventIds: new Float64Array(0),
     tses: new BigInt64Array(0),
     utids: new Float64Array(0),
-    trackKeys: [],
+    trackUris: [],
     sources: [],
     totalResults: 0,
   };
@@ -304,6 +317,10 @@
   constructor() {
     const {start, end} = defaultTraceContext;
     this._timeline = new Timeline(this._store, new TimeSpan(start, end));
+
+    const defaultWorkspace = new Workspace(DEFAULT_WORKSPACE_NAME);
+    this.workspaces.push(defaultWorkspace);
+    this._currentWorkspace = defaultWorkspace;
   }
 
   initialize(
@@ -343,7 +360,6 @@
     this._selectedFlows = [];
     this._visibleFlowCategories = new Map<string, boolean>();
     this._threadStateDetails = {};
-    this._cpuProfileDetails = {};
     this.engines.clear();
     this._selectionManager.clear();
   }
@@ -474,14 +490,6 @@
     this._metricResult = result;
   }
 
-  get cpuProfileDetails() {
-    return assertExists(this._cpuProfileDetails);
-  }
-
-  set cpuProfileDetails(click: CpuProfileDetails) {
-    this._cpuProfileDetails = assertExists(click);
-  }
-
   set numQueuedQueries(value: number) {
     this._numQueriesQueued = value;
   }
@@ -596,11 +604,11 @@
   }
 
   selectSingleEvent(
-    trackKey: string,
+    trackUri: string,
     eventId: number,
     args: Partial<LegacySelectionArgs> = {},
   ): void {
-    this._selectionManager.setEvent(trackKey, eventId);
+    this._selectionManager.setEvent(trackUri, eventId);
     this.handleSelectionArgs(args);
   }
 
@@ -649,7 +657,7 @@
       eventIds: new Float64Array(0),
       tses: new BigInt64Array(0),
       utids: new Float64Array(0),
-      trackKeys: [],
+      trackUris: [],
       sources: [],
       totalResults: 0,
     };
@@ -750,17 +758,15 @@
         }
       }
     } else if (sel.kind === 'single') {
-      const uri = globals.state.tracks[sel.trackKey]?.uri;
-      if (uri) {
-        const bounds = await globals.trackManager
-          .resolveTrackInfo(uri)
-          ?.getEventBounds?.(sel.eventId);
-        if (bounds) {
-          return {
-            start: bounds.ts,
-            end: Time.add(bounds.ts, bounds.dur),
-          };
-        }
+      const uri = sel.trackUri;
+      const bounds = await globals.trackManager
+        .getTrack(uri)
+        ?.getEventBounds?.(sel.eventId);
+      if (bounds) {
+        return {
+          start: bounds.ts,
+          end: Time.add(bounds.ts, bounds.dur),
+        };
       }
       return undefined;
     }
diff --git a/ui/src/frontend/index.ts b/ui/src/frontend/index.ts
index e331eee..d61b5a6 100644
--- a/ui/src/frontend/index.ts
+++ b/ui/src/frontend/index.ts
@@ -36,7 +36,7 @@
 import {initWasm} from '../trace_processor/wasm_engine_proxy';
 import {setScheduleFullRedraw} from '../widgets/raf';
 
-import {App} from './app';
+import {UiMain} from './ui_main';
 import {initCssConstants} from './css_constants';
 import {registerDebugGlobals} from './debug';
 import {maybeShowErrorDialog} from './error_dialog';
@@ -289,7 +289,7 @@
   router.onRouteChanged = routeChange;
 
   raf.domRedraw = () => {
-    m.render(document.body, m(App, router.resolve()));
+    m.render(document.body, m(UiMain, router.resolve()));
   };
 
   if (
@@ -356,7 +356,7 @@
   });
 
   // Force one initial render to get everything in place
-  m.render(document.body, m(App, router.resolve()));
+  m.render(document.body, m(UiMain, router.resolve()));
 
   // Initialize plugins, now that we are ready to go
   pluginManager.initialize();
diff --git a/ui/src/frontend/keyboard_event_handler.ts b/ui/src/frontend/keyboard_event_handler.ts
index 53d3352..38b7c70 100644
--- a/ui/src/frontend/keyboard_event_handler.ts
+++ b/ui/src/frontend/keyboard_event_handler.ts
@@ -96,14 +96,17 @@
   for (const flow of globals.connectedFlows) {
     if (flow.id === flowId) {
       const flowPoint = direction === 'Backward' ? flow.begin : flow.end;
-      const trackKeyByTrackId = globals.trackManager.trackKeyByTrackId;
-      const trackKey = trackKeyByTrackId.get(flowPoint.trackId);
-      if (trackKey) {
+      const track = globals.workspace.flatTracks.find((t) => {
+        return globals.trackManager
+          .getTrack(t.uri)
+          ?.tags?.trackIds?.includes(flowPoint.trackId);
+      });
+      if (track) {
         globals.setLegacySelection(
           {
             kind: 'SLICE',
             id: flowPoint.sliceId,
-            trackKey,
+            trackUri: track.uri,
             table: 'slice',
           },
           {
@@ -126,7 +129,7 @@
     focusHorizontalRange(range.start, range.end);
   }
 
-  if (selection.trackKey) {
-    verticalScrollToTrack(selection.trackKey, true);
+  if (selection.trackUri) {
+    verticalScrollToTrack(selection.trackUri, true);
   }
 }
diff --git a/ui/src/frontend/legacy_flamegraph.ts b/ui/src/frontend/legacy_flamegraph.ts
deleted file mode 100644
index caa5e3e..0000000
--- a/ui/src/frontend/legacy_flamegraph.ts
+++ /dev/null
@@ -1,489 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {CallsiteInfo} from '../common/legacy_flamegraph_util';
-import {searchSegment} from '../base/binary_search';
-import {cropText} from '../base/string_utils';
-
-interface Node {
-  width: number;
-  x: number;
-  nextXForChildren: number;
-  size: number;
-}
-
-interface CallsiteInfoWidth {
-  callsite: CallsiteInfo;
-  width: number;
-}
-
-// Height of one 'row' on the flame chart including 1px of whitespace
-// below the box.
-const NODE_HEIGHT = 18;
-
-export const FLAMEGRAPH_HOVERED_COLOR = 'hsl(224, 45%, 55%)';
-
-export function findRootSize(data: ReadonlyArray<CallsiteInfo>) {
-  let totalSize = 0;
-  let i = 0;
-  while (i < data.length && data[i].depth === 0) {
-    totalSize += data[i].totalSize;
-    i++;
-  }
-  return totalSize;
-}
-
-export interface NodeRendering {
-  totalSize?: string;
-  selfSize?: string;
-}
-
-export class Flamegraph {
-  private nodeRendering: NodeRendering = {};
-  private flamegraphData: ReadonlyArray<CallsiteInfo>;
-  private highlightSomeNodes = false;
-  private maxDepth = -1;
-  private totalSize = -1;
-  // Initialised on first draw() call
-  private labelCharWidth = 0;
-  private labelFontStyle = '12px Roboto Mono';
-  private rolloverFontStyle = '12px Roboto Condensed';
-  // Key for the map is depth followed by x coordinate - `depth;x`
-  private graphData: Map<string, CallsiteInfoWidth> = new Map();
-  private xStartsPerDepth: Map<number, number[]> = new Map();
-
-  private hoveredX = -1;
-  private hoveredY = -1;
-  private hoveredCallsite?: CallsiteInfo;
-  private clickedCallsite?: CallsiteInfo;
-
-  private startingY = 0;
-
-  constructor(flamegraphData: CallsiteInfo[]) {
-    this.flamegraphData = flamegraphData;
-    this.findMaxDepth();
-  }
-
-  private findMaxDepth() {
-    this.maxDepth = Math.max(
-      ...this.flamegraphData.map((value) => value.depth),
-    );
-  }
-
-  // Instead of highlighting the interesting nodes, we actually want to
-  // de-emphasize the non-highlighted nodes. Returns true if there
-  // are any highlighted nodes in the flamegraph.
-  private highlightingExists() {
-    this.highlightSomeNodes = this.flamegraphData.some((e) => e.highlighted);
-  }
-
-  generateColor(
-    name: string,
-    isGreyedOut = false,
-    highlighted: boolean,
-  ): string {
-    if (isGreyedOut) {
-      return '#d9d9d9';
-    }
-    if (name === 'unknown' || name === 'root') {
-      return '#c0c0c0';
-    }
-    let x = 0;
-    for (let i = 0; i < name.length; i += 1) {
-      x += name.charCodeAt(i) % 64;
-    }
-    x = x % 360;
-    let l = '76';
-    // Make non-highlighted node lighter.
-    if (this.highlightSomeNodes && !highlighted) {
-      l = '90';
-    }
-    return `hsl(${x}deg, 45%, ${l}%)`;
-  }
-
-  // Caller will have to call draw method after updating data to have updated
-  // graph.
-  updateDataIfChanged(
-    nodeRendering: NodeRendering,
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    clickedCallsite?: CallsiteInfo,
-  ) {
-    this.nodeRendering = nodeRendering;
-    this.clickedCallsite = clickedCallsite;
-    if (this.flamegraphData === flamegraphData) {
-      return;
-    }
-    this.flamegraphData = flamegraphData;
-    this.clickedCallsite = clickedCallsite;
-    this.findMaxDepth();
-    this.highlightingExists();
-    // Finding total size of roots.
-    this.totalSize = findRootSize(flamegraphData);
-  }
-
-  draw(
-    ctx: CanvasRenderingContext2D,
-    width: number,
-    height: number,
-    x = 0,
-    y = 0,
-    unit = 'B',
-  ) {
-    if (this.flamegraphData === undefined) {
-      return;
-    }
-
-    ctx.font = this.labelFontStyle;
-    ctx.textBaseline = 'middle';
-    if (this.labelCharWidth === 0) {
-      this.labelCharWidth = ctx.measureText('_').width;
-    }
-
-    this.startingY = y;
-
-    // For each node, we use this map to get information about its parent
-    // (total size of it, width and where it starts in graph) so we can
-    // calculate it's own position in graph.
-    const nodesMap = new Map<number, Node>();
-    let currentY = y;
-    nodesMap.set(-1, {width, nextXForChildren: x, size: this.totalSize, x});
-
-    // Initialize data needed for click/hover behavior.
-    this.graphData = new Map();
-    this.xStartsPerDepth = new Map();
-
-    // Draw root node.
-    ctx.fillStyle = this.generateColor('root', false, false);
-    ctx.fillRect(x, currentY, width, NODE_HEIGHT - 1);
-    const text = cropText(
-      `root: ${this.displaySize(
-        this.totalSize,
-        unit,
-        unit === 'B' ? 1024 : 1000,
-      )}`,
-      this.labelCharWidth,
-      width - 2,
-    );
-    ctx.fillStyle = 'black';
-    ctx.fillText(text, x + 5, currentY + (NODE_HEIGHT - 1) / 2);
-    currentY += NODE_HEIGHT;
-
-    // Set style for borders.
-    ctx.strokeStyle = 'white';
-    ctx.lineWidth = 0.5;
-
-    for (let i = 0; i < this.flamegraphData.length; i++) {
-      if (currentY > height) {
-        break;
-      }
-      const value = this.flamegraphData[i];
-      const parentNode = nodesMap.get(value.parentId);
-      if (parentNode === undefined) {
-        continue;
-      }
-
-      const isClicked = this.clickedCallsite !== undefined;
-      const isFullWidth =
-        isClicked && value.depth <= this.clickedCallsite!.depth;
-      const isGreyedOut =
-        isClicked && value.depth < this.clickedCallsite!.depth;
-
-      const parent = value.parentId;
-      const parentSize = parent === -1 ? this.totalSize : parentNode.size;
-      // Calculate node's width based on its proportion in parent.
-      const width =
-        (isFullWidth ? 1 : value.totalSize / parentSize) * parentNode.width;
-
-      const currentX = parentNode.nextXForChildren;
-      currentY = y + NODE_HEIGHT * (value.depth + 1);
-
-      // Draw node.
-      const name = this.getCallsiteName(value);
-      ctx.fillStyle = this.generateColor(name, isGreyedOut, value.highlighted);
-      ctx.fillRect(currentX, currentY, width, NODE_HEIGHT - 1);
-
-      // Set current node's data in map for children to use.
-      nodesMap.set(value.id, {
-        width,
-        nextXForChildren: currentX,
-        size: value.totalSize,
-        x: currentX,
-      });
-      // Update next x coordinate in parent.
-      nodesMap.set(value.parentId, {
-        width: parentNode.width,
-        nextXForChildren: currentX + width,
-        size: parentNode.size,
-        x: parentNode.x,
-      });
-
-      // Draw name.
-      const labelPaddingPx = 5;
-      const maxLabelWidth = width - labelPaddingPx * 2;
-      let text = cropText(name, this.labelCharWidth, maxLabelWidth);
-      // If cropped text and the original text are within 20% we keep the
-      // original text and just squish it a bit.
-      if (text.length * 1.2 > name.length) {
-        text = name;
-      }
-      ctx.fillStyle = 'black';
-      ctx.fillText(
-        text,
-        currentX + labelPaddingPx,
-        currentY + (NODE_HEIGHT - 1) / 2,
-        maxLabelWidth,
-      );
-
-      // Draw border on the right of node.
-      ctx.beginPath();
-      ctx.moveTo(currentX + width, currentY);
-      ctx.lineTo(currentX + width, currentY + NODE_HEIGHT);
-      ctx.stroke();
-
-      // Add this node for recognizing in click/hover.
-      // Map graphData contains one callsite which is on that depth and X
-      // start. Map xStartsPerDepth for each depth contains all X start
-      // coordinates that callsites on that level have.
-      this.graphData.set(`${value.depth};${currentX}`, {
-        callsite: value,
-        width,
-      });
-      const xStarts = this.xStartsPerDepth.get(value.depth);
-      if (xStarts === undefined) {
-        this.xStartsPerDepth.set(value.depth, [currentX]);
-      } else {
-        xStarts.push(currentX);
-      }
-    }
-
-    // Draw the tooltip.
-    if (this.hoveredX > -1 && this.hoveredY > -1 && this.hoveredCallsite) {
-      // Must set these before measureText below.
-      ctx.font = this.rolloverFontStyle;
-      ctx.textBaseline = 'top';
-
-      // Size in px of the border around the text and the edge of the rollover
-      // background.
-      const paddingPx = 8;
-      // Size in px of the x and y offset between the mouse and the top left
-      // corner of the rollover box.
-      const offsetPx = 4;
-
-      const lines: string[] = [];
-
-      let textWidth = this.addToTooltip(
-        this.getCallsiteName(this.hoveredCallsite),
-        width - paddingPx,
-        ctx,
-        lines,
-      );
-      if (this.hoveredCallsite.location != null) {
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(this.hoveredCallsite.location, width, ctx, lines),
-        );
-      }
-      textWidth = Math.max(
-        textWidth,
-        this.addToTooltip(this.hoveredCallsite.mapping, width, ctx, lines),
-      );
-
-      if (this.nodeRendering.totalSize !== undefined) {
-        const percentage =
-          (this.hoveredCallsite.totalSize / this.totalSize) * 100;
-        const totalSizeText = `${
-          this.nodeRendering.totalSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.totalSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${percentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(totalSizeText, width, ctx, lines),
-        );
-      }
-
-      if (
-        this.nodeRendering.selfSize !== undefined &&
-        this.hoveredCallsite.selfSize > 0
-      ) {
-        const selfPercentage =
-          (this.hoveredCallsite.selfSize / this.totalSize) * 100;
-        const selfSizeText = `${
-          this.nodeRendering.selfSize
-        }: ${this.displaySize(
-          this.hoveredCallsite.selfSize,
-          unit,
-          unit === 'B' ? 1024 : 1000,
-        )} (${selfPercentage.toFixed(2)}%)`;
-        textWidth = Math.max(
-          textWidth,
-          this.addToTooltip(selfSizeText, width, ctx, lines),
-        );
-      }
-
-      // Compute a line height as the bounding box height + 50%:
-      const heightSample = ctx.measureText(
-        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ',
-      );
-      const lineHeight = Math.round(
-        heightSample.actualBoundingBoxDescent * 1.5,
-      );
-
-      const rectWidth = textWidth + 2 * paddingPx;
-      const rectHeight = lineHeight * lines.length + 2 * paddingPx;
-
-      let rectXStart = this.hoveredX + offsetPx;
-      let rectYStart = this.hoveredY + offsetPx;
-
-      if (rectXStart + rectWidth > width) {
-        rectXStart = width - rectWidth;
-      }
-
-      if (rectYStart + rectHeight > height) {
-        rectYStart = height - rectHeight;
-      }
-
-      ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
-      ctx.fillRect(rectXStart, rectYStart, rectWidth, rectHeight);
-      ctx.fillStyle = 'hsl(200, 50%, 40%)';
-      ctx.textAlign = 'left';
-      for (let i = 0; i < lines.length; i++) {
-        const line = lines[i];
-        ctx.fillText(
-          line,
-          rectXStart + paddingPx,
-          rectYStart + paddingPx + i * lineHeight,
-        );
-      }
-    }
-  }
-
-  private addToTooltip(
-    text: string,
-    width: number,
-    ctx: CanvasRenderingContext2D,
-    lines: string[],
-  ): number {
-    const lineSplitter: LineSplitter = splitIfTooBig(
-      text,
-      width,
-      ctx.measureText(text).width,
-    );
-    lines.push(...lineSplitter.lines);
-    return lineSplitter.lineWidth;
-  }
-
-  private getCallsiteName(value: CallsiteInfo): string {
-    return value.name === undefined || value.name === ''
-      ? 'unknown'
-      : value.name;
-  }
-
-  private displaySize(totalSize: number, unit: string, step = 1024): string {
-    if (unit === '') return totalSize.toLocaleString();
-    if (totalSize === 0) return `0 ${unit}`;
-    const units = [
-      ['', 1],
-      ['K', step],
-      ['M', Math.pow(step, 2)],
-      ['G', Math.pow(step, 3)],
-    ];
-    let unitsIndex = Math.trunc(Math.log(totalSize) / Math.log(step));
-    unitsIndex = unitsIndex > units.length - 1 ? units.length - 1 : unitsIndex;
-    const result = totalSize / +units[unitsIndex][1];
-    const resultString =
-      totalSize % +units[unitsIndex][1] === 0
-        ? result.toString()
-        : result.toFixed(2);
-    return `${resultString} ${units[unitsIndex][0]}${unit}`;
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    this.hoveredX = x;
-    this.hoveredY = y;
-    this.hoveredCallsite = this.findSelectedCallsite(x, y);
-    const isCallsiteSelected = this.hoveredCallsite !== undefined;
-    if (!isCallsiteSelected) {
-      this.onMouseOut();
-    }
-    return isCallsiteSelected;
-  }
-
-  onMouseOut() {
-    this.hoveredX = -1;
-    this.hoveredY = -1;
-    this.hoveredCallsite = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}): CallsiteInfo | undefined {
-    const clickedCallsite = this.findSelectedCallsite(x, y);
-    // TODO(b/148596659): Allow to expand [merged] callsites. Currently,
-    // this expands to the biggest of the nodes that were merged, which
-    // is confusing, so we disallow clicking on them.
-    if (clickedCallsite === undefined || clickedCallsite.merged) {
-      return undefined;
-    }
-    return clickedCallsite;
-  }
-
-  private findSelectedCallsite(x: number, y: number): CallsiteInfo | undefined {
-    const depth = Math.trunc((y - this.startingY) / NODE_HEIGHT) - 1; // at 0 is root
-    if (depth >= 0 && this.xStartsPerDepth.has(depth)) {
-      const startX = this.searchSmallest(this.xStartsPerDepth.get(depth)!, x);
-      const result = this.graphData.get(`${depth};${startX}`);
-      if (result !== undefined) {
-        const width = result.width;
-        return startX + width >= x ? result.callsite : undefined;
-      }
-    }
-    return undefined;
-  }
-
-  searchSmallest(haystack: number[], needle: number): number {
-    haystack = haystack.sort((n1, n2) => n1 - n2);
-    const [left] = searchSegment(haystack, needle);
-    return left === -1 ? -1 : haystack[left];
-  }
-
-  getHeight(): number {
-    return this.flamegraphData.length === 0
-      ? 0
-      : (this.maxDepth + 2) * NODE_HEIGHT;
-  }
-}
-
-export interface LineSplitter {
-  lineWidth: number;
-  lines: string[];
-}
-
-export function splitIfTooBig(
-  line: string,
-  width: number,
-  lineWidth: number,
-): LineSplitter {
-  if (line === '') return {lineWidth, lines: []};
-  const lines: string[] = [];
-  const charWidth = lineWidth / line.length;
-  const maxWidth = width - 32;
-  const maxLineLen = Math.trunc(maxWidth / charWidth);
-  while (line.length > 0) {
-    lines.push(line.slice(0, maxLineLen));
-    line = line.slice(maxLineLen);
-  }
-  lineWidth = Math.min(maxLineLen * charWidth, lineWidth);
-  return {lineWidth, lines};
-}
diff --git a/ui/src/frontend/legacy_flamegraph_panel.ts b/ui/src/frontend/legacy_flamegraph_panel.ts
deleted file mode 100644
index 9a476b8..0000000
--- a/ui/src/frontend/legacy_flamegraph_panel.ts
+++ /dev/null
@@ -1,902 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use size file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m, {Vnode} from 'mithril';
-
-import {findRef} from '../base/dom_utils';
-import {assertExists, assertTrue} from '../base/logging';
-import {time} from '../base/time';
-import {Actions} from '../common/actions';
-import {
-  CallsiteInfo,
-  FlamegraphViewingOption,
-  defaultViewingOption,
-  expandCallsites,
-  findRootSize,
-  mergeCallsites,
-  viewingOptions,
-} from '../common/legacy_flamegraph_util';
-import {ProfileType} from '../common/state';
-import {raf} from '../core/raf_scheduler';
-import {Button} from '../widgets/button';
-import {Icon} from '../widgets/icon';
-import {Modal, ModalAttrs} from '../widgets/modal';
-import {Popup} from '../widgets/popup';
-import {EmptyState} from '../widgets/empty_state';
-import {Spinner} from '../widgets/spinner';
-
-import {Flamegraph, NodeRendering} from './legacy_flamegraph';
-import {globals} from './globals';
-import {debounce} from './rate_limiters';
-import {Router} from './router';
-import {ButtonBar} from '../widgets/button';
-import {DurationWidget} from './widgets/duration';
-import {DetailsShell} from '../widgets/details_shell';
-import {Intent} from '../widgets/common';
-import {Engine, NUM, STR} from '../public';
-import {Monitor} from '../base/monitor';
-import {arrayEquals} from '../base/array_utils';
-import {getCurrentTrace} from './sidebar';
-import {convertTraceToPprofAndDownload} from './trace_converter';
-import {AsyncLimiter} from '../base/async_limiter';
-import {LegacyFlamegraphCache} from '../core/legacy_flamegraph_cache';
-
-const HEADER_HEIGHT = 30;
-
-export function profileType(s: string): ProfileType {
-  if (isProfileType(s)) {
-    return s;
-  }
-  if (s.startsWith('heap_profile')) {
-    return ProfileType.HEAP_PROFILE;
-  }
-  throw new Error('Unknown type ${s}');
-}
-
-function isProfileType(s: string): s is ProfileType {
-  return Object.values(ProfileType).includes(s as ProfileType);
-}
-
-function getFlamegraphType(type: ProfileType) {
-  switch (type) {
-    case ProfileType.HEAP_PROFILE:
-    case ProfileType.MIXED_HEAP_PROFILE:
-    case ProfileType.NATIVE_HEAP_PROFILE:
-    case ProfileType.JAVA_HEAP_SAMPLES:
-      return 'native';
-    case ProfileType.JAVA_HEAP_GRAPH:
-      return 'graph';
-    case ProfileType.PERF_SAMPLE:
-      return 'perf';
-    default:
-      const exhaustiveCheck: never = type;
-      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-  }
-}
-
-const HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS = [
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY,
-  FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY,
-] as const;
-
-export type HeapGraphDominatorTreeViewingOption =
-  (typeof HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS)[number];
-
-export function isHeapGraphDominatorTreeViewingOption(
-  option: FlamegraphViewingOption,
-): option is HeapGraphDominatorTreeViewingOption {
-  return (
-    HEAP_GRAPH_DOMINATOR_TREE_VIEWING_OPTIONS as readonly FlamegraphViewingOption[]
-  ).includes(option);
-}
-
-const MIN_PIXEL_DISPLAYED = 1;
-
-function toSelectedCallsite(c: CallsiteInfo | undefined): string {
-  if (c !== undefined && c.name !== undefined) {
-    return c.name;
-  }
-  return '(none)';
-}
-
-const RENDER_SELF_AND_TOTAL: NodeRendering = {
-  selfSize: 'Self',
-  totalSize: 'Total',
-};
-const RENDER_OBJ_COUNT: NodeRendering = {
-  selfSize: 'Self objects',
-  totalSize: 'Subtree objects',
-};
-
-export interface FlamegraphSelectionParams {
-  readonly profileType: ProfileType;
-  readonly upids: number[];
-  readonly start: time;
-  readonly end: time;
-}
-
-interface FlamegraphDetailsPanelAttrs {
-  cache: LegacyFlamegraphCache;
-  selection: FlamegraphSelectionParams;
-}
-
-interface FlamegraphResult {
-  queryResults: ReadonlyArray<CallsiteInfo>;
-  incomplete: boolean;
-  renderResults?: ReadonlyArray<CallsiteInfo>;
-}
-
-interface FlamegraphState {
-  selection: FlamegraphSelectionParams;
-  viewingOption: FlamegraphViewingOption;
-  focusRegex: string;
-  result?: FlamegraphResult;
-  selectedCallsites: Readonly<{
-    [key: string]: CallsiteInfo | undefined;
-  }>;
-}
-
-export class LegacyFlamegraphDetailsPanel
-  implements m.ClassComponent<FlamegraphDetailsPanelAttrs>
-{
-  private undebouncedFocusRegex = '';
-  private updateFocusRegexDebounced = debounce(() => {
-    if (this.state === undefined) {
-      return;
-    }
-    this.state.focusRegex = this.undebouncedFocusRegex;
-    raf.scheduleFullRedraw();
-  }, 20);
-
-  private flamegraph: Flamegraph = new Flamegraph([]);
-  private queryLimiter = new AsyncLimiter();
-
-  private state?: FlamegraphState;
-  private queryMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-    () => this.state?.viewingOption,
-  ]);
-  private selectedCallsitesMonitor = new Monitor([
-    () => this.state?.selection,
-    () => this.state?.focusRegex,
-  ]);
-  private renderResultMonitor = new Monitor([
-    () => this.state?.result?.queryResults,
-    () => this.state?.selectedCallsites,
-  ]);
-
-  view({attrs}: Vnode<FlamegraphDetailsPanelAttrs>) {
-    if (attrs.selection === undefined) {
-      this.state = undefined;
-    } else if (
-      attrs.selection.profileType !== this.state?.selection.profileType ||
-      attrs.selection.start !== this.state.selection.start ||
-      attrs.selection.end !== this.state.selection.end ||
-      !arrayEquals(attrs.selection.upids, this.state.selection.upids)
-    ) {
-      this.state = {
-        selection: attrs.selection,
-        focusRegex: '',
-        viewingOption: defaultViewingOption(attrs.selection.profileType),
-        selectedCallsites: {},
-      };
-    }
-    if (this.state === undefined) {
-      return m(
-        '.details-panel',
-        m('.details-panel-heading', m('h2', `Flamegraph Profile`)),
-      );
-    }
-
-    if (this.queryMonitor.ifStateChanged()) {
-      this.state.result = undefined;
-      const state = this.state;
-      this.queryLimiter.schedule(() => {
-        return LegacyFlamegraphDetailsPanel.fetchQueryResults(
-          assertExists(this.getCurrentEngine()),
-          attrs.cache,
-          state,
-        );
-      });
-    }
-
-    if (this.selectedCallsitesMonitor.ifStateChanged()) {
-      this.state.selectedCallsites = {};
-    }
-
-    if (
-      this.renderResultMonitor.ifStateChanged() &&
-      this.state.result !== undefined
-    ) {
-      const selected = this.state.selectedCallsites[this.state.viewingOption];
-      const expanded = expandCallsites(
-        this.state.result.queryResults,
-        selected?.id ?? -1,
-      );
-      this.state.result.renderResults = mergeCallsites(
-        expanded,
-        LegacyFlamegraphDetailsPanel.getMinSizeDisplayed(
-          expanded,
-          selected?.totalSize,
-        ),
-      );
-    }
-
-    let height: number | undefined;
-    if (this.state.result?.renderResults !== undefined) {
-      this.flamegraph.updateDataIfChanged(
-        this.nodeRendering(),
-        this.state.result.renderResults,
-        this.state.selectedCallsites[this.state.viewingOption],
-      );
-      height = this.flamegraph.getHeight() + HEADER_HEIGHT;
-    } else {
-      height = undefined;
-    }
-
-    return m(
-      '.flamegraph-profile',
-      this.maybeShowModal(),
-      m(
-        DetailsShell,
-        {
-          fillParent: true,
-          title: m(
-            'div.title',
-            this.getTitle(),
-            this.state.selection.profileType ===
-              ProfileType.MIXED_HEAP_PROFILE &&
-              m(
-                Popup,
-                {
-                  trigger: m(Icon, {icon: 'warning'}),
-                },
-                m(
-                  '',
-                  {style: {width: '300px'}},
-                  'This is a mixed java/native heap profile, free()s are not visualized. To visualize free()s, remove "all_heaps: true" from the config.',
-                ),
-              ),
-            ':',
-          ),
-          description: this.getViewingOptionButtons(),
-          buttons: [
-            m(
-              'div.selected',
-              `Selected function: ${toSelectedCallsite(
-                this.state.selectedCallsites[this.state.viewingOption],
-              )}`,
-            ),
-            m(
-              'div.time',
-              `Snapshot time: `,
-              m(DurationWidget, {
-                dur: this.state.selection.end - this.state.selection.start,
-              }),
-            ),
-            m('input[type=text][placeholder=Focus]', {
-              oninput: (e: Event) => {
-                const target = e.target as HTMLInputElement;
-                this.undebouncedFocusRegex = target.value;
-                this.updateFocusRegexDebounced();
-              },
-              // Required to stop hot-key handling:
-              onkeydown: (e: Event) => e.stopPropagation(),
-            }),
-            (this.state.selection.profileType ===
-              ProfileType.NATIVE_HEAP_PROFILE ||
-              this.state.selection.profileType ===
-                ProfileType.JAVA_HEAP_SAMPLES) &&
-              m(Button, {
-                icon: 'file_download',
-                intent: Intent.Primary,
-                onclick: () => {
-                  this.downloadPprof();
-                  raf.scheduleFullRedraw();
-                },
-              }),
-          ],
-        },
-        m(
-          '.flamegraph-content',
-          this.state.result === undefined
-            ? m(
-                '.loading-container',
-                m(
-                  EmptyState,
-                  {
-                    icon: 'bar_chart',
-                    title: 'Computing graph ...',
-                    className: 'flamegraph-loading',
-                  },
-                  m(Spinner, {easing: true}),
-                ),
-              )
-            : m(`canvas[ref=canvas]`, {
-                style: `height:${height}px; width:100%`,
-                onmousemove: (e: MouseEvent) => {
-                  const {offsetX, offsetY} = e;
-                  this.flamegraph.onMouseMove({x: offsetX, y: offsetY});
-                  raf.scheduleFullRedraw();
-                },
-                onmouseout: () => {
-                  this.flamegraph.onMouseOut();
-                  raf.scheduleFullRedraw();
-                },
-                onclick: (e: MouseEvent) => {
-                  if (
-                    this.state === undefined ||
-                    this.state.result === undefined
-                  ) {
-                    return;
-                  }
-                  const {offsetX, offsetY} = e;
-                  const cs = {...this.state.selectedCallsites};
-                  cs[this.state.viewingOption] = this.flamegraph.onMouseClick({
-                    x: offsetX,
-                    y: offsetY,
-                  });
-                  this.state.selectedCallsites = cs;
-                  raf.scheduleFullRedraw();
-                },
-              }),
-        ),
-      ),
-    );
-  }
-
-  private getTitle(): string {
-    const state = assertExists(this.state);
-    switch (state.selection.profileType) {
-      case ProfileType.MIXED_HEAP_PROFILE:
-        return 'Mixed heap profile';
-      case ProfileType.HEAP_PROFILE:
-        return 'Heap profile';
-      case ProfileType.NATIVE_HEAP_PROFILE:
-        return 'Native heap profile';
-      case ProfileType.JAVA_HEAP_SAMPLES:
-        return 'Java heap samples';
-      case ProfileType.JAVA_HEAP_GRAPH:
-        return 'Java heap graph';
-      case ProfileType.PERF_SAMPLE:
-        return 'Profile';
-      default:
-        throw new Error('unknown type');
-    }
-  }
-
-  private nodeRendering(): NodeRendering {
-    const state = assertExists(this.state);
-    const profileType = state.selection.profileType;
-    switch (profileType) {
-      case ProfileType.JAVA_HEAP_GRAPH:
-        if (
-          state.viewingOption ===
-            FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY ||
-          state.viewingOption ===
-            FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY
-        ) {
-          return RENDER_OBJ_COUNT;
-        } else {
-          return RENDER_SELF_AND_TOTAL;
-        }
-      case ProfileType.MIXED_HEAP_PROFILE:
-      case ProfileType.HEAP_PROFILE:
-      case ProfileType.NATIVE_HEAP_PROFILE:
-      case ProfileType.JAVA_HEAP_SAMPLES:
-      case ProfileType.PERF_SAMPLE:
-        return RENDER_SELF_AND_TOTAL;
-      default:
-        const exhaustiveCheck: never = profileType;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-    }
-  }
-
-  private getViewingOptionButtons(): m.Children {
-    const ret = [];
-    const state = assertExists(this.state);
-    for (const {option, name} of viewingOptions(state.selection.profileType)) {
-      ret.push(
-        m(Button, {
-          label: name,
-          active: option === state.viewingOption,
-          onclick: () => {
-            const state = assertExists(this.state);
-            state.viewingOption = option;
-            raf.scheduleFullRedraw();
-          },
-        }),
-      );
-    }
-    return m(ButtonBar, ret);
-  }
-
-  onupdate({dom}: m.VnodeDOM<FlamegraphDetailsPanelAttrs>) {
-    const canvas = findRef(dom, 'canvas');
-    if (canvas === null || !(canvas instanceof HTMLCanvasElement)) {
-      return;
-    }
-    if (!this.state?.result?.renderResults) {
-      return;
-    }
-    canvas.width = canvas.offsetWidth * devicePixelRatio;
-    canvas.height = canvas.offsetHeight * devicePixelRatio;
-
-    const ctx = canvas.getContext('2d');
-    if (ctx === null) {
-      return;
-    }
-
-    ctx.clearRect(0, 0, canvas.width, canvas.height);
-    ctx.save();
-    ctx.scale(devicePixelRatio, devicePixelRatio);
-    const {offsetWidth: width, offsetHeight: height} = canvas;
-    const unit =
-      this.state.viewingOption ===
-        FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY ||
-      this.state.viewingOption ===
-        FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY
-        ? 'B'
-        : '';
-    this.flamegraph.draw(ctx, width, height, 0, 0, unit);
-    ctx.restore();
-  }
-
-  private static async fetchQueryResults(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ) {
-    const table = await LegacyFlamegraphDetailsPanel.prepareViewsAndTables(
-      engine,
-      cache,
-      state,
-    );
-    const queryResults =
-      await LegacyFlamegraphDetailsPanel.getFlamegraphDataFromTables(
-        engine,
-        table,
-        state.viewingOption,
-        state.focusRegex,
-      );
-
-    let incomplete = false;
-    if (state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH) {
-      const it = await engine.query(`
-        select value from stats
-        where severity = 'error' and name = 'heap_graph_non_finalized_graph'
-      `);
-      incomplete = it.firstRow({value: NUM}).value > 0;
-    }
-    state.result = {
-      queryResults,
-      incomplete,
-    };
-    raf.scheduleFullRedraw();
-  }
-
-  private static async prepareViewsAndTables(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    state: FlamegraphState,
-  ): Promise<string> {
-    const flamegraphType = getFlamegraphType(state.selection.profileType);
-    if (state.selection.profileType === ProfileType.PERF_SAMPLE) {
-      let upid: string;
-      let upidGroup: string;
-      if (state.selection.upids.length > 1) {
-        upid = `NULL`;
-        upidGroup = `'${this.serializeUpidGroup(state.selection.upids)}'`;
-      } else {
-        upid = `${state.selection.upids[0]}`;
-        upidGroup = `NULL`;
-      }
-      return cache.getTableName(
-        engine,
-        `
-          select
-            id,
-            name,
-            map_name,
-            parent_id,
-            depth,
-            cumulative_size,
-            cumulative_alloc_size,
-            cumulative_count,
-            cumulative_alloc_count,
-            size,
-            alloc_size,
-            count,
-            alloc_count,
-            source_file,
-            line_number
-          from experimental_flamegraph(
-            '${flamegraphType}',
-            NULL,
-            '>=${state.selection.start},<=${state.selection.end}',
-            ${upid},
-            ${upidGroup},
-            '${state.focusRegex}'
-          )
-        `,
-      );
-    }
-    if (
-      state.selection.profileType === ProfileType.JAVA_HEAP_GRAPH &&
-      isHeapGraphDominatorTreeViewingOption(state.viewingOption)
-    ) {
-      assertTrue(state.selection.start == state.selection.end);
-      return cache.getTableName(
-        engine,
-        await this.loadHeapGraphDominatorTreeQuery(
-          engine,
-          cache,
-          state.selection.upids[0],
-          state.selection.start,
-        ),
-      );
-    }
-    assertTrue(state.selection.start == state.selection.end);
-    return cache.getTableName(
-      engine,
-      `
-        select
-          id,
-          name,
-          map_name,
-          parent_id,
-          depth,
-          cumulative_size,
-          cumulative_alloc_size,
-          cumulative_count,
-          cumulative_alloc_count,
-          size,
-          alloc_size,
-          count,
-          alloc_count,
-          source_file,
-          line_number
-        from experimental_flamegraph(
-          '${flamegraphType}',
-          ${state.selection.start},
-          NULL,
-          ${state.selection.upids[0]},
-          NULL,
-          '${state.focusRegex}'
-        )
-      `,
-    );
-  }
-
-  private static async loadHeapGraphDominatorTreeQuery(
-    engine: Engine,
-    cache: LegacyFlamegraphCache,
-    upid: number,
-    timestamp: time,
-  ) {
-    const outputTableName = `heap_graph_type_dominated_${upid}_${timestamp}`;
-    const outputQuery = `SELECT * FROM ${outputTableName}`;
-    if (cache.hasQuery(outputQuery)) {
-      return outputQuery;
-    }
-
-    await engine.query(`
-      INCLUDE PERFETTO MODULE android.memory.heap_graph.dominator_tree;
-
-      -- heap graph dominator tree with objects as nodes and all relavant
-      -- object self stats and dominated stats
-      CREATE PERFETTO TABLE _heap_graph_object_dominated AS
-      SELECT
-      node.id,
-      node.idom_id,
-      node.dominated_obj_count,
-      node.dominated_size_bytes + node.dominated_native_size_bytes AS dominated_size,
-      node.depth,
-      obj.type_id,
-      obj.root_type,
-      obj.self_size + obj.native_size AS self_size
-      FROM heap_graph_dominator_tree node
-      JOIN heap_graph_object obj USING(id)
-      WHERE obj.upid = ${upid} AND obj.graph_sample_ts = ${timestamp}
-      -- required to accelerate the recursive cte below
-      ORDER BY idom_id;
-
-      -- calculate for each object node in the dominator tree the
-      -- HASH(path of type_id's from the super root to the object)
-      CREATE PERFETTO TABLE _dominator_tree_path_hash AS
-      WITH RECURSIVE _tree_visitor(id, path_hash) AS (
-        SELECT
-          id,
-          HASH(
-            CAST(type_id AS TEXT) || '-' || IFNULL(root_type, '')
-          ) AS path_hash
-        FROM _heap_graph_object_dominated
-        WHERE depth = 1
-        UNION ALL
-        SELECT
-          child.id,
-          HASH(CAST(parent.path_hash AS TEXT) || '/' || CAST(type_id AS TEXT)) AS path_hash
-        FROM _heap_graph_object_dominated child
-        JOIN _tree_visitor parent ON child.idom_id = parent.id
-      )
-      SELECT * from _tree_visitor
-      ORDER BY id;
-
-      -- merge object nodes with the same path into one "class type node", so the
-      -- end result is a tree where nodes are identified by their types and the
-      -- dominator relationships are preserved.
-      CREATE PERFETTO TABLE ${outputTableName} AS
-      SELECT
-        map.path_hash as id,
-        COALESCE(cls.deobfuscated_name, cls.name, '[NULL]') || IIF(
-          node.root_type IS NOT NULL,
-          ' [' || node.root_type || ']', ''
-        ) AS name,
-        IFNULL(parent_map.path_hash, -1) AS parent_id,
-        node.depth - 1 AS depth,
-        sum(dominated_size) AS cumulative_size,
-        -1 AS cumulative_alloc_size,
-        sum(dominated_obj_count) AS cumulative_count,
-        -1 AS cumulative_alloc_count,
-        '' as map_name,
-        '' as source_file,
-        -1 as line_number,
-        sum(self_size) AS size,
-        count(*) AS count
-      FROM _heap_graph_object_dominated node
-      JOIN _dominator_tree_path_hash map USING(id)
-      LEFT JOIN _dominator_tree_path_hash parent_map ON node.idom_id = parent_map.id
-      JOIN heap_graph_class cls ON node.type_id = cls.id
-      GROUP BY map.path_hash, name, parent_id, depth, map_name, source_file, line_number;
-
-      -- These are intermediates and not needed
-      DROP TABLE _heap_graph_object_dominated;
-      DROP TABLE _dominator_tree_path_hash;
-    `);
-
-    return outputQuery;
-  }
-
-  private static async getFlamegraphDataFromTables(
-    engine: Engine,
-    tableName: string,
-    viewingOption: FlamegraphViewingOption,
-    focusRegex: string,
-  ) {
-    let orderBy = '';
-    let totalColumnName:
-      | 'cumulativeSize'
-      | 'cumulativeAllocSize'
-      | 'cumulativeCount'
-      | 'cumulativeAllocCount' = 'cumulativeSize';
-    let selfColumnName: 'size' | 'count' = 'size';
-    // TODO(fmayer): Improve performance so this is no longer necessary.
-    // Alternatively consider collapsing frames of the same label.
-    const maxDepth = 100;
-    switch (viewingOption) {
-      case FlamegraphViewingOption.ALLOC_SPACE_MEMORY_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_size desc, name`;
-        totalColumnName = 'cumulativeAllocSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_count desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.OBJECTS_ALLOCATED_KEY:
-        orderBy = `where cumulative_alloc_count > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_alloc_count desc, name`;
-        totalColumnName = 'cumulativeAllocCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.PERF_SAMPLES_KEY:
-      case FlamegraphViewingOption.SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY:
-        orderBy = `where cumulative_size > 0 and depth < ${maxDepth} order by depth, parent_id,
-            cumulative_size desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_COUNT_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeCount desc, name`;
-        totalColumnName = 'cumulativeCount';
-        selfColumnName = 'count';
-        break;
-      case FlamegraphViewingOption.DOMINATOR_TREE_OBJ_SIZE_KEY:
-        orderBy = `where depth < ${maxDepth} order by depth,
-          cumulativeSize desc, name`;
-        totalColumnName = 'cumulativeSize';
-        selfColumnName = 'size';
-        break;
-      default:
-        const exhaustiveCheck: never = viewingOption;
-        throw new Error(`Unhandled case: ${exhaustiveCheck}`);
-        break;
-    }
-
-    const callsites = await engine.query(`
-      SELECT
-        id as hash,
-        IFNULL(IFNULL(DEMANGLE(name), name), '[NULL]') as name,
-        IFNULL(parent_id, -1) as parentHash,
-        depth,
-        cumulative_size as cumulativeSize,
-        cumulative_alloc_size as cumulativeAllocSize,
-        cumulative_count as cumulativeCount,
-        cumulative_alloc_count as cumulativeAllocCount,
-        map_name as mapping,
-        size,
-        count,
-        IFNULL(source_file, '') as sourceFile,
-        IFNULL(line_number, -1) as lineNumber
-      from ${tableName}
-      ${orderBy}
-    `);
-
-    const flamegraphData: CallsiteInfo[] = [];
-    const hashToindex: Map<number, number> = new Map();
-    const it = callsites.iter({
-      hash: NUM,
-      name: STR,
-      parentHash: NUM,
-      depth: NUM,
-      cumulativeSize: NUM,
-      cumulativeAllocSize: NUM,
-      cumulativeCount: NUM,
-      cumulativeAllocCount: NUM,
-      mapping: STR,
-      sourceFile: STR,
-      lineNumber: NUM,
-      size: NUM,
-      count: NUM,
-    });
-    for (let i = 0; it.valid(); ++i, it.next()) {
-      const hash = it.hash;
-      let name = it.name;
-      const parentHash = it.parentHash;
-      const depth = it.depth;
-      const totalSize = it[totalColumnName];
-      const selfSize = it[selfColumnName];
-      const mapping = it.mapping;
-      const highlighted =
-        focusRegex !== '' &&
-        name.toLocaleLowerCase().includes(focusRegex.toLocaleLowerCase());
-      const parentId = hashToindex.has(+parentHash)
-        ? hashToindex.get(+parentHash)!
-        : -1;
-
-      let location: string | undefined;
-      if (/[a-zA-Z]/i.test(it.sourceFile)) {
-        location = it.sourceFile;
-        if (it.lineNumber !== -1) {
-          location += `:${it.lineNumber}`;
-        }
-      }
-
-      if (depth === maxDepth - 1) {
-        name += ' [tree truncated]';
-      }
-      // Instead of hash, we will store index of callsite in this original array
-      // as an id of callsite. That way, we have quicker access to parent and it
-      // will stay unique:
-      hashToindex.set(hash, i);
-
-      flamegraphData.push({
-        id: i,
-        totalSize,
-        depth,
-        parentId,
-        name,
-        selfSize,
-        mapping,
-        merged: false,
-        highlighted,
-        location,
-      });
-    }
-    return flamegraphData;
-  }
-
-  private async downloadPprof() {
-    if (this.state === undefined) {
-      return;
-    }
-    const engine = this.getCurrentEngine();
-    if (engine === undefined) {
-      return;
-    }
-    try {
-      assertTrue(
-        this.state.selection.upids.length === 1,
-        'Native profiles can only contain one pid.',
-      );
-      const pid = await engine.query(
-        `select pid from process where upid = ${this.state.selection.upids[0]}`,
-      );
-      const trace = await getCurrentTrace();
-      convertTraceToPprofAndDownload(
-        trace,
-        pid.firstRow({pid: NUM}).pid,
-        this.state.selection.start,
-      );
-    } catch (error) {
-      throw new Error(`Failed to get current trace ${error}`);
-    }
-  }
-
-  private maybeShowModal() {
-    const state = assertExists(this.state);
-    if (state.result?.incomplete === undefined || !state.result.incomplete) {
-      return undefined;
-    }
-    if (globals.state.flamegraphModalDismissed) {
-      return undefined;
-    }
-    return m(Modal, {
-      title: 'The flamegraph is incomplete',
-      vAlign: 'TOP',
-      content: m(
-        'div',
-        'The current trace does not have a fully formed flamegraph',
-      ),
-      buttons: [
-        {
-          text: 'Show the errors',
-          primary: true,
-          action: () => Router.navigate('#!/info'),
-        },
-        {
-          text: 'Skip',
-          action: () => {
-            globals.dispatch(Actions.dismissFlamegraphModal({}));
-            raf.scheduleFullRedraw();
-          },
-        },
-      ],
-    } as ModalAttrs);
-  }
-
-  private static getMinSizeDisplayed(
-    flamegraphData: ReadonlyArray<CallsiteInfo>,
-    rootSize?: number,
-  ): number {
-    // Note: This is a hack. Really we should obtain the size of the canvas and
-    // use that to determine the number of buckets to display, but this code is
-    // legacy and going away soon, and the calculation before was just plain
-    // wrong anyway so this isn't really any worse.
-    //
-    // 800 buckets is a decent placeholder until the new flamegraph code lands.
-    const bucketCount = 800;
-    if (rootSize === undefined) {
-      rootSize = findRootSize(flamegraphData);
-    }
-    return (MIN_PIXEL_DISPLAYED * rootSize) / bucketCount;
-  }
-
-  private static serializeUpidGroup(upids: number[]) {
-    return new Array(upids).join();
-  }
-
-  private getCurrentEngine() {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) return undefined;
-    return globals.engines.get(engineId);
-  }
-}
diff --git a/ui/src/frontend/legacy_flamegraph_unittest.ts b/ui/src/frontend/legacy_flamegraph_unittest.ts
deleted file mode 100644
index ddb006a..0000000
--- a/ui/src/frontend/legacy_flamegraph_unittest.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-// Copyright (C) 2019 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {splitIfTooBig} from './legacy_flamegraph';
-
-test('textGoingToMultipleLines', () => {
-  const text = 'Dummy text to go to multiple lines.';
-
-  const lineSplitter = splitIfTooBig(text, 7 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy t', 'ext to ', 'go to m', 'ultiple', ' lines.'],
-    lineWidth: 7,
-  });
-});
-
-test('emptyText', () => {
-  const text = '';
-
-  const lineSplitter = splitIfTooBig(text, 10, 5);
-
-  expect(lineSplitter).toEqual({lines: [], lineWidth: 5});
-});
-
-test('textEnoughForOneLine', () => {
-  const text = 'Dummy text to go to one lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length + 32, text.length);
-
-  expect(lineSplitter).toEqual({lines: [text], lineWidth: text.length});
-});
-
-test('textGoingToTwoLines', () => {
-  const text = 'Dummy text to go to two lines.';
-
-  const lineSplitter = splitIfTooBig(text, text.length / 2 + 32, text.length);
-
-  expect(lineSplitter).toEqual({
-    lines: ['Dummy text to g', 'o to two lines.'],
-    lineWidth: text.length / 2,
-  });
-});
diff --git a/ui/src/frontend/named_slice_track.ts b/ui/src/frontend/named_slice_track.ts
index 24fa2cf..34220b8 100644
--- a/ui/src/frontend/named_slice_track.ts
+++ b/ui/src/frontend/named_slice_track.ts
@@ -72,7 +72,7 @@
       {
         kind: 'SLICE',
         id: args.slice.id,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
         table: 'slice',
       },
       {
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index a9ab565..d4ad50a 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -65,9 +65,7 @@
   private hoveredX: null | number = null;
 
   render(): m.Children {
-    const allCollapsed = Object.values(globals.state.trackGroups).every(
-      (group) => group.collapsed,
-    );
+    const allCollapsed = globals.workspace.flatGroups.every((n) => n.collapsed);
 
     return m(
       '.notes-panel',
@@ -114,7 +112,10 @@
           m(Button, {
             onclick: (e: Event) => {
               e.preventDefault();
-              globals.dispatch(Actions.clearAllPinnedTracks({}));
+              globals.workspace.pinnedTracks.forEach((t) =>
+                globals.workspace.unpinTrack(t),
+              );
+              raf.scheduleFullRedraw();
             },
             title: 'Clear all pinned tracks',
             icon: 'clear_all',
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 7537f85..5c2ab81 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -46,8 +46,8 @@
   readonly kind: 'panel';
   render(): m.Children;
   readonly selectable: boolean;
-  readonly trackKey?: string; // Defined if this panel represents are track
-  readonly groupKey?: string; // Defined if this panel represents a group - i.e. a group summary track
+  readonly trackUri?: string; // Defined if this panel represents are track
+  readonly groupUri?: string; // Defined if this panel represents a group - i.e. a group summary track
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size): void;
   getSliceVerticalBounds?(depth: number): Optional<VerticalBounds>;
 }
@@ -55,8 +55,8 @@
 export interface PanelGroup {
   readonly kind: 'group';
   readonly collapsed: boolean;
-  readonly header: Panel;
-  readonly childPanels: Panel[];
+  readonly header?: Panel;
+  readonly childPanels: PanelOrGroup[];
 }
 
 export type PanelOrGroup = Panel | PanelGroup;
@@ -189,17 +189,19 @@
     // Get the track ids from the panels.
     const tracks = [];
     for (const panel of panels) {
-      if (panel.trackKey !== undefined) {
-        tracks.push(panel.trackKey);
+      if (panel.trackUri !== undefined) {
+        tracks.push(panel.trackUri);
         continue;
       }
-      if (panel.groupKey !== undefined) {
-        const trackGroup = globals.state.trackGroups[panel.groupKey];
+      if (panel.groupUri !== undefined) {
+        const trackGroup = globals.workspace.flatGroups.find(
+          (g) => g.uri === panel.groupUri,
+        );
         // Only select a track group and all child tracks if it is closed.
-        if (trackGroup.collapsed) {
-          tracks.push(panel.groupKey);
-          for (const track of trackGroup.tracks) {
-            tracks.push(track);
+        if (trackGroup && trackGroup.collapsed) {
+          tracks.push(panel.groupUri);
+          for (const track of trackGroup.flatTracks) {
+            tracks.push(track.uri);
           }
         }
       }
@@ -289,11 +291,12 @@
     if (node.kind === 'group') {
       return m(
         'div.pf-panel-group',
-        this.renderPanel(
-          node.header,
-          `${panelId}-header`,
-          node.collapsed ? '' : '.pf-sticky',
-        ),
+        node.header &&
+          this.renderPanel(
+            node.header,
+            `${panelId}-header`,
+            node.collapsed ? '' : '.pf-sticky',
+          ),
         ...node.childPanels.map((child, index) =>
           this.renderTree(child, `${panelId}-${index}`),
         ),
@@ -338,7 +341,7 @@
       const panel = assertExists(this.panelById.get(panelId));
 
       // NOTE: the id can be undefined for singletons like overview timeline.
-      const key = panel.trackKey || panel.groupKey || '';
+      const key = panel.trackUri || panel.groupUri || '';
       const rect = panelElement.getBoundingClientRect();
       this.panelInfos.push({
         trackOrGroupKey: key,
@@ -456,14 +459,14 @@
     ) {
       return;
     }
-    if (this.panelInfos.length === 0 || area.tracks.length === 0) return;
+    if (this.panelInfos.length === 0 || area.trackUris.length === 0) return;
 
     // Find the minY and maxY of the selected tracks in this panel container.
     let selectedTracksMinY = this.panelContainerHeight + this.panelContainerTop;
     let selectedTracksMaxY = this.panelContainerTop;
     let trackFromCurrentContainerSelected = false;
     for (let i = 0; i < this.panelInfos.length; i++) {
-      if (area.tracks.includes(this.panelInfos[i].trackOrGroupKey)) {
+      if (area.trackUris.includes(this.panelInfos[i].trackOrGroupKey)) {
         trackFromCurrentContainerSelected = true;
         selectedTracksMinY = Math.min(
           selectedTracksMinY,
diff --git a/ui/src/frontend/pivot_table.ts b/ui/src/frontend/pivot_table.ts
index 87b176b..2b17b93 100644
--- a/ui/src/frontend/pivot_table.ts
+++ b/ui/src/frontend/pivot_table.ts
@@ -45,7 +45,7 @@
 import {DurationWidget} from './widgets/duration';
 import {addSqlTableTab} from './sql_table_tab_command';
 import {getSqlTableDescription} from './widgets/sql/table/sql_table_registry';
-import {assertExists} from '../base/logging';
+import {assertExists, assertFalse} from '../base/logging';
 import {Filter, SqlColumn} from './widgets/sql/table/column';
 import {argSqlColumn} from './widgets/sql/table/well_known_columns';
 
@@ -206,6 +206,8 @@
     ) {
       if (typeof value === 'bigint') {
         return m(DurationWidget, {dur: value});
+      } else if (typeof value === 'number') {
+        return m(DurationWidget, {dur: BigInt(Math.round(value))});
       }
     }
     return `${value}`;
@@ -546,12 +548,10 @@
     const queryResult: PivotTableResult = state.queryResult;
 
     const renderedRows: m.Vnode[] = [];
-    const tree = state.queryResult.tree;
 
-    if (tree.children.size === 0 && tree.rows.length === 0) {
-      // Empty result, render a special message
-      return m('.empty-result', 'No slices in the current selection.');
-    }
+    // We should not even be showing the tab if there's no results.
+    const tree = state.queryResult.tree;
+    assertFalse(tree.children.size === 0 && tree.rows.length === 0);
 
     this.renderTree(
       attrs.selectionArea,
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index 84158c0..5572814 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -22,7 +22,6 @@
 import {getLegacySelection} from '../common/state';
 
 import {
-  CpuProfileDetails,
   Flow,
   globals,
   QuantizedLoad,
@@ -73,11 +72,6 @@
   raf.scheduleFullRedraw();
 }
 
-export function publishCpuProfileDetails(details: CpuProfileDetails) {
-  globals.cpuProfileDetails = details;
-  globals.publishRedraw();
-}
-
 export function publishHasFtrace(value: boolean): void {
   globals.hasFtrace = value;
   globals.publishRedraw();
@@ -143,9 +137,9 @@
   globals.publishRedraw();
 }
 
-export function publishSliceDetails(click: SliceDetails) {
-  globals.sliceDetails = click;
-  const id = click.id;
+export function publishSliceDetails(sliceDetails: SliceDetails) {
+  globals.sliceDetails = sliceDetails;
+  const id = sliceDetails.id;
   if (id !== undefined && id === globals.state.pendingScrollId) {
     findCurrentSelection();
     globals.dispatch(Actions.clearPendingScrollId({id: undefined}));
diff --git a/ui/src/frontend/query_table.ts b/ui/src/frontend/query_table.ts
index dd7cc19..84fe7ed 100644
--- a/ui/src/frontend/query_table.ts
+++ b/ui/src/frontend/query_table.ts
@@ -30,7 +30,7 @@
 import {downloadData} from './download_utils';
 import {globals} from './globals';
 import {Router} from './router';
-import {reveal} from './scroll_helper';
+import {scrollToTrackAndTimeSpan} from './scroll_helper';
 
 interface QueryTableRowAttrs {
   row: Row;
@@ -149,24 +149,31 @@
     const sliceStart = Time.fromRaw(BigInt(row.ts));
     // row.dur can be negative. Clamp to 1ns.
     const sliceDur = BigintMath.max(BigInt(row.dur), 1n);
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    if (trackKey !== undefined) {
-      reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true);
+    const trackUri = globals.trackManager.findTrack((td) =>
+      td.tags?.trackIds?.includes(trackId),
+    )?.uri;
+    if (trackUri !== undefined) {
+      scrollToTrackAndTimeSpan(
+        trackUri,
+        sliceStart,
+        Time.add(sliceStart, sliceDur),
+        true,
+      );
       const sliceId = getSliceId(row);
       if (sliceId !== undefined) {
-        this.selectSlice(sliceId, trackKey, switchToCurrentSelectionTab);
+        this.selectSlice(sliceId, trackUri, switchToCurrentSelectionTab);
       }
     }
   }
 
   private selectSlice(
     sliceId: number,
-    trackKey: string,
+    trackUuid: string,
     switchToCurrentSelectionTab: boolean,
   ) {
     const action = Actions.selectSlice({
       id: sliceId,
-      trackKey,
+      trackUri: trackUuid,
       table: 'slice',
     });
     globals.makeSelection(action, {switchToCurrentSelectionTab});
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index 6511744..b74069d 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -14,10 +14,8 @@
 
 import {time} from '../base/time';
 import {escapeCSSSelector, exists} from '../base/utils';
-import {Actions} from '../common/actions';
 import {HighPrecisionTime} from '../common/high_precision_time';
 import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
-import {getContainingGroupKey} from '../common/state';
 import {raf} from '../core/raf_scheduler';
 import {globals} from './globals';
 
@@ -64,8 +62,8 @@
 // Given a track id, find a track with that id and scroll it into view. If the
 // track is nested inside a track group, scroll to that track group instead.
 // If |openGroup| then open the track group and scroll to the track.
-export function verticalScrollToTrack(trackKey: string, openGroup = false) {
-  const track = document.querySelector('#track_' + escapeCSSSelector(trackKey));
+export function verticalScrollToTrack(trackUri: string, openGroup = false) {
+  const track = document.querySelector('#track_' + escapeCSSSelector(trackUri));
 
   if (track) {
     // block: 'nearest' means that it will only scroll if the track is not
@@ -74,49 +72,46 @@
     return;
   }
 
-  let trackGroup = null;
-  const groupKey = getContainingGroupKey(globals.state, trackKey);
-  if (groupKey) {
-    trackGroup = document.querySelector('#track_' + groupKey);
-  }
+  // If we get here, the element for this track was not present in the DOM, this
+  // might be because it's inside a collapsed group.
+  // Find the track node in the current workspace, and reveal it.
+  const trackNode = globals.workspace.getTrackByUri(trackUri);
+  if (!trackNode) return;
 
-  if (!groupKey || !trackGroup) {
-    console.error(`Can't scroll, track (${trackKey}) not found.`);
-    return;
-  }
-
-  // The requested track is inside a closed track group, either open the track
-  // group and scroll to the track or just scroll to the track group.
   if (openGroup) {
-    // After the track exists in the dom, it will be scrolled to.
-    globals.scrollToTrackKey = trackKey;
-    globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
+    trackNode.reveal();
+    globals.scrollToTrackUri = trackUri;
     return;
-  } else {
-    trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
   }
+  // Find the first closed ancestor of our target track.
+  const groupNode = trackNode.closestVisibleAncestor;
+  if (groupNode) {
+    document
+      .querySelector('#track_' + groupNode.uri)
+      ?.scrollIntoView({behavior: 'smooth', block: 'nearest'});
+  }
+  // If we get here, it means this track isn't in the workspace.
+  // TODO(stevegolton): Warn the user about this?
 }
 
-// Scroll vertically and horizontally to reach track (|trackKey|) at |ts|.
+// Scroll vertically and horizontally to reach track |track| at |ts|.
 export function scrollToTrackAndTs(
-  trackKey: string | undefined,
+  trackUri: string,
   ts: time,
   openGroup = false,
 ) {
-  if (trackKey !== undefined) {
-    verticalScrollToTrack(trackKey, openGroup);
-  }
+  verticalScrollToTrack(trackUri, openGroup);
   horizontalScrollToTs(ts);
 }
 
 // Scroll vertically and horizontally to a track and time range
-export function reveal(
-  trackKey: string,
+export function scrollToTrackAndTimeSpan(
+  trackUri: string,
   start: time,
   end: time,
   openGroup = false,
 ) {
-  verticalScrollToTrack(trackKey, openGroup);
+  verticalScrollToTrack(trackUri, openGroup);
   focusHorizontalRange(start, end);
 }
 
diff --git a/ui/src/frontend/search_handler.ts b/ui/src/frontend/search_handler.ts
index 8a65bcb..eac3ff8 100644
--- a/ui/src/frontend/search_handler.ts
+++ b/ui/src/frontend/search_handler.ts
@@ -85,20 +85,20 @@
   const searchIndex = globals.state.searchIndex;
   const source = globals.currentSearchResults.sources[searchIndex];
   const currentId = globals.currentSearchResults.eventIds[searchIndex];
-  const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
+  const uri = globals.currentSearchResults.trackUris[searchIndex];
 
   if (currentId === undefined) return;
 
   switch (source) {
     case 'track':
-      verticalScrollToTrack(trackKey, true);
+      verticalScrollToTrack(uri, true);
       break;
     case 'cpu':
       globals.setLegacySelection(
         {
           kind: 'SCHED_SLICE',
           id: currentId,
-          trackKey,
+          trackUri: uri,
         },
         {
           clearSearch: false,
@@ -112,7 +112,7 @@
         {
           kind: 'LOG',
           id: currentId,
-          trackKey,
+          trackUri: uri,
         },
         {
           clearSearch: false,
@@ -128,7 +128,7 @@
         {
           kind: 'SLICE',
           id: currentId,
-          trackKey,
+          trackUri: uri,
           table: 'slice',
         },
         {
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 2e784b5..42f7c6d 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -35,7 +35,7 @@
   ) {
     super({
       engine,
-      trackKey: ctx.trackKey,
+      uri: ctx.trackUri,
       options: config.options,
     });
     this.config = config;
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index 1a49da7..c63f6e3 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -40,11 +40,11 @@
   ) {
     super({
       engine,
-      trackKey: ctx.trackKey,
+      uri: ctx.trackUri,
     });
 
     this.config = config;
-    this.sqlTableName = `__simple_slice_${uuidv4Sql(ctx.trackKey)}`;
+    this.sqlTableName = `__simple_slice_${uuidv4Sql(ctx.trackUri)}`;
   }
 
   async getSqlDataSource(): Promise<CustomSqlTableDefConfig> {
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index bdf83d8..9c23048 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -226,29 +226,22 @@
       return;
     }
 
-    let trackKey: string | undefined;
-    for (const track of Object.values(globals.state.tracks)) {
-      const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-      // TODO(stevegolton): Handle v2.
-      if (
-        trackDesc &&
-        trackDesc.tags?.kind === THREAD_STATE_TRACK_KIND &&
-        trackDesc.tags?.utid === threadInfo.utid
-      ) {
-        trackKey = track.key;
-      }
-    }
+    const trackDescriptor = globals.trackManager.findTrack(
+      (td) =>
+        td.tags?.kind === THREAD_STATE_TRACK_KIND &&
+        td.tags?.utid === threadInfo.utid,
+    );
 
     // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (trackKey && sliceInfo.threadStateId) {
+    if (trackDescriptor && sliceInfo.threadStateId) {
       globals.makeSelection(
         Actions.selectThreadState({
           id: sliceInfo.threadStateId,
-          trackKey: trackKey.toString(),
+          trackUri: trackDescriptor.uri,
         }),
       );
 
-      scrollToTrackAndTs(trackKey, sliceInfo.ts, true);
+      scrollToTrackAndTs(trackDescriptor.uri, sliceInfo.ts, true);
     }
   }
 
diff --git a/ui/src/frontend/tab_panel.ts b/ui/src/frontend/tab_panel.ts
index 05f443e..1601351 100644
--- a/ui/src/frontend/tab_panel.ts
+++ b/ui/src/frontend/tab_panel.ts
@@ -148,11 +148,10 @@
 
     // Show single selection panels if they are registered
     if (currentSelection.kind === 'single') {
-      const trackKey = currentSelection.trackKey;
-      const uri = globals.state.tracks[trackKey]?.uri;
+      const uri = currentSelection.trackUri;
 
       if (uri) {
-        const trackDesc = globals.trackManager.resolveTrackInfo(uri);
+        const trackDesc = globals.trackManager.getTrack(uri);
         const panel = trackDesc?.detailsPanel;
         if (panel) {
           return {
diff --git a/ui/src/frontend/thread_slice_track.ts b/ui/src/frontend/thread_slice_track.ts
index 11a1b78..0bcf2b5 100644
--- a/ui/src/frontend/thread_slice_track.ts
+++ b/ui/src/frontend/thread_slice_track.ts
@@ -87,7 +87,7 @@
       {
         kind: 'SLICE',
         id: args.slice.id,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
         table: this.tableName,
       },
       {
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index ed20d99..e137437 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -38,7 +38,6 @@
 } from '../trace_processor/sql_utils/thread_state';
 import {DurationWidget, renderDuration} from './widgets/duration';
 import {Timestamp} from './widgets/timestamp';
-import {addDebugSliceTrack} from './debug_tracks/debug_tracks';
 import {globals} from './globals';
 import {getProcessName} from '../trace_processor/sql_utils/process';
 import {
@@ -47,6 +46,10 @@
   getThreadName,
 } from '../trace_processor/sql_utils/thread';
 import {ThreadStateRef} from './widgets/thread_state';
+import {
+  CRITICAL_PATH_CMD,
+  CRITICAL_PATH_LITE_CMD,
+} from '../public/exposed_commands';
 
 interface ThreadStateTabConfig {
   // Id into |thread_state| sql table.
@@ -257,22 +260,9 @@
         name,
       });
 
-    const sliceColumns = {ts: 'ts', dur: 'dur', name: 'name'};
-    const sliceColumnNames = ['id', 'utid', 'ts', 'dur', 'name', 'table_name'];
-
-    const sliceLiteColumns = {ts: 'ts', dur: 'dur', name: 'thread_name'};
-    const sliceLiteColumnNames = [
-      'id',
-      'utid',
-      'ts',
-      'dur',
-      'thread_name',
-      'process_name',
-      'table_name',
-    ];
-
     const nameForNextOrPrev = (state: ThreadState) =>
       `${state.state} for ${renderDuration(state.dur)}`;
+
     return [
       m(
         Tree,
@@ -321,83 +311,28 @@
             ),
           ),
       ),
-      m(Button, {
-        label: 'Critical path lite',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                        ${this.state?.thread?.utid},
-                        trace_bounds.start_ts,
-                        trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                      trace_bounds
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-                  columns: sliceLiteColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceLiteColumns,
-                sliceLiteColumnNames,
-              ),
-            ),
-      }),
-      m(Button, {
-        label: 'Critical path',
-        intent: Intent.Primary,
-        onclick: () =>
-          this.engine
-            .query(
-              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            )
-            .then(() =>
-              addDebugSliceTrack(
-                // NOTE(stevegolton): This is a temporary patch, this menu
-                // should become part of a critical path plugin, at which point
-                // we can just use the plugin's context object.
-                {
-                  engine: this.engine,
-                  registerTrack: (x) => globals.trackManager.registerTrack(x),
-                },
-                {
-                  sqlSource: `
-                    SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                      FROM
-                        _thread_executing_span_critical_path_stack(
-                          ${this.state?.thread?.utid},
-                          trace_bounds.start_ts,
-                          trace_bounds.end_ts - trace_bounds.start_ts) cr,
-                        trace_bounds WHERE name IS NOT NULL
-                  `,
-                  columns: sliceColumnNames,
-                },
-                `${this.state?.thread?.name}`,
-                sliceColumns,
-                sliceColumnNames,
-              ),
-            ),
-      }),
+      globals.commandManager.hasCommand(CRITICAL_PATH_LITE_CMD) &&
+        m(Button, {
+          label: 'Critical path lite',
+          intent: Intent.Primary,
+          onclick: () => {
+            globals.commandManager.runCommand(
+              CRITICAL_PATH_LITE_CMD,
+              this.state?.thread?.utid,
+            );
+          },
+        }),
+      globals.commandManager.hasCommand(CRITICAL_PATH_CMD) &&
+        m(Button, {
+          label: 'Critical path',
+          intent: Intent.Primary,
+          onclick: () => {
+            globals.commandManager.runCommand(
+              CRITICAL_PATH_CMD,
+              this.state?.thread?.utid,
+            );
+          },
+        }),
     ];
   }
 
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index c980970..943e6a7 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -33,6 +33,7 @@
 export class TimeAxisPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
+  readonly id = 'time-axis-panel';
 
   render(): m.Children {
     return m('.time-axis-panel');
diff --git a/ui/src/frontend/timeline.ts b/ui/src/frontend/timeline.ts
index bdd244e..854f687 100644
--- a/ui/src/frontend/timeline.ts
+++ b/ui/src/frontend/timeline.ts
@@ -83,13 +83,13 @@
   selectArea(
     start: time,
     end: time,
-    tracks = this._selectedArea ? this._selectedArea.tracks : [],
+    tracks = this._selectedArea ? this._selectedArea.trackUris : [],
   ) {
     assertTrue(
       end >= start,
       `Impossible select area: start [${start}] >= end [${end}]`,
     );
-    this._selectedArea = {start, end, tracks};
+    this._selectedArea = {start, end, trackUris: tracks};
     raf.scheduleFullRedraw();
   }
 
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index c6462b0..1ea3a40 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -15,6 +15,6 @@
 import {Engine} from '../trace_processor/engine';
 
 export interface NewTrackArgs {
-  trackKey: string;
+  uri: string;
   engine: Engine;
 }
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index 55a5ad5..5d02c78 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -15,9 +15,7 @@
 import m from 'mithril';
 
 import {Icons} from '../base/semantic_icons';
-import {Actions} from '../common/actions';
-import {getContainingGroupKey} from '../common/state';
-import {TrackCacheEntry} from '../common/track_cache';
+import {TrackRenderer} from '../common/track_manager';
 import {TrackTags} from '../public';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
@@ -41,14 +39,17 @@
 import {PxSpan, TimeScale} from './time_scale';
 import {exists} from '../base/utils';
 import {classNames} from '../base/classnames';
+import {GroupNode} from '../public/workspace';
+import {raf} from '../core/raf_scheduler';
+import {Actions} from '../common/actions';
 
 interface Attrs {
-  readonly groupKey: string;
+  readonly groupNode: GroupNode;
   readonly title: m.Children;
   readonly tooltip: string;
   readonly collapsed: boolean;
   readonly collapsable: boolean;
-  readonly trackFSM?: TrackCacheEntry;
+  readonly trackRenderer?: TrackRenderer;
   readonly tags?: TrackTags;
   readonly subtitle?: string;
   readonly chips?: ReadonlyArray<string>;
@@ -57,14 +58,14 @@
 export class TrackGroupPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = true;
-  readonly groupKey: string;
+  readonly groupUri: string;
 
-  constructor(private attrs: Attrs) {
-    this.groupKey = attrs.groupKey;
+  constructor(private readonly attrs: Attrs) {
+    this.groupUri = attrs.groupNode.uri;
   }
 
   render(): m.Children {
-    const {groupKey, title, subtitle, chips, collapsed, trackFSM, tooltip} =
+    const {title, subtitle, chips, collapsed, trackRenderer, tooltip} =
       this.attrs;
 
     // The shell should be highlighted if the current search result is inside
@@ -72,37 +73,38 @@
     let highlightClass = '';
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
-      const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
-      if (containingGroupKey === groupKey) {
+      const uri = globals.currentSearchResults.trackUris[searchIndex];
+      if (this.attrs.groupNode.flatTracks.find((t) => t.uri === uri)) {
         highlightClass = 'flash';
       }
     }
 
     const selection = globals.state.selection;
 
-    const trackGroup = globals.state.trackGroups[groupKey];
+    // const trackGroup = globals.state.trackGroups[groupKey];
     let checkBox = Icons.BlankCheckbox;
     if (selection.kind === 'area') {
       if (
-        selection.tracks.includes(groupKey) &&
-        trackGroup.tracks.every((id) => selection.tracks.includes(id))
+        this.attrs.groupNode.flatTracks.every((track) =>
+          selection.trackUris.includes(track.uri),
+        )
       ) {
         checkBox = Icons.Checkbox;
       } else if (
-        selection.tracks.includes(groupKey) ||
-        trackGroup.tracks.some((id) => selection.tracks.includes(id))
+        this.attrs.groupNode.flatTracks.some((track) =>
+          selection.trackUris.includes(track.uri),
+        )
       ) {
         checkBox = Icons.IndeterminateCheckbox;
       }
     }
 
-    const error = trackFSM?.getError();
+    const error = trackRenderer?.getError();
 
     return m(
       `.track-group-panel[collapsed=${collapsed}]`,
       {
-        id: 'track_' + groupKey,
+        id: 'track_' + this.groupUri,
         oncreate: () => this.onupdate(),
         onupdate: () => this.onupdate(),
       },
@@ -116,11 +118,8 @@
           onclick: (e: MouseEvent) => {
             if (e.defaultPrevented) return;
             if (this.attrs.collapsable) {
-              globals.dispatch(
-                Actions.toggleTrackGroupCollapsed({
-                  groupKey,
-                }),
-              );
+              this.attrs.groupNode.toggleCollapsed();
+              raf.scheduleFullRedraw();
             }
             e.stopPropagation();
           },
@@ -150,9 +149,11 @@
             m(Button, {
               onclick: (e: MouseEvent) => {
                 globals.dispatch(
-                  Actions.toggleTrackSelection({
-                    key: groupKey,
-                    isTrackGroup: true,
+                  Actions.toggleGroupAreaSelection({
+                    // Dump URIs of all contained tracks & nodes, including this group
+                    trackUris: this.attrs.groupNode.flatNodes
+                      .map((t) => t.uri)
+                      .concat(this.attrs.groupNode.uri),
                   }),
                 );
                 e.stopPropagation();
@@ -162,13 +163,13 @@
             }),
         ),
       ),
-      trackFSM
+      trackRenderer
         ? m(
             TrackContent,
             {
-              track: trackFSM.track,
-              hasError: Boolean(trackFSM.getError()),
-              height: this.attrs.trackFSM?.track.getHeight(),
+              track: trackRenderer.track,
+              hasError: Boolean(trackRenderer.getError()),
+              height: this.attrs.trackRenderer?.track.getHeight(),
             },
             !collapsed && subtitle !== null ? m('span', subtitle) : null,
           )
@@ -177,8 +178,8 @@
   }
 
   private onupdate() {
-    if (this.attrs.trackFSM !== undefined) {
-      this.attrs.trackFSM.track.onFullRedraw?.();
+    if (this.attrs.trackRenderer !== undefined) {
+      this.attrs.trackRenderer.track.onFullRedraw?.();
     }
   }
 
@@ -189,8 +190,11 @@
   ) {
     const selection = globals.state.selection;
     if (selection.kind !== 'area') return;
+    const someSelected = this.attrs.groupNode.flatTracks.some((track) =>
+      selection.trackUris.includes(track.uri),
+    );
     const selectedAreaDuration = selection.end - selection.start;
-    if (selection.tracks.includes(this.groupKey)) {
+    if (someSelected) {
       ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
       ctx.fillRect(
         timescale.timeToPx(selection.start),
@@ -202,7 +206,7 @@
   }
 
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size) {
-    const {collapsed, trackFSM: track} = this.attrs;
+    const {collapsed, trackRenderer: track} = this.attrs;
 
     if (!collapsed) return;
 
@@ -230,7 +234,7 @@
           visibleWindow,
           size: trackSize,
           ctx,
-          trackKey: track.trackKey,
+          trackUri: track.desc.uri,
           resolution: calculateResolution(visibleWindow, trackSize.width),
           timescale,
         };
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index fa55e6f..e11cbbf 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -19,7 +19,7 @@
 import {Icons} from '../base/semantic_icons';
 import {TimeSpan} from '../base/time';
 import {Actions} from '../common/actions';
-import {TrackCacheEntry} from '../common/track_cache';
+import {TrackRenderer} from '../common/track_manager';
 import {raf} from '../core/raf_scheduler';
 import {Track, TrackTags} from '../public';
 
@@ -33,7 +33,6 @@
 import {generateTicks, TickType, getMaxMajorTicks} from './gridline_helper';
 import {Size, VerticalBounds} from '../base/geom';
 import {Panel} from './panel_container';
-import {verticalScrollToTrack} from './scroll_helper';
 import {drawVerticalLineAtTime} from './vertical_line_helper';
 import {classNames} from '../base/classnames';
 import {Button, ButtonBar} from '../widgets/button';
@@ -41,13 +40,13 @@
 import {canvasClip} from '../common/canvas_utils';
 import {PxSpan, TimeScale} from './time_scale';
 import {getLegacySelection} from '../common/state';
-import {CloseTrackButton} from './close_track_button';
 import {exists, Optional} from '../base/utils';
 import {Intent} from '../widgets/common';
 import {TrackRenderContext} from '../public/tracks';
 import {calculateResolution} from '../common/resolution';
 import {featureFlags} from '../core/feature_flags';
 import {Tree, TreeNode} from '../widgets/tree';
+import {TrackNode} from '../public/workspace';
 
 export const SHOW_TRACK_DETAILS_BUTTON = featureFlags.register({
   id: 'showTrackDetailsButton',
@@ -76,14 +75,10 @@
   return undefined;
 }
 
-function isTrackPinned(trackKey: string) {
-  return globals.state.pinnedTracks.indexOf(trackKey) !== -1;
-}
-
-function isTrackSelected(trackKey: string) {
+function isTrackSelected(track: TrackNode) {
   const selection = globals.state.selection;
   if (selection.kind !== 'area') return false;
-  return selection.tracks.includes(trackKey);
+  return selection.trackUris.includes(track.uri);
 }
 
 interface TrackChipAttrs {
@@ -135,13 +130,13 @@
 }
 
 interface TrackShellAttrs {
-  readonly trackKey: string;
   readonly title: m.Children;
   readonly buttons: m.Children;
   readonly tags?: TrackTags;
   readonly chips?: ReadonlyArray<string>;
   readonly button?: string;
   readonly pluginId?: string;
+  readonly track: TrackNode;
 }
 
 class TrackShell implements m.ClassComponent<TrackShellAttrs> {
@@ -155,14 +150,14 @@
     let highlightClass = undefined;
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
-      const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      if (trackKey === attrs.trackKey) {
+      const uri = globals.currentSearchResults.trackUris[searchIndex];
+      if (uri === attrs.track.uri) {
         highlightClass = 'flash';
       }
     }
 
     const currentSelection = globals.state.selection;
-    const pinned = isTrackPinned(attrs.trackKey);
+    const pinned = attrs.track.isPinned;
 
     return m(
       `.track-shell[draggable=true]`,
@@ -172,18 +167,18 @@
           this.dragging && 'drag',
           this.dropping && `drop-${this.dropping}`,
         ),
-        ondragstart: (e: DragEvent) => this.ondragstart(e, attrs.trackKey),
+        ondragstart: (e: DragEvent) => this.ondragstart(e, attrs.track),
         ondragend: this.ondragend.bind(this),
         ondragover: this.ondragover.bind(this),
         ondragleave: this.ondragleave.bind(this),
-        ondrop: (e: DragEvent) => this.ondrop(e, attrs.trackKey),
+        ondrop: (e: DragEvent) => this.ondrop(e, attrs.track),
       },
       m(
         '.track-menubar',
         m(
           'h1',
           {
-            title: attrs.title,
+            title: attrs.track.displayName,
           },
           attrs.title,
           attrs.chips && renderChips(attrs.chips),
@@ -193,13 +188,12 @@
           {className: 'track-buttons'},
           attrs.buttons,
           SHOW_TRACK_DETAILS_BUTTON.get() &&
-            this.renderTrackDetailsButton(pinned, attrs),
+            this.renderTrackDetailsButton(attrs),
           m(Button, {
             className: classNames(!pinned && 'pf-visible-on-hover'),
             onclick: () => {
-              globals.dispatch(
-                Actions.toggleTrackPinned({trackKey: attrs.trackKey}),
-              );
+              pinned ? attrs.track.unpin() : attrs.track.pin();
+              raf.scheduleFullRedraw();
             },
             icon: Icons.Pin,
             iconFilled: pinned,
@@ -210,18 +204,17 @@
             ? m(Button, {
                 onclick: (e: MouseEvent) => {
                   globals.dispatch(
-                    Actions.toggleTrackSelection({
-                      key: attrs.trackKey,
-                      isTrackGroup: false,
+                    Actions.toggleTrackAreaSelection({
+                      key: attrs.track.uri,
                     }),
                   );
                   e.stopPropagation();
                 },
                 compact: true,
-                icon: isTrackSelected(attrs.trackKey)
+                icon: isTrackSelected(attrs.track)
                   ? Icons.Checkbox
                   : Icons.BlankCheckbox,
-                title: isTrackSelected(attrs.trackKey)
+                title: isTrackSelected(attrs.track)
                   ? 'Remove track'
                   : 'Add track to selection',
               })
@@ -231,12 +224,12 @@
     );
   }
 
-  ondragstart(e: DragEvent, trackKey: string) {
+  ondragstart(e: DragEvent, track: TrackNode) {
     const dataTransfer = e.dataTransfer;
     if (dataTransfer === null) return;
     this.dragging = true;
     raf.scheduleFullRedraw();
-    dataTransfer.setData('perfetto/track', `${trackKey}`);
+    dataTransfer.setData('perfetto/track', `${track.uri}`);
     dataTransfer.setDragImage(new Image(), 0, 0);
   }
 
@@ -269,23 +262,30 @@
     raf.scheduleFullRedraw();
   }
 
-  ondrop(e: DragEvent, trackKey: string) {
+  ondrop(e: DragEvent, track: TrackNode) {
     if (this.dropping === undefined) return;
     const dataTransfer = e.dataTransfer;
     if (dataTransfer === null) return;
     raf.scheduleFullRedraw();
     const srcId = dataTransfer.getData('perfetto/track');
-    const dstId = trackKey;
-    globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
+    const dstId = track.uri;
+    console.log(srcId, dstId);
+    // globals.dispatch(Actions.moveTrack({srcId, op: this.dropping, dstId}));
     this.dropping = undefined;
   }
 
-  private renderTrackDetailsButton(pinned: boolean, attrs: TrackShellAttrs) {
+  private renderTrackDetailsButton(attrs: TrackShellAttrs) {
+    let parent = attrs.track.parent;
+    let fullPath: m.ChildArray = [attrs.track.displayName];
+    while (parent && parent !== globals.workspace) {
+      fullPath = [parent.displayName, ' \u2023 ', ...fullPath];
+      parent = parent.parent;
+    }
     return m(
       Popup,
       {
         trigger: m(Button, {
-          className: classNames(!pinned && 'pf-visible-on-hover'),
+          className: 'pf-visible-on-hover',
           icon: 'info',
           title: 'Show track details',
           compact: true,
@@ -298,10 +298,14 @@
           Tree,
           m(TreeNode, {
             left: 'URI',
-            right: globals.state.tracks[attrs.trackKey]?.uri,
+            right: attrs.track.uri,
           }),
-          m(TreeNode, {left: 'Title', right: attrs.title}),
-          m(TreeNode, {left: 'Track Key', right: attrs.trackKey}),
+          m(TreeNode, {
+            left: 'Key',
+            right: attrs.track.uri,
+          }),
+          m(TreeNode, {left: 'Path', right: fullPath}),
+          m(TreeNode, {left: 'Display Name', right: attrs.track.displayName}),
           m(TreeNode, {left: 'Plugin ID', right: attrs.pluginId}),
           m(
             TreeNode,
@@ -409,7 +413,6 @@
 }
 
 interface TrackComponentAttrs {
-  readonly trackKey: string;
   readonly heightPx?: number;
   readonly title: m.Children;
   readonly buttons?: m.Children;
@@ -417,8 +420,8 @@
   readonly chips?: ReadonlyArray<string>;
   readonly track?: Track;
   readonly error?: Error | undefined;
-  readonly closeable: boolean;
   readonly pluginId?: string;
+  readonly trackNode: TrackNode;
 
   // Issues a scrollTo() on this DOM element at creation time. Default: false.
   revealOnCreate?: boolean;
@@ -442,20 +445,19 @@
           // Round up to the nearest integer number of pixels.
           height: `${Math.ceil(trackHeight)}px`,
         },
-        id: 'track_' + attrs.trackKey,
+        id: 'track_' + attrs.trackNode.uri,
       },
       [
         m(TrackShell, {
           buttons: [
             attrs.error && m(CrashButton, {error: attrs.error}),
-            attrs.closeable && m(CloseTrackButton, {trackKey: attrs.trackKey}),
             attrs.buttons,
           ],
           title: attrs.title,
-          trackKey: attrs.trackKey,
           tags: attrs.tags,
           chips: attrs.chips,
           pluginId: attrs.pluginId,
+          track: attrs.trackNode,
         }),
         attrs.track &&
           m(TrackContent, {
@@ -469,9 +471,9 @@
 
   oncreate(vnode: m.VnodeDOM<TrackComponentAttrs>) {
     const {attrs} = vnode;
-    if (globals.scrollToTrackKey === attrs.trackKey) {
-      verticalScrollToTrack(attrs.trackKey);
-      globals.scrollToTrackKey = undefined;
+    if (globals.scrollToTrackUri === attrs.trackNode.uri) {
+      vnode.dom.scrollIntoView();
+      globals.scrollToTrackUri = undefined;
     }
     this.onupdate(vnode);
 
@@ -486,14 +488,13 @@
 }
 
 interface TrackPanelAttrs {
-  readonly trackKey: string;
   readonly title: m.Children;
   readonly tags?: TrackTags;
   readonly chips?: ReadonlyArray<string>;
-  readonly trackFSM?: TrackCacheEntry;
+  readonly trackRenderer?: TrackRenderer;
   readonly revealOnCreate?: boolean;
-  readonly closeable: boolean;
   readonly pluginId?: string;
+  readonly track: TrackNode;
 }
 
 export class TrackPanel implements Panel {
@@ -502,46 +503,43 @@
 
   constructor(private readonly attrs: TrackPanelAttrs) {}
 
-  get trackKey(): string {
-    return this.attrs.trackKey;
+  get trackUri(): string {
+    return this.attrs.track.uri;
   }
 
   render(): m.Children {
     const attrs = this.attrs;
 
-    if (attrs.trackFSM) {
-      if (attrs.trackFSM.getError()) {
+    if (attrs.trackRenderer) {
+      if (attrs.trackRenderer.getError()) {
         return m(TrackComponent, {
           title: attrs.title,
-          trackKey: attrs.trackKey,
-          error: attrs.trackFSM.getError(),
-          track: attrs.trackFSM.track,
-          closeable: attrs.closeable,
+          error: attrs.trackRenderer.getError(),
+          track: attrs.trackRenderer.track,
           chips: attrs.chips,
           pluginId: attrs.pluginId,
+          trackNode: attrs.track,
         });
       }
       return m(TrackComponent, {
-        trackKey: attrs.trackKey,
         title: attrs.title,
-        heightPx: attrs.trackFSM.track.getHeight(),
-        buttons: attrs.trackFSM.track.getTrackShellButtons?.(),
+        heightPx: attrs.trackRenderer.track.getHeight(),
+        buttons: attrs.trackRenderer.track.getTrackShellButtons?.(),
         tags: attrs.tags,
-        track: attrs.trackFSM.track,
-        error: attrs.trackFSM.getError(),
+        track: attrs.trackRenderer.track,
+        error: attrs.trackRenderer.getError(),
         revealOnCreate: attrs.revealOnCreate,
-        closeable: attrs.closeable,
         chips: attrs.chips,
         pluginId: attrs.pluginId,
+        trackNode: attrs.track,
       });
     } else {
       return m(TrackComponent, {
-        trackKey: attrs.trackKey,
         title: attrs.title,
         revealOnCreate: attrs.revealOnCreate,
-        closeable: attrs.closeable,
         chips: attrs.chips,
         pluginId: attrs.pluginId,
+        trackNode: attrs.track,
       });
     }
   }
@@ -556,7 +554,7 @@
       return;
     }
     const selectedAreaDuration = selection.end - selection.start;
-    if (selection.tracks.includes(this.attrs.trackKey)) {
+    if (selection.trackUris.includes(this.attrs.track.uri)) {
       ctx.fillStyle = SELECTION_FILL_COLOR;
       ctx.fillRect(
         timescale.timeToPx(selection.start),
@@ -582,11 +580,11 @@
     );
     drawGridLines(ctx, timespan, timescale, trackSize);
 
-    const track = this.attrs.trackFSM;
+    const track = this.attrs.trackRenderer;
 
     if (track !== undefined) {
       const trackRenderCtx: TrackRenderContext = {
-        trackKey: track.trackKey,
+        trackUri: track.desc.uri,
         visibleWindow,
         size: trackSize,
         resolution: calculateResolution(visibleWindow, trackSize.width),
@@ -612,10 +610,10 @@
   }
 
   getSliceVerticalBounds(depth: number): Optional<VerticalBounds> {
-    if (this.attrs.trackFSM === undefined) {
+    if (this.attrs.trackRenderer === undefined) {
       return undefined;
     }
-    return this.attrs.trackFSM.track.getSliceVerticalBounds?.(depth);
+    return this.attrs.trackRenderer.track.getSliceVerticalBounds?.(depth);
   }
 }
 
diff --git a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index 832fe44..51a29f1 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -114,7 +114,7 @@
     if (selection.kind !== 'GENERIC_SLICE') {
       return false;
     }
-    return selection.trackKey === this.trackKey;
+    return selection.trackUri === this.uri;
   }
 
   onSliceClick(args: OnSliceClickArgs<Slice>) {
@@ -129,7 +129,7 @@
         sqlTableName: this.tableName,
         start: args.slice.ts,
         duration: args.slice.dur,
-        trackKey: this.trackKey,
+        trackUri: this.uri,
         detailsPanelConfig: {
           kind: detailsPanelConfig.kind,
           config: detailsPanelConfig.config,
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/ui_main.ts
similarity index 75%
rename from ui/src/frontend/app.ts
rename to ui/src/frontend/ui_main.ts
index 872c232..7e62524 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/ui_main.ts
@@ -28,7 +28,7 @@
   TimestampFormat,
 } from '../core/timestamp_format';
 import {raf} from '../core/raf_scheduler';
-import {Command, Engine, addDebugSliceTrack} from '../public';
+import {Command} from '../public';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
 import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
@@ -52,10 +52,7 @@
 } from './keyboard_event_handler';
 import {publishPermalinkHash} from './publish';
 import {OmniboxMode, PromptOption} from './omnibox_manager';
-import {Utid} from '../trace_processor/sql_utils/core_types';
-import {THREAD_STATE_TRACK_KIND} from '../core/track_kinds';
 import {DisposableStack} from '../base/disposable_stack';
-import {getThreadInfo} from '../trace_processor/sql_utils/thread';
 
 function renderPermalink(): m.Children {
   const hash = globals.permalinkHash;
@@ -81,36 +78,7 @@
   }
 }
 
-const criticalPathSliceColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'name',
-};
-const criticalPathsliceColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'name',
-  'table_name',
-];
-
-const criticalPathsliceLiteColumns = {
-  ts: 'ts',
-  dur: 'dur',
-  name: 'thread_name',
-};
-const criticalPathsliceLiteColumnNames = [
-  'id',
-  'utid',
-  'ts',
-  'dur',
-  'thread_name',
-  'process_name',
-  'table_name',
-];
-
-export class App implements m.ClassComponent {
+export class UiMain implements m.ClassComponent {
   private trash = new DisposableStack();
   static readonly OMNIBOX_INPUT_REF = 'omnibox';
   private omniboxInputEl?: HTMLInputElement;
@@ -121,37 +89,6 @@
     this.trash.use(new AggregationsTabs());
   }
 
-  private getEngine(): Engine | undefined {
-    const engineId = globals.getCurrentEngine()?.id;
-    if (engineId === undefined) {
-      return undefined;
-    }
-    const engine = globals.engines.get(engineId)?.getProxy('QueryPage');
-    return engine;
-  }
-
-  private getFirstUtidOfSelectionOrVisibleWindow(): number {
-    const selection = globals.state.selection;
-    if (selection.kind === 'area') {
-      const firstThreadStateTrack = selection.tracks.find((trackId) => {
-        return globals.state.tracks[trackId];
-      });
-
-      if (firstThreadStateTrack) {
-        const trackInfo = globals.state.tracks[firstThreadStateTrack];
-        const trackDesc = globals.trackManager.resolveTrackInfo(trackInfo.uri);
-        if (
-          trackDesc?.tags?.kind === THREAD_STATE_TRACK_KIND &&
-          trackDesc?.tags?.utid !== undefined
-        ) {
-          return trackDesc.tags.utid;
-        }
-      }
-    }
-
-    return 0;
-  }
-
   private cmds: Command[] = [
     {
       id: 'perfetto.SetTimestampFormat',
@@ -205,116 +142,6 @@
       },
     },
     {
-      id: 'perfetto.CriticalPathLite',
-      name: `Critical path lite`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                   SELECT
-                      cr.id,
-                      cr.utid,
-                      cr.ts,
-                      cr.dur,
-                      thread.name AS thread_name,
-                      process.name AS process_name,
-                      'thread_state' AS table_name
-                    FROM
-                      _thread_executing_span_critical_path(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}) cr
-                    JOIN thread USING(utid)
-                    JOIN process USING(upid)
-                  `,
-              columns: criticalPathsliceLiteColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathsliceLiteColumns,
-            criticalPathsliceLiteColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPath',
-      name: `Critical path`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          await engine.query(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-          );
-          await addDebugSliceTrack(
-            // NOTE(stevegolton): This is a temporary patch, this menu should
-            // become part of a critical path plugin, at which point we can just
-            // use the plugin's context object.
-            {
-              engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
-            },
-            {
-              sqlSource: `
-                        SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
-                        FROM
-                        _critical_path_stack(
-                          ${trackUtid},
-                          ${window.start},
-                          ${window.end} - ${window.start}, 1, 1, 1, 1) cr WHERE name IS NOT NULL
-                  `,
-              columns: criticalPathsliceColumnNames,
-            },
-            (await getThreadInfo(engine, trackUtid as Utid)).name ??
-              '<thread name>',
-            criticalPathSliceColumns,
-            criticalPathsliceColumnNames,
-          );
-        }
-      },
-    },
-    {
-      id: 'perfetto.CriticalPathPprof',
-      name: `Critical path pprof`,
-      callback: async () => {
-        const trackUtid = this.getFirstUtidOfSelectionOrVisibleWindow();
-        const window = await getTimeSpanOfSelectionOrVisibleWindow();
-        const engine = this.getEngine();
-
-        if (engine !== undefined && trackUtid != 0) {
-          addQueryResultsTab({
-            query: `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;
-                   SELECT *
-                      FROM
-                        _thread_executing_span_critical_path_graph(
-                        "criical_path",
-                         ${trackUtid},
-                         ${window.start},
-                         ${window.end} - ${window.start}) cr`,
-            title: 'Critical path',
-          });
-        }
-      },
-    },
-    {
       id: 'perfetto.TogglePerformanceMetrics',
       name: 'Toggle performance metrics',
       callback: () => {
@@ -384,7 +211,6 @@
       id: 'perfetto.Deselect',
       name: 'Deselect',
       callback: () => {
-        globals.timeline.deselectArea();
         globals.clearSelection();
         globals.dispatch(Actions.removeNote({id: '0'}));
       },
@@ -482,22 +308,22 @@
             // If the current selection is an area which does not cover the
             // entire time range, preserve the list of selected tracks and
             // expand the time range.
-            tracksToSelect = selection.tracks;
+            tracksToSelect = selection.trackUris;
           } else {
             // If the entire time range is already covered, update the selection
             // to cover all tracks.
-            tracksToSelect = Object.keys(globals.state.tracks);
+            tracksToSelect = globals.workspace.flatTracks.map((t) => t.uri);
           }
         } else {
           // If the current selection is not an area, select all.
-          tracksToSelect = Object.keys(globals.state.tracks);
+          tracksToSelect = globals.workspace.flatTracks.map((t) => t.uri);
         }
         const {start, end} = globals.traceContext;
         globals.dispatch(
           Actions.selectArea({
             start,
             end,
-            tracks: tracksToSelect,
+            trackUris: tracksToSelect,
           }),
         );
       },
@@ -562,7 +388,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: prompt.text,
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'prompt-mode',
       closeOnOutsideClick: true,
       options,
@@ -622,7 +448,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: 'Filter commands...',
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'command-mode',
       options,
       closeOnSubmit: true,
@@ -666,7 +492,7 @@
     return m(Omnibox, {
       value: globals.omnibox.text,
       placeholder: ph,
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       extraClasses: 'query-mode',
 
       onInput: (value) => {
@@ -703,7 +529,7 @@
     return m(Omnibox, {
       value: globals.state.omniboxState.omnibox,
       placeholder: "Search or type '>' for commands or ':' for SQL mode",
-      inputRef: App.OMNIBOX_INPUT_REF,
+      inputRef: UiMain.OMNIBOX_INPUT_REF,
       onInput: (value, prev) => {
         if (prev === '') {
           if (value === '>') {
@@ -822,7 +648,7 @@
   }
 
   private updateOmniboxInputRef(dom: Element): void {
-    const el = findRef(dom, App.OMNIBOX_INPUT_REF);
+    const el = findRef(dom, UiMain.OMNIBOX_INPUT_REF);
     if (el && el instanceof HTMLInputElement) {
       this.omniboxInputEl = el;
     }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index e5d5ae5..40d851e 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -18,10 +18,8 @@
 import {clamp} from '../base/math_utils';
 import {Time} from '../base/time';
 import {Actions} from '../common/actions';
-import {TrackCacheEntry} from '../common/track_cache';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
-import {TrackTags} from '../public';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {globals} from './globals';
@@ -30,7 +28,6 @@
 import {createPage} from './pages';
 import {PanAndZoomHandler} from './pan_and_zoom_handler';
 import {
-  Panel,
   PanelContainer,
   PanelOrGroup,
   RenderedPanelInfo,
@@ -45,9 +42,10 @@
 import {TrackPanel, getTitleFontSize} from './track_panel';
 import {assertExists} from '../base/logging';
 import {PxSpan, TimeScale} from './time_scale';
-import {TrackGroupState} from '../common/state';
-import {FuzzyFinder, fuzzyMatch, FuzzySegment} from '../base/fuzzy';
-import {exists} from '../base/utils';
+import {GroupNode, Node, TrackNode} from '../public/workspace';
+import {fuzzyMatch, FuzzySegment} from '../base/fuzzy';
+
+import {exists, Optional} from '../base/utils';
 import {EmptyState} from '../widgets/empty_state';
 import {removeFalsyValues} from '../base/array_utils';
 import {renderFlows} from './flow_events_renderer';
@@ -195,7 +193,7 @@
             timeline.selectArea(
               Time.max(Time.min(keepTime, newTime), traceTime.start),
               Time.min(Time.max(keepTime, newTime), traceTime.end),
-              selection.tracks,
+              selection.trackUris,
             );
           }
         } else {
@@ -276,17 +274,16 @@
         ),
         m(PanelContainer, {
           className: 'pinned-panel-container',
-          panels: globals.state.pinnedTracks.map((key) => {
-            const trackBundle = resolveTrack(key);
+          panels: globals.workspace.pinnedTracks.map((track) => {
+            const tr = globals.trackManager.getTrackRenderer(track.uri);
             return new TrackPanel({
-              trackKey: key,
-              title: trackBundle.title,
-              tags: trackBundle.tags,
-              trackFSM: trackBundle.trackFSM,
+              track: track,
+              title: track.displayName,
+              tags: tr?.desc.tags,
+              trackRenderer: tr,
               revealOnCreate: true,
-              closeable: trackBundle.closeable,
-              chips: trackBundle.chips,
-              pluginId: trackBundle.pluginId,
+              chips: tr?.desc.chips,
+              pluginId: tr?.desc.pluginId,
             });
           }),
         }),
@@ -355,160 +352,128 @@
 }
 
 // Render the toplevel "scrolling" tracks and track groups
-function renderToplevelPanels(filterTerm: string | undefined): PanelOrGroup[] {
-  const scrollingPanels: PanelOrGroup[] = renderTrackPanels(
-    globals.state.scrollingTracks,
-    filterTerm,
-  );
-
-  for (const group of Object.values(globals.state.trackGroups)) {
-    if (filterTermIsValid(filterTerm)) {
-      const tokens = tokenizeFilterTerm(filterTerm);
-      // Match group names that match any of the tokens
-      const result = fuzzyMatch(group.name, ...tokens);
-      if (result.matches) {
-        // If the group name matches, render the entire group as normal
-        const title = renderFuzzyMatchedTrackTitle(result.segments);
-        scrollingPanels.push({
-          kind: 'group',
-          collapsed: group.collapsed,
-          childPanels: group.collapsed ? [] : renderTrackPanels(group.tracks),
-          header: renderTrackGroupPanel(group, true, group.collapsed, title),
-        });
-      } else {
-        // If we are filtering, render the group header only if it contains
-        // matching tracks
-        const childPanels = renderTrackPanels(group.tracks, filterTerm);
-        if (childPanels.length === 0) continue;
-        scrollingPanels.push({
-          kind: 'group',
-          collapsed: false,
-          childPanels,
-          header: renderTrackGroupPanel(group, false, false),
-        });
-      }
-    } else {
-      // Always render the group header, but only render child tracks if not
-      // collapsed
-      scrollingPanels.push({
-        kind: 'group',
-        collapsed: group.collapsed,
-        childPanels: group.collapsed ? [] : renderTrackPanels(group.tracks),
-        header: renderTrackGroupPanel(group, true, group.collapsed),
-      });
-    }
-  }
-
-  return scrollingPanels;
+function renderToplevelPanels(filterTerm: Optional<string>): PanelOrGroup[] {
+  return renderNodes(globals.workspace.children, filterTerm);
 }
 
 // Given a list of tracks and a filter term, return a list pf panels filtered by
 // the filter term
-function renderTrackPanels(trackKeys: string[], filterTerm?: string): Panel[] {
-  if (filterTermIsValid(filterTerm)) {
-    const tokens = tokenizeFilterTerm(filterTerm);
-    const matcher = new FuzzyFinder(trackKeys, (key) => {
-      return globals.state.tracks[key].name;
-    });
-    // Filter tracks which match any of the tokens
-    const filtered = matcher.find(...tokens);
-    return filtered.map(({item: key, segments}) => {
-      return renderTrackPanel(key, renderFuzzyMatchedTrackTitle(segments));
-    });
-  } else {
-    // No point in applying any filtering...
-    return trackKeys.map((key) => {
-      return renderTrackPanel(key);
-    });
-  }
+function renderNodes(
+  nodes: ReadonlyArray<Node>,
+  filterTerm?: string,
+): PanelOrGroup[] {
+  return nodes.flatMap((node) => {
+    if (node instanceof GroupNode) {
+      if (node.headless) {
+        return renderNodes(node.children, filterTerm);
+      } else {
+        if (filterTermIsValid(filterTerm)) {
+          const tokens = tokenizeFilterTerm(filterTerm);
+          const match = fuzzyMatch(node.displayName, ...tokens);
+          if (match.matches) {
+            return {
+              kind: 'group',
+              collapsed: node.collapsed,
+              header: renderGroupHeaderPanel(
+                node,
+                true,
+                node.collapsed,
+                renderFuzzyMatchedTrackTitle(match.segments),
+              ),
+              childPanels: node.collapsed ? [] : renderNodes(node.children),
+            };
+          } else {
+            const childPanels = renderNodes(node.children, filterTerm);
+            if (childPanels.length > 0) {
+              return {
+                kind: 'group',
+                collapsed: false,
+                header: renderGroupHeaderPanel(node, false, node.collapsed),
+                childPanels,
+              };
+            }
+            return [];
+          }
+        } else {
+          return {
+            kind: 'group',
+            collapsed: node.collapsed,
+            header: renderGroupHeaderPanel(node, true, node.collapsed),
+            childPanels: node.collapsed
+              ? []
+              : renderNodes(node.children, filterTerm),
+          };
+        }
+      }
+    } else {
+      if (filterTermIsValid(filterTerm)) {
+        const tokens = tokenizeFilterTerm(filterTerm);
+        const match = fuzzyMatch(node.displayName, ...tokens);
+        if (match.matches) {
+          return renderTrackPanel(
+            node,
+            renderFuzzyMatchedTrackTitle(match.segments),
+          );
+        } else {
+          return [];
+        }
+      } else {
+        return renderTrackPanel(node);
+      }
+    }
+  });
 }
 
-function renderTrackPanel(key: string, title?: m.Children) {
-  const trackBundle = resolveTrack(key);
+function renderTrackPanel(track: TrackNode, title?: m.Children) {
+  const tr = globals.trackManager.getTrackRenderer(track.uri);
   return new TrackPanel({
-    trackKey: key,
+    track: track,
     title: m(
       'span',
       {
         style: {
-          'font-size': getTitleFontSize(trackBundle.title),
+          'font-size': getTitleFontSize(track.displayName),
         },
       },
-      Boolean(title) ? title : trackBundle.title,
+      Boolean(title) ? title : track.displayName,
     ),
-    tags: trackBundle.tags,
-    trackFSM: trackBundle.trackFSM,
-    closeable: trackBundle.closeable,
-    chips: trackBundle.chips,
-    pluginId: trackBundle.pluginId,
+    tags: tr?.desc.tags,
+    trackRenderer: tr,
+    chips: tr?.desc.chips,
+    pluginId: tr?.desc.pluginId,
   });
 }
 
-function renderTrackGroupPanel(
-  group: TrackGroupState,
+function renderGroupHeaderPanel(
+  group: GroupNode,
   collapsable: boolean,
   collapsed: boolean,
   title?: m.Children,
 ): TrackGroupPanel {
-  const summaryTrackKey = group.summaryTrack;
-
-  if (exists(summaryTrackKey)) {
-    const trackBundle = resolveTrack(summaryTrackKey);
+  if (group.headerTrackUri !== undefined) {
+    const tr = globals.trackManager.getTrackRenderer(group.headerTrackUri);
     return new TrackGroupPanel({
-      groupKey: group.key,
-      trackFSM: trackBundle.trackFSM,
-      subtitle: trackBundle.subtitle,
-      tags: trackBundle.tags,
-      chips: trackBundle.chips,
+      groupNode: group,
+      trackRenderer: tr,
+      subtitle: tr?.desc.subtitle,
+      tags: tr?.desc.tags,
+      chips: tr?.desc.chips,
       collapsed,
-      title: exists(title) ? title : group.name,
-      tooltip: group.name,
+      title: exists(title) ? title : group.displayName,
+      tooltip: group.displayName,
       collapsable,
     });
   } else {
     return new TrackGroupPanel({
-      groupKey: group.key,
+      groupNode: group,
       collapsed,
-      title: exists(title) ? title : group.name,
-      tooltip: group.name,
+      title: exists(title) ? title : group.displayName,
+      tooltip: group.displayName,
       collapsable,
     });
   }
 }
 
-// Resolve a track and its metadata through the track cache
-function resolveTrack(key: string): TrackBundle {
-  const trackState = globals.state.tracks[key];
-  const {uri, name, closeable} = trackState;
-  const trackDesc = globals.trackManager.resolveTrackInfo(uri);
-  const trackCacheEntry =
-    trackDesc && globals.trackManager.resolveTrack(key, trackDesc);
-  const trackFSM = trackCacheEntry;
-  const tags = trackCacheEntry?.desc.tags;
-  const subtitle = trackCacheEntry?.desc.subtitle;
-  const chips = trackCacheEntry?.desc.chips;
-  const plugin = trackCacheEntry?.desc.pluginId;
-  return {
-    title: name,
-    subtitle,
-    closeable: closeable ?? false,
-    tags,
-    trackFSM,
-    chips,
-    pluginId: plugin,
-  };
-}
-
-interface TrackBundle {
-  readonly title: string;
-  readonly subtitle?: string;
-  readonly closeable: boolean;
-  readonly trackFSM?: TrackCacheEntry;
-  readonly tags?: TrackTags;
-  readonly chips?: ReadonlyArray<string>;
-  readonly pluginId?: string;
-}
-
 export const ViewerPage = createPage({
   view() {
     return m(TraceViewer);
diff --git a/ui/src/frontend/visualized_args_track.ts b/ui/src/frontend/visualized_args_track.ts
index d2064bc..be81eaf 100644
--- a/ui/src/frontend/visualized_args_track.ts
+++ b/ui/src/frontend/visualized_args_track.ts
@@ -14,17 +14,16 @@
 
 import m from 'mithril';
 
-import {Actions} from '../common/actions';
-import {globals} from './globals';
 import {Button} from '../widgets/button';
 import {Icons} from '../base/semantic_icons';
 import {ThreadSliceTrack} from './thread_slice_track';
 import {uuidv4Sql} from '../base/uuid';
 import {Engine} from '../trace_processor/engine';
 import {createView} from '../trace_processor/sql_utils';
+import {globals} from './globals';
 
 export interface VisualizedArgsTrackAttrs {
-  readonly trackKey: string;
+  readonly uri: string;
   readonly engine: Engine;
   readonly trackId: number;
   readonly maxDepth: number;
@@ -36,7 +35,7 @@
   private readonly argName: string;
 
   constructor({
-    trackKey,
+    uri,
     engine,
     trackId,
     maxDepth,
@@ -46,7 +45,7 @@
     const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
     const viewName = `__arg_visualisation_helper_${escapedArgName}_${uuid}_slice`;
 
-    super({engine, trackKey}, trackId, maxDepth, viewName);
+    super({engine, uri}, trackId, maxDepth, viewName);
     this.viewName = viewName;
     this.argName = argName;
   }
@@ -84,13 +83,7 @@
   getTrackShellButtons(): m.Children {
     return m(Button, {
       onclick: () => {
-        // This behavior differs to the original behavior a little.
-        // Originally, hitting the close button on a single track removed ALL
-        // tracks with this argName, whereas this one only closes the single
-        // track.
-        // This will be easily fixable once we transition to using dynamic
-        // tracks instead of this "initial state" approach to add these tracks.
-        globals.dispatch(Actions.removeTracks({trackKeys: [this.trackKey]}));
+        globals.workspace.getTrackByUri(this.uri)?.remove();
       },
       icon: Icons.Close,
       title: 'Close',
diff --git a/ui/src/frontend/visualized_args_tracks.ts b/ui/src/frontend/visualized_args_tracks.ts
index 9aea25e..eb7c3bc 100644
--- a/ui/src/frontend/visualized_args_tracks.ts
+++ b/ui/src/frontend/visualized_args_tracks.ts
@@ -12,15 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {assertExists} from '../base/logging';
 import {uuidv4} from '../base/uuid';
-import {Actions, AddTrackArgs} from '../common/actions';
-import {InThreadTrackSortKey} from '../common/state';
+// import {THREAD_SLICE_TRACK_KIND} from '../public';
 import {TrackDescriptor} from '../public/tracks';
 import {Engine} from '../trace_processor/engine';
 import {NUM} from '../trace_processor/query_result';
 import {globals} from './globals';
 import {VisualisedArgsTrack} from './visualized_args_track';
+import {TrackNode} from '../public/workspace';
 
 const VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX = 'perfetto.VisualisedArgs';
 
@@ -75,48 +74,40 @@
         group by track_id;
     `);
 
-  const tracksToAdd: AddTrackArgs[] = [];
   const it = result.iter({trackId: NUM, maxDepth: NUM});
-  const addedTrackKeys: string[] = [];
   for (; it.valid(); it.next()) {
     const trackId = it.trackId;
     const maxDepth = it.maxDepth;
-    const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
-    const track = globals.state.tracks[assertExists(trackKey)];
-    const utid = (track.trackSortKey as {utid?: number}).utid;
-    const key = uuidv4();
-    addedTrackKeys.push(key);
 
     const uri = `${VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX}#${uuidv4()}`;
     ctx.registerTrack({
       uri,
       title: argName,
       chips: ['metric'],
-      trackFactory: (trackCtx) => {
-        return new VisualisedArgsTrack({
-          engine: ctx.engine,
-          trackKey: trackCtx.trackKey,
-          trackId,
-          maxDepth,
-          argName,
-        });
-      },
+      track: new VisualisedArgsTrack({
+        engine: ctx.engine,
+        uri,
+        trackId,
+        maxDepth,
+        argName,
+      }),
     });
 
-    tracksToAdd.push({
-      key,
-      trackGroup: track.trackGroup,
-      name: argName,
-      trackSortKey:
-        utid === undefined
-          ? track.trackSortKey
-          : {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
-      uri,
+    // Find the thread slice track that corresponds with this trackID and insert
+    // this track before it.
+    const threadSliceTrack = globals.workspace.flatTracks.find((trackNode) => {
+      const trackDescriptor = globals.trackManager.getTrack(trackNode.uri);
+      return (
+        trackDescriptor &&
+        trackDescriptor.tags?.kind === 'ThreadSliceTrack' &&
+        trackDescriptor.tags?.trackIds?.includes(trackId)
+      );
     });
+
+    const parentGroup = threadSliceTrack?.parent;
+    if (parentGroup) {
+      const newTrack = new TrackNode(uri, argName);
+      parentGroup.insertBefore(newTrack, threadSliceTrack);
+    }
   }
-
-  globals.dispatchMultiple([
-    Actions.addTracks({tracks: tracksToAdd}),
-    Actions.sortThreadTracks({}),
-  ]);
 }
diff --git a/ui/src/frontend/widgets/process.ts b/ui/src/frontend/widgets/process.ts
index 7b668aa..a1af563 100644
--- a/ui/src/frontend/widgets/process.ts
+++ b/ui/src/frontend/widgets/process.ts
@@ -34,6 +34,25 @@
 } from './sql/details/sql_ref_renderer_registry';
 import {asUpid} from '../../trace_processor/sql_utils/core_types';
 
+export function showProcessDetailsMenuItem(
+  upid: Upid,
+  pid?: number,
+): m.Children {
+  return m(MenuItem, {
+    icon: Icons.ExternalLink,
+    label: 'Show process details',
+    onclick: () =>
+      addEphemeralTab(
+        'processDetails',
+        new ProcessDetailsTab({
+          engine: getEngine('ProcessDetails'),
+          upid,
+          pid,
+        }),
+      ),
+  });
+}
+
 export function processRefMenuItems(info: {
   upid: Upid;
   name?: string;
@@ -59,19 +78,7 @@
       label: 'Copy upid',
       onclick: () => copyToClipboard(`${info.upid}`),
     }),
-    m(MenuItem, {
-      icon: Icons.ExternalLink,
-      label: 'Show process details',
-      onclick: () =>
-        addEphemeralTab(
-          'processDetails',
-          new ProcessDetailsTab({
-            engine: getEngine('ProcessDetails'),
-            upid: info.upid,
-            pid: info.pid,
-          }),
-        ),
-    }),
+    showProcessDetailsMenuItem(info.upid, info.pid),
   ];
 }
 
diff --git a/ui/src/frontend/widgets/sched.ts b/ui/src/frontend/widgets/sched.ts
index 03ab24e..2ee8886 100644
--- a/ui/src/frontend/widgets/sched.ts
+++ b/ui/src/frontend/widgets/sched.ts
@@ -20,7 +20,6 @@
 import {globals} from '../globals';
 import {CPU_SLICE_TRACK_KIND} from '../../core/track_kinds';
 import {scrollToTrackAndTs} from '../scroll_helper';
-import {exists} from '../../base/utils';
 
 interface SchedRefAttrs {
   id: SchedSqlId;
@@ -37,29 +36,21 @@
 }
 
 export function findSchedTrack(cpu: number): string | undefined {
-  for (const track of Object.values(globals.state.tracks)) {
-    if (exists(track?.uri)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        if (trackInfo?.tags?.cpu === cpu) {
-          return track.key;
-        }
-      }
-    }
-  }
-  return undefined;
+  return globals.trackManager.findTrack((t) => {
+    return t.tags?.kind === CPU_SLICE_TRACK_KIND && t.tags.cpu === cpu;
+  })?.uri;
 }
 
 export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: time) {
-  const trackKey = findSchedTrack(cpu);
-  if (trackKey === undefined) {
+  const trackUri = findSchedTrack(cpu);
+  if (trackUri === undefined) {
     return;
   }
   globals.setLegacySelection(
     {
       kind: 'SCHED_SLICE',
       id,
-      trackKey,
+      trackUri,
     },
     {
       clearSearch: true,
@@ -68,7 +59,7 @@
     },
   );
 
-  scrollToTrackAndTs(trackKey, ts);
+  scrollToTrackAndTs(trackUri, ts);
 }
 
 export class SchedRef implements m.ClassComponent<SchedRefAttrs> {
@@ -78,14 +69,14 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          const trackKey = findSchedTrack(vnode.attrs.cpu);
-          if (trackKey === undefined) return;
+          const trackUri = findSchedTrack(vnode.attrs.cpu);
+          if (trackUri === undefined) return;
 
           globals.setLegacySelection(
             {
               kind: 'SCHED_SLICE',
               id: vnode.attrs.id,
-              trackKey,
+              trackUri,
             },
             {
               clearSearch: true,
@@ -95,7 +86,7 @@
             },
           );
 
-          scrollToTrackAndTs(trackKey, vnode.attrs.ts, true);
+          scrollToTrackAndTs(trackUri, vnode.attrs.ts, true);
         },
       },
       vnode.attrs.name ?? `Sched ${vnode.attrs.id}`,
diff --git a/ui/src/frontend/widgets/slice.ts b/ui/src/frontend/widgets/slice.ts
index 3f6dc7a..d60a10e 100644
--- a/ui/src/frontend/widgets/slice.ts
+++ b/ui/src/frontend/widgets/slice.ts
@@ -51,10 +51,11 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          const trackKeyByTrackId = globals.trackManager.trackKeyByTrackId;
-          const trackKey = trackKeyByTrackId.get(vnode.attrs.sqlTrackId);
-          if (trackKey === undefined) return;
-          verticalScrollToTrack(trackKey, true);
+          const track = globals.trackManager.findTrack((td) => {
+            return td.tags?.trackIds?.includes(vnode.attrs.sqlTrackId);
+          });
+          if (track === undefined) return;
+          verticalScrollToTrack(track.uri, true);
           // Clamp duration to 1 - i.e. for instant events
           const dur = BigintMath.max(1n, vnode.attrs.dur);
           focusHorizontalRange(
@@ -66,7 +67,7 @@
             {
               kind: 'SLICE',
               id: vnode.attrs.id,
-              trackKey,
+              trackUri: track.uri,
               table: 'slice',
             },
             {
diff --git a/ui/src/frontend/widgets/sql/table/argument_selector.ts b/ui/src/frontend/widgets/sql/table/argument_selector.ts
index 0114e79..a2039af 100644
--- a/ui/src/frontend/widgets/sql/table/argument_selector.ts
+++ b/ui/src/frontend/widgets/sql/table/argument_selector.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 
 import {raf} from '../../../../core/raf_scheduler';
-import {FilterableSelect} from '../../../../widgets/select';
 import {Spinner} from '../../../../widgets/spinner';
 
 import {
@@ -24,6 +23,11 @@
   TableColumnSet,
   TableManager,
 } from './column';
+import {TextInput} from '../../../../widgets/text_input';
+import {scheduleFullRedraw} from '../../../../widgets/raf';
+import {hasModKey, modKey} from '../../../../base/hotkeys';
+import {MenuItem} from '../../../../widgets/menu';
+import {uuidv4} from '../../../../base/uuid';
 
 const MAX_ARGS_TO_DISPLAY = 15;
 
@@ -38,33 +42,85 @@
 export class ArgumentSelector
   implements m.ClassComponent<ArgumentSelectorAttrs>
 {
-  columns?: {[key: string]: TableColumn};
+  searchText = '';
+  columns?: {key: string; column: TableColumn}[];
 
   constructor({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
     this.load(attrs);
   }
 
   private async load(attrs: ArgumentSelectorAttrs) {
-    const potentialColumns = await attrs.columnSet.discover(attrs.tableManager);
-    this.columns = Object.fromEntries(
-      potentialColumns
-        .filter(
-          ({column}) =>
-            !attrs.alreadySelectedColumnIds.has(tableColumnId(column)),
-        )
-        .map(({key, column}) => [key, column]),
-    );
+    this.columns = await attrs.columnSet.discover(attrs.tableManager);
     raf.scheduleFullRedraw();
   }
 
   view({attrs}: m.Vnode<ArgumentSelectorAttrs>) {
     const columns = this.columns;
     if (columns === undefined) return m(Spinner);
-    return m(FilterableSelect, {
-      values: Object.keys(columns),
-      onSelected: (value: string) => attrs.onArgumentSelected(columns[value]),
-      maxDisplayedItems: MAX_ARGS_TO_DISPLAY,
-      autofocusInput: true,
+
+    // Candidates are the columns which have not been selected yet.
+    const candidates = columns.filter(
+      ({column}) => !attrs.alreadySelectedColumnIds.has(tableColumnId(column)),
+    );
+
+    // Filter the candidates based on the search text.
+    const filtered = candidates.filter(({key}) => {
+      return key.toLowerCase().includes(this.searchText.toLowerCase());
     });
+
+    const displayed = filtered.slice(0, MAX_ARGS_TO_DISPLAY);
+
+    const extraItems = Math.max(0, filtered.length - MAX_ARGS_TO_DISPLAY);
+
+    const firstButtonUuid = uuidv4();
+
+    return [
+      m(
+        '.pf-search-bar',
+        m(TextInput, {
+          autofocus: true,
+          oninput: (event: Event) => {
+            const eventTarget = event.target as HTMLTextAreaElement;
+            this.searchText = eventTarget.value;
+            scheduleFullRedraw();
+          },
+          onkeydown: (event: KeyboardEvent) => {
+            if (filtered.length === 0) return;
+            if (event.key === 'Enter') {
+              // If there is only one item or Mod-Enter was pressed, select the first element.
+              if (filtered.length === 1 || hasModKey(event)) {
+                const params = {bubbles: true};
+                if (hasModKey(event)) {
+                  Object.assign(params, modKey());
+                }
+                const pointerEvent = new PointerEvent('click', params);
+                (
+                  document.getElementById(firstButtonUuid) as HTMLElement | null
+                )?.dispatchEvent(pointerEvent);
+              }
+            }
+          },
+          value: this.searchText,
+          placeholder: 'Filter...',
+          className: 'pf-search-box',
+        }),
+      ),
+      ...displayed.map(({key, column}, index) =>
+        m(MenuItem, {
+          id: index === 0 ? firstButtonUuid : undefined,
+          label: key,
+          onclick: (event) => {
+            attrs.onArgumentSelected(column);
+            // For Control-Click, we don't want to close the menu to allow the user
+            // to select multiple items in one go.
+            if (hasModKey(event)) {
+              event.stopPropagation();
+            }
+            // Otherwise this popup will be closed.
+          },
+        }),
+      ),
+      Boolean(extraItems) && m('i', `+${extraItems} more`),
+    ];
   }
 }
diff --git a/ui/src/frontend/widgets/sql/table/column.ts b/ui/src/frontend/widgets/sql/table/column.ts
index bf1164a..1815551 100644
--- a/ui/src/frontend/widgets/sql/table/column.ts
+++ b/ui/src/frontend/widgets/sql/table/column.ts
@@ -30,6 +30,10 @@
 export type SourceTable = {
   table: string;
   joinOn: {[key: string]: SqlColumn};
+  // Whether more performant 'INNER JOIN' can be used instead of 'LEFT JOIN'.
+  // Special care should be taken to ensure that a) all rows exist in a target table, and b) the source is not null, otherwise the rows will be filtered out.
+  // false by default.
+  innerJoin?: boolean;
 };
 
 // A column in the SQL query. It can be either a column from a base table or a "lookup" column from a joined table.
@@ -41,17 +45,31 @@
     };
 
 // A unique identifier for the SQL column.
-export function sqlColumnId(column: SqlColumn) {
+export function sqlColumnId(column: SqlColumn): string {
   if (typeof column === 'string') {
     return column;
   }
-  // If the join is performed on a single column `id`, we can use a simpler representation (i.e. `table[id].column`).
+  // Special case: If the join is performed on a single column `id`, we can use a simpler representation (i.e. `table[id].column`).
   if (arrayEquals(Object.keys(column.source.joinOn), ['id'])) {
-    return `${column.source.table}[${Object.values(column.source.joinOn)[0]}].${column.column}`;
+    return `${column.source.table}[${sqlColumnId(Object.values(column.source.joinOn)[0])}].${column.column}`;
+  }
+  // Special case: args lookup. For it, we can use a simpler representation (i.e. `arg_set_id[key]`).
+  if (
+    column.column === 'display_value' &&
+    column.source.table === 'args' &&
+    arrayEquals(Object.keys(column.source.joinOn).sort(), ['arg_set_id', 'key'])
+  ) {
+    const key = column.source.joinOn['key'];
+    const argSetId = column.source.joinOn['arg_set_id'];
+    return `${sqlColumnId(argSetId)}[${sqlColumnId(key)}]`;
   }
   // Otherwise, we need to list all the join constraints.
   const lookup = Object.entries(column.source.joinOn)
-    .map(([key, value]): string => `${key}=${sqlColumnId(value)}`)
+    .map(([key, value]): string => {
+      const valueStr = sqlColumnId(value);
+      if (key === valueStr) return key;
+      return `${key}=${sqlColumnId(value)}`;
+    })
     .join(', ');
   return `${column.source.table}[${lookup}].${column.column}`;
 }
@@ -84,6 +102,10 @@
   startsHidden?: boolean;
 }
 
+export interface AggregationConfig {
+  dataType?: 'nominal' | 'quantitative';
+}
+
 // Class which represents a column in a table, which can be displayed to the user.
 // It is based on the primary SQL column, but also contains additional information needed for displaying it as a part of a table.
 export abstract class TableColumn {
@@ -122,6 +144,11 @@
     tableManager: TableManager,
     dependentColumns: {[key: string]: SqlValue},
   ): m.Children;
+
+  // Specifies how this column should be aggregated. If not set, then all
+  // numeric columns will be treated as quantitative, and all other columns as
+  // nominal.
+  aggregation?(): AggregationConfig;
 }
 
 // Returns a unique identifier for the table column.
diff --git a/ui/src/frontend/widgets/sql/table/column_unittest.ts b/ui/src/frontend/widgets/sql/table/column_unittest.ts
new file mode 100644
index 0000000..faa9a94
--- /dev/null
+++ b/ui/src/frontend/widgets/sql/table/column_unittest.ts
@@ -0,0 +1,138 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {sqlColumnId} from './column';
+import {argSqlColumn} from './well_known_columns';
+
+test('sql_column_id.basic', () => {
+  // Straightforward case: just a column selection.
+  expect(sqlColumnId('utid')).toBe('utid');
+});
+
+test('sql_column_id.single_join', () => {
+  expect(
+    sqlColumnId({
+      column: 'bar',
+      source: {
+        table: 'foo',
+        joinOn: {
+          foo_id: 'id',
+        },
+      },
+    }),
+  ).toBe('foo[foo_id=id].bar');
+});
+
+test('sql_column_id.double_join', () => {
+  expect(
+    sqlColumnId({
+      column: 'abc',
+      source: {
+        table: 'alphabet',
+        joinOn: {
+          abc_id: {
+            column: 'bar',
+            source: {
+              table: 'foo',
+              joinOn: {
+                foo_id: 'id',
+              },
+            },
+          },
+        },
+      },
+    }),
+  ).toBe('alphabet[abc_id=foo[foo_id=id].bar].abc');
+});
+
+test('sql_column_id.join_on_id', () => {
+  // Special case: joins on `id` should be simplified.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          id: 'foo_id',
+        },
+      },
+    }),
+  ).toBe('foo[foo_id].name');
+});
+
+test('sql_column_id.nested_join_on_id', () => {
+  // Special case: joins on `id` should be simplified in nested joins.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          id: {
+            column: 'foo_id',
+            source: {
+              table: 'bar',
+              joinOn: {
+                x: 'y',
+              },
+            },
+          },
+        },
+      },
+    }),
+  ).toBe('foo[bar[x=y].foo_id].name');
+});
+
+test('sql_column_id.simplied_join', () => {
+  // Special case: if both sides of the join are the same, only one can be shown.
+  expect(
+    sqlColumnId({
+      column: 'name',
+      source: {
+        table: 'foo',
+        joinOn: {
+          x: 'y',
+          z: 'z',
+        },
+      },
+    }),
+  ).toBe('foo[x=y, z].name');
+});
+
+test('sql_column_id.arg_set_id', () => {
+  // Special case: arg_set_id.
+  expect(sqlColumnId(argSqlColumn('arg_set_id', 'arg1'))).toBe(
+    "arg_set_id['arg1']",
+  );
+});
+
+test('sql_column_id.arg_set_id_with_join', () => {
+  // Special case: arg_set_id.
+  expect(
+    sqlColumnId(
+      argSqlColumn(
+        {
+          column: 'arg_set_id',
+          source: {
+            table: 'foo',
+            joinOn: {
+              x: 'y',
+            },
+          },
+        },
+        'arg1',
+      ),
+    ),
+  ).toBe("foo[x=y].arg_set_id['arg1']");
+});
diff --git a/ui/src/frontend/widgets/sql/table/query_builder.ts b/ui/src/frontend/widgets/sql/table/query_builder.ts
index 6e23288..be3400d 100644
--- a/ui/src/frontend/widgets/sql/table/query_builder.ts
+++ b/ui/src/frontend/widgets/sql/table/query_builder.ts
@@ -36,6 +36,7 @@
 type NormalisedSourceTable = {
   table: string;
   joinOn: {[key: string]: NormalisedSqlColumn};
+  innerJoin: boolean;
 };
 
 // Checks whether two join constraints are equal.
@@ -95,6 +96,7 @@
       const table = this.tables[i];
       if (
         table.table === column.source.table &&
+        table.innerJoin === (column.source.innerJoin ?? false) &&
         areJoinConstraintsEqual(table.joinOn, normalisedJoinOn)
       ) {
         return {
@@ -108,6 +110,7 @@
     this.tables.push({
       table: column.source.table,
       joinOn: normalisedJoinOn,
+      innerJoin: column.source.innerJoin ?? false,
     });
     return {
       column: column.column,
@@ -136,7 +139,7 @@
       ([key, value]) => `${alias}.${key} = ${this.printColumn(value)}`,
     );
     // Join IDs are 0-indexed, but we want to display them as 1-indexed to reserve 0 for the primary table.
-    return `LEFT JOIN ${join.table} AS ${alias} ON ${clauses.join(' AND ')}`;
+    return `${join.innerJoin ? '' : 'LEFT '}JOIN ${join.table} AS ${alias} ON ${clauses.join(' AND ')}`;
   }
 }
 
diff --git a/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts b/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts
index e5af636..1403dea 100644
--- a/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts
+++ b/ui/src/frontend/widgets/sql/table/query_builder_unittest.ts
@@ -107,6 +107,83 @@
   );
 });
 
+// Check a query with INNER JOIN instead of LEFT JOIN.
+test('query_builder.left_join', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'foo',
+        columns: {
+          foo_id: 'id',
+          slice_name: {
+            column: 'name',
+            source: {
+              table: 'slice',
+              innerJoin: true,
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      foo_0.id AS foo_id,
+      slice_1.name AS slice_name
+    FROM foo AS foo_0
+    JOIN slice AS slice_1 ON slice_1.id = foo_0.slice_id
+  `),
+  );
+});
+
+// Check a query which has both INNER JOIN and LEFT JOIN on the same table.
+// The correct behaviour here is debatable (probably we can upgrade INNER JOIN to LEFT JOIN),
+// but for now we just generate the query with two separate joins.
+test('query_builder.left_join_and_inner_join', () => {
+  expect(
+    normalise(
+      buildSqlQuery({
+        table: 'foo',
+        columns: {
+          foo_id: 'id',
+          slice_name: {
+            column: 'name',
+            source: {
+              table: 'slice',
+              innerJoin: true,
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+          slice_depth: {
+            column: 'depth',
+            source: {
+              table: 'slice',
+              joinOn: {
+                id: 'slice_id',
+              },
+            },
+          },
+        },
+      }),
+    ),
+  ).toBe(
+    normalise(`
+    SELECT
+      foo_0.id AS foo_id,
+      slice_1.name AS slice_name,
+      slice_2.depth AS slice_depth
+    FROM foo AS foo_0
+    JOIN slice AS slice_1 ON slice_1.id = foo_0.slice_id
+    LEFT JOIN slice AS slice_2 ON slice_2.id = foo_0.slice_id
+  `),
+  );
+});
+
 test('query_builder.join_with_multiple_columns', () => {
   // This test checks that the query builder can correctly deduplicate joins when we request multiple columns from the joined table.
   const parent: SourceTable = {
diff --git a/ui/src/frontend/widgets/sql/table/table.ts b/ui/src/frontend/widgets/sql/table/table.ts
index 3bf4b24..34f0a22 100644
--- a/ui/src/frontend/widgets/sql/table/table.ts
+++ b/ui/src/frontend/widgets/sql/table/table.ts
@@ -65,6 +65,38 @@
   return sqlColumnId(column.primaryColumn());
 }
 
+interface AddColumnMenuItemAttrs {
+  table: SqlTable;
+  state: SqlTableState;
+  index: number;
+}
+
+// This is separated into a separate class to store the index of the column to be
+// added and increment it when multiple columns are added from the same popup menu.
+class AddColumnMenuItem implements m.ClassComponent<AddColumnMenuItemAttrs> {
+  // Index where the new column should be inserted.
+  // In the regular case, a click would close the popup (destroying this class) and
+  // the `index` would not change during its lifetime.
+  // However, for mod-click, we want to keep adding columns to the right of the recently
+  // added column, so to achieve that we keep track of the index and increment it for
+  // each new column added.
+  index: number;
+
+  constructor({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
+    this.index = attrs.index;
+  }
+
+  view({attrs}: m.Vnode<AddColumnMenuItemAttrs>) {
+    return m(
+      MenuItem,
+      {label: 'Add column', icon: Icons.AddColumn},
+      attrs.table.renderAddColumnOptions((column) => {
+        attrs.state.addColumn(column, this.index++);
+      }),
+    );
+  }
+}
+
 export class SqlTable implements m.ClassComponent<SqlTableConfig> {
   private readonly table: SqlTableDescription;
 
@@ -201,6 +233,7 @@
               query: this.state.getSqlQuery(
                 Object.fromEntries([[columnAlias, column.primaryColumn()]]),
               ),
+              aggregationType: column.aggregation?.().dataType,
             },
             this.state.engine,
           );
@@ -209,13 +242,7 @@
       // Menu items before divider apply to selected column
       m(MenuDivider),
       // Menu items after divider apply to entire table
-      m(
-        MenuItem,
-        {label: 'Add column', icon: Icons.AddColumn},
-        this.renderAddColumnOptions((column) => {
-          this.state.addColumn(column, index);
-        }),
-      ),
+      m(AddColumnMenuItem, {table: this, state: this.state, index}),
     );
   }
 
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 954e2ca..e41e85e 100644
--- a/ui/src/frontend/widgets/sql/table/well_known_columns.ts
+++ b/ui/src/frontend/widgets/sql/table/well_known_columns.ts
@@ -14,25 +14,10 @@
 
 import m from 'mithril';
 
-import {
-  TableColumn,
-  TableColumnSet,
-  TableManager,
-  SqlColumn,
-  sqlColumnId,
-  TableColumnParams,
-  SourceTable,
-} from './column';
-import {
-  getStandardContextMenuItems,
-  getStandardFilters,
-  renderStandardCell,
-} from './render_cell_utils';
-import {Timestamp} from '../../timestamp';
+import {Icons} from '../../../../base/semantic_icons';
+import {sqliteString} from '../../../../base/string_utils';
 import {Duration, Time} from '../../../../base/time';
-import {DurationWidget} from '../../duration';
-import {renderError} from '../../../../widgets/error';
-import {SliceRef} from '../../slice';
+import {SqlValue, STR} from '../../../../trace_processor/query_result';
 import {
   asSchedSqlId,
   asSliceSqlId,
@@ -40,26 +25,53 @@
   asUpid,
   asUtid,
 } from '../../../../trace_processor/sql_utils/core_types';
-import {sqliteString} from '../../../../base/string_utils';
-import {ThreadStateRef} from '../../thread_state';
-import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
+import {getProcessName} from '../../../../trace_processor/sql_utils/process';
 import {getThreadName} from '../../../../trace_processor/sql_utils/thread';
 import {Anchor} from '../../../../widgets/anchor';
-import {threadRefMenuItems} from '../../thread';
-import {Icons} from '../../../../base/semantic_icons';
-import {SqlValue, STR} from '../../../../trace_processor/query_result';
-import {getProcessName} from '../../../../trace_processor/sql_utils/process';
+import {renderError} from '../../../../widgets/error';
+import {MenuDivider, MenuItem, PopupMenu2} from '../../../../widgets/menu';
+import {DurationWidget} from '../../duration';
 import {processRefMenuItems} from '../../process';
 import {SchedRef} from '../../sched';
+import {SliceRef} from '../../slice';
+import {threadRefMenuItems} from '../../thread';
+import {ThreadStateRef} from '../../thread_state';
+import {Timestamp} from '../../timestamp';
 
-type ColumnParams = TableColumnParams & {
+import {
+  AggregationConfig,
+  SourceTable,
+  SqlColumn,
+  sqlColumnId,
+  TableColumn,
+  TableColumnParams,
+  TableColumnSet,
+  TableManager,
+} from './column';
+import {
+  getStandardContextMenuItems,
+  getStandardFilters,
+  renderStandardCell,
+} from './render_cell_utils';
+
+export type ColumnParams = TableColumnParams & {
   title?: string;
 };
 
+export type StandardColumnParams = ColumnParams & {
+  aggregationType?: 'nominal' | 'quantitative';
+};
+
+export interface IdColumnParams {
+  // Whether the column is guaranteed not to have null values.
+  // (this will allow us to upgrage the joins on this column to more performant INNER JOINs).
+  notNull?: boolean;
+}
+
 export class StandardColumn extends TableColumn {
   constructor(
     private column: SqlColumn,
-    private params?: ColumnParams,
+    private params?: StandardColumnParams,
   ) {
     super(params);
   }
@@ -68,6 +80,10 @@
     return this.column;
   }
 
+  aggregate(): AggregationConfig {
+    return {dataType: this.params?.aggregationType};
+  }
+
   getTitle() {
     return this.params?.title;
   }
@@ -151,13 +167,15 @@
 
   constructor(
     private id: SqlColumn,
-    private params?: ColumnParams,
+    private params?: ColumnParams & IdColumnParams,
   ) {
     super(params);
 
     const sliceTable: SourceTable = {
       table: 'slice',
       joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
     };
 
     this.columns = {
@@ -231,13 +249,15 @@
 
   constructor(
     private id: SqlColumn,
-    private params?: ColumnParams,
+    private params?: ColumnParams & IdColumnParams,
   ) {
     super(params);
 
     const schedTable: SourceTable = {
       table: 'sched',
       joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
     };
 
     this.columns = {
@@ -315,13 +335,15 @@
 
   constructor(
     private id: SqlColumn,
-    private params?: ColumnParams,
+    private params?: ColumnParams & IdColumnParams,
   ) {
     super(params);
 
     const threadStateTable: SourceTable = {
       table: 'thread_state',
       joinOn: {id: this.id},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
     };
 
     this.columns = {
@@ -399,13 +421,15 @@
 
   constructor(
     private utid: SqlColumn,
-    private params?: ColumnParams,
+    private params?: ColumnParams & IdColumnParams,
   ) {
     super(params);
 
     const threadTable: SourceTable = {
       table: 'thread',
       joinOn: {id: this.utid},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
     };
 
     this.columns = {
@@ -504,6 +528,10 @@
       ),
     );
   }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
 }
 
 export class ProcessColumn extends TableColumn {
@@ -511,13 +539,15 @@
 
   constructor(
     private upid: SqlColumn,
-    private params?: ColumnParams,
+    private params?: ColumnParams & IdColumnParams,
   ) {
     super(params);
 
     const processTable: SourceTable = {
       table: 'process',
       joinOn: {id: this.upid},
+      // If the column is guaranteed not to have null values, we can use an INNER JOIN.
+      innerJoin: this.params?.notNull === true,
     };
 
     this.columns = {
@@ -613,6 +643,10 @@
       ),
     );
   }
+
+  aggregation(): AggregationConfig {
+    return {dataType: 'nominal'};
+  }
 }
 
 export class ArgSetColumnSet extends TableColumnSet {
diff --git a/ui/src/frontend/widgets/thread.ts b/ui/src/frontend/widgets/thread.ts
index 1ffab44..5cdad79 100644
--- a/ui/src/frontend/widgets/thread.ts
+++ b/ui/src/frontend/widgets/thread.ts
@@ -34,6 +34,25 @@
 import {asUtid} from '../../trace_processor/sql_utils/core_types';
 import {Utid} from '../../trace_processor/sql_utils/core_types';
 
+export function showThreadDetailsMenuItem(
+  utid: Utid,
+  tid?: number,
+): m.Children {
+  return m(MenuItem, {
+    icon: Icons.ExternalLink,
+    label: 'Show thread details',
+    onclick: () =>
+      addEphemeralTab(
+        'threadDetails',
+        new ThreadDetailsTab({
+          engine: getEngine('ThreadDetails'),
+          utid,
+          tid,
+        }),
+      ),
+  });
+}
+
 export function threadRefMenuItems(info: {
   utid: Utid;
   name?: string;
@@ -59,19 +78,7 @@
       label: 'Copy utid',
       onclick: () => copyToClipboard(`${info.utid}`),
     }),
-    m(MenuItem, {
-      icon: Icons.ExternalLink,
-      label: 'Show thread details',
-      onclick: () =>
-        addEphemeralTab(
-          'threadDetails',
-          new ThreadDetailsTab({
-            engine: getEngine('ThreadDetails'),
-            utid: info.utid,
-            tid: info.tid,
-          }),
-        ),
-    }),
+    showThreadDetailsMenuItem(info.utid, info.tid),
   ];
 }
 
diff --git a/ui/src/frontend/widgets/thread_state.ts b/ui/src/frontend/widgets/thread_state.ts
index a572666..fdb86b5 100644
--- a/ui/src/frontend/widgets/thread_state.ts
+++ b/ui/src/frontend/widgets/thread_state.ts
@@ -46,25 +46,21 @@
       {
         icon: Icons.UpdateSelection,
         onclick: () => {
-          let trackKey: string | undefined;
-          for (const track of Object.values(globals.state.tracks)) {
-            const trackDesc = globals.trackManager.resolveTrackInfo(track.uri);
-            if (
-              trackDesc &&
-              trackDesc.tags?.kind === THREAD_STATE_TRACK_KIND &&
-              trackDesc.tags?.utid === vnode.attrs.utid
-            ) {
-              trackKey = track.key;
-            }
-          }
+          const trackDescriptor = globals.trackManager
+            .getAllTracks()
+            .find(
+              (td) =>
+                td.tags?.kind === THREAD_STATE_TRACK_KIND &&
+                td.tags?.utid === vnode.attrs.utid,
+            );
 
-          if (trackKey === undefined) return;
+          if (trackDescriptor === undefined) return;
 
           globals.setLegacySelection(
             {
               kind: 'THREAD_STATE',
               id: vnode.attrs.id,
-              trackKey,
+              trackUri: trackDescriptor.uri,
             },
             {
               clearSearch: true,
@@ -74,7 +70,7 @@
             },
           );
 
-          scrollToTrackAndTs(trackKey, vnode.attrs.ts, true);
+          scrollToTrackAndTs(trackDescriptor.uri, vnode.attrs.ts, true);
         },
       },
       vnode.attrs.name ?? `Thread State ${vnode.attrs.id}`,
diff --git a/ui/src/frontend/widgets_page.ts b/ui/src/frontend/widgets_page.ts
index 9b258b1..e61be11 100644
--- a/ui/src/frontend/widgets_page.ts
+++ b/ui/src/frontend/widgets_page.ts
@@ -37,7 +37,7 @@
 } from '../widgets/multiselect';
 import {Popup, PopupPosition} from '../widgets/popup';
 import {Portal} from '../widgets/portal';
-import {FilterableSelect, Select} from '../widgets/select';
+import {Select} from '../widgets/select';
 import {Spinner} from '../widgets/spinner';
 import {Switch} from '../widgets/switch';
 import {TextInput} from '../widgets/text_input';
@@ -724,14 +724,6 @@
         },
       }),
       m(WidgetShowcase, {
-        label: 'Filterable Select',
-        renderWidget: () =>
-          m(FilterableSelect, {
-            values: ['foo', 'bar', 'baz'],
-            onSelected: () => {},
-          }),
-      }),
-      m(WidgetShowcase, {
         label: 'Empty State',
         renderWidget: ({header, content}) =>
           m(
diff --git a/ui/src/plugins/com.google.android.GoogleCamera/index.ts b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
index 3a33a5f..5f7ca77 100644
--- a/ui/src/plugins/com.google.android.GoogleCamera/index.ts
+++ b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
@@ -16,7 +16,6 @@
   PerfettoPlugin,
   PluginContextTrace,
   PluginDescriptor,
-  TrackRef,
 } from '../../public';
 
 import * as cameraConstants from './googleCameraConstants';
@@ -58,15 +57,11 @@
     this.pinTracks(cameraConstants.STARTUP_RELATED_TRACKS);
   }
 
-  private pinTracks(trackNames: string[]) {
-    const tracks: TrackRef[] = this.ctx.timeline.tracks;
-    trackNames.forEach((trackName) => {
-      const desiredTracks = tracks.filter((track) => {
-        return track.title.match(trackName);
-      });
-      desiredTracks.forEach((desiredTrack) => {
-        this.ctx.timeline.pinTrack(desiredTrack.key!);
-      });
+  private pinTracks(trackNames: ReadonlyArray<string>) {
+    this.ctx.timeline.workspace.flatTracks.forEach((track) => {
+      if (trackNames.includes(track.displayName)) {
+        track.pin();
+      }
     });
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
index e661076..88043b0 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/trackUtils.ts
@@ -14,11 +14,15 @@
 
 import {globals} from '../../frontend/globals';
 import {SimpleSliceTrackConfig} from '../../frontend/simple_slice_track';
-import {addDebugSliceTrack, PluginContextTrace} from '../../public';
+import {
+  addDebugSliceTrack,
+  PluginContextTrace,
+  TrackDescriptor,
+} from '../../public';
 import {findCurrentSelection} from '../../frontend/keyboard_event_handler';
 import {time, Time} from '../../base/time';
 import {BigintMath} from '../../base/bigint_math';
-import {reveal} from '../../frontend/scroll_helper';
+import {scrollToTrackAndTimeSpan} from '../../frontend/scroll_helper';
 
 /**
  * Adds debug tracks from SimpleSliceTrackConfig
@@ -83,12 +87,12 @@
     return;
   }
   const trackId = slice.trackId;
-  const trackKey = getTrackKey(trackId);
+  const track = getTrackForTrackId(trackId);
   globals.setLegacySelection(
     {
       kind: 'SLICE',
       id: slice.sliceId,
-      trackKey: trackKey,
+      trackUri: track?.uri,
       table: 'slice',
     },
     {
@@ -101,13 +105,14 @@
 }
 
 /**
- * Given the trackId of the track, retrieves its trackKey
+ * Given the trackId of the track, retrieves its corresponding TrackDescriptor.
  *
- * @param {number} trackId track_id of the track
- * @returns {string} trackKey given to the track with queried trackId
+ * @param trackId track_id of the track
  */
-function getTrackKey(trackId: number): string | undefined {
-  return globals.trackManager.trackKeyByTrackId.get(trackId);
+function getTrackForTrackId(trackId: number): TrackDescriptor | undefined {
+  return globals.trackManager.findTrack((trackDescriptor) => {
+    return trackDescriptor?.tags?.trackIds?.includes(trackId);
+  });
 }
 
 /**
@@ -131,10 +136,15 @@
   const sliceStart = slice.ts;
   // row.dur can be negative. Clamp to 1ns.
   const sliceDur = BigintMath.max(slice.dur, 1n);
-  const trackKey = getTrackKey(trackId);
+  const track = getTrackForTrackId(trackId);
   // true for whether to expand the process group the track belongs to
-  if (trackKey == undefined) {
+  if (track == undefined) {
     return;
   }
-  reveal(trackKey, sliceStart, Time.add(sliceStart, sliceDur), true);
+  scrollToTrackAndTimeSpan(
+    track.uri,
+    sliceStart,
+    Time.add(sliceStart, sliceDur),
+    true,
+  );
 }
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 90342b7..99c8b98 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -27,6 +27,7 @@
   SimpleCounterTrack,
   SimpleCounterTrackConfig,
 } from '../../frontend/simple_counter_track';
+import {globals} from '../../frontend/globals';
 
 interface ContainedTrace {
   uuid: string;
@@ -1173,13 +1174,11 @@
     } else {
       uri = `/long_battery_tracing_${name}`;
     }
-    ctx.registerStaticTrack({
+    ctx.registerTrackAndShowOnTraceLoad({
       uri,
       title: name,
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
-      },
-      groupName,
+      track: new SimpleSliceTrack(ctx.engine, {trackUri: uri}, config),
+      tags: {groupName},
     });
   }
 
@@ -1206,13 +1205,11 @@
       uri = `/long_battery_tracing_${name}`;
     }
 
-    ctx.registerStaticTrack({
+    ctx.registerTrackAndShowOnTraceLoad({
       uri,
       title: name,
-      trackFactory: (trackCtx) => {
-        return new SimpleCounterTrack(ctx.engine, trackCtx, config);
-      },
-      groupName,
+      track: new SimpleCounterTrack(ctx.engine, {trackUri: uri}, config),
+      tags: {groupName},
     });
   }
 
@@ -1466,6 +1463,41 @@
       );
 
     const e = ctx.engine;
+
+    if (
+      globals.extraSqlPackages.find((x) => x.name === 'google3') !== undefined
+    ) {
+      await e.query(
+        `INCLUDE PERFETTO MODULE
+            google3.wireless.android.telemetry.trace_extractor.modules.modem_tea_metrics`,
+      );
+      const counters = await e.query(
+        `select distinct name from pixel_modem_counters`,
+      );
+      const countersIt = counters.iter({name: 'str'});
+      for (; countersIt.valid(); countersIt.next()) {
+        this.addCounterTrack(
+          ctx,
+          countersIt.name,
+          `select ts, value from pixel_modem_counters where name = '${countersIt.name}'`,
+          groupName,
+        );
+      }
+      const slices = await e.query(
+        `select distinct track_name from pixel_modem_slices`,
+      );
+      const slicesIt = slices.iter({track_name: 'str'});
+      for (; slicesIt.valid(); slicesIt.next()) {
+        this.addSliceTrack(
+          ctx,
+          it.name,
+          `select ts dur, slice_name as name from pixel_modem_counters
+              where track_name = '${slicesIt.track_name}'`,
+          groupName,
+        );
+      }
+    }
+
     await e.query(MODEM_RIL_STRENGTH);
     await e.query(MODEM_RIL_CHANNELS_PREAMBLE);
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index fb7586f..69754fd 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -46,7 +46,7 @@
                       process_name,
                       intent,
                       'slice' AS table_name
-                    FROM _android_app_process_starts
+                    FROM android_app_process_starts
                     WHERE reason = '${reason}'
                  `,
         columns: sliceColumns,
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index 59ae3d5..45541e1 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -44,12 +44,11 @@
       columns: {ts: 'ts', dur: 'dur', name: 'name'},
       argColumns: [],
     };
-    ctx.registerStaticTrack({
-      uri: `/android_startups`,
+    const uri = `/android_startups`;
+    ctx.registerTrackAndShowOnTraceLoad({
+      uri,
       title: 'Android App Startups',
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, config);
-      },
+      track: new SimpleSliceTrack(ctx.engine, {trackUri: uri}, config),
     });
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
index d57ad70..1075645 100644
--- a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
+++ b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
@@ -82,12 +82,11 @@
         processName = `${it.pid}`;
       }
 
-      ctx.registerStaticTrack({
-        uri: `dev.perfetto.GpuByProcess#${upid}`,
+      const uri = `dev.perfetto.GpuByProcess#${upid}`;
+      ctx.registerTrackAndShowOnTraceLoad({
+        uri,
         title: `GPU ${processName}`,
-        trackFactory: ({trackKey}) => {
-          return new GpuPidTrack({engine: ctx.engine, trackKey}, upid);
-        },
+        track: new GpuPidTrack({engine: ctx.engine, uri}, upid),
       });
     }
   }
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 6a302a9..dc06537 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -24,19 +24,21 @@
       id: 'dev.perfetto.LargeScreensPerf#PinUnfoldLatencyTracks',
       name: 'Pin: Unfold latency tracks',
       callback: () => {
-        ctx.timeline.pinTracksByPredicate((track) => {
-          return (
-            !!track.title.includes('UnfoldTransition') ||
-            track.title.includes('Screen on blocked') ||
-            track.title.includes('hingeAngle') ||
-            track.title.includes('UnfoldLightRevealOverlayAnimation') ||
-            track.title.startsWith('waitForAllWindowsDrawn') ||
-            track.title.endsWith('UNFOLD_ANIM>') ||
-            track.title.endsWith('UNFOLD>') ||
-            track.title == 'Waiting for KeyguardDrawnCallback#onDrawn' ||
-            track.title == 'FoldedState' ||
-            track.title == 'FoldUpdate'
-          );
+        ctx.timeline.workspace.flatTracks.forEach((track) => {
+          if (
+            !!track.displayName.includes('UnfoldTransition') ||
+            track.displayName.includes('Screen on blocked') ||
+            track.displayName.includes('hingeAngle') ||
+            track.displayName.includes('UnfoldLightRevealOverlayAnimation') ||
+            track.displayName.startsWith('waitForAllWindowsDrawn') ||
+            track.displayName.endsWith('UNFOLD_ANIM>') ||
+            track.displayName.endsWith('UNFOLD>') ||
+            track.displayName == 'Waiting for KeyguardDrawnCallback#onDrawn' ||
+            track.displayName == 'FoldedState' ||
+            track.displayName == 'FoldUpdate'
+          ) {
+            track.pin();
+          }
         });
       },
     });
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
index b655639..de52817 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/OWNERS
@@ -1,4 +1,3 @@
-paulsoumyadeep@google.com
 nishantpanwar@google.com
 bvineeth@google.com
 nicomazz@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS b/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
index b655639..de52817 100644
--- a/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
+++ b/ui/src/plugins/dev.perfetto.PinSysUITracks/OWNERS
@@ -1,4 +1,3 @@
-paulsoumyadeep@google.com
 nishantpanwar@google.com
 bvineeth@google.com
 nicomazz@google.com
\ No newline at end of file
diff --git a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
index 025c91c..b67c836 100644
--- a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
@@ -54,21 +54,24 @@
       id: 'dev.perfetto.PinSysUITracks#PinSysUITracks',
       name: 'Pin: System UI Related Tracks',
       callback: () => {
-        ctx.timeline.pinTracksByPredicate((track) => {
-          if (!track.uri.startsWith(`/process_${sysuiUpid}`)) return false;
+        ctx.timeline.workspace.flatTracks.forEach((track) => {
+          // Ensure we only grab tracks that are in the SysUI process group
+          if (!track.uri.startsWith(`/process_${sysuiUpid}`)) return;
           if (
             !TRACKS_TO_PIN.some((trackName) =>
-              track.title.startsWith(trackName),
+              track.displayName.startsWith(trackName),
             )
           ) {
-            return false;
+            return;
           }
-          return true;
+          track.pin();
         });
 
         // expand the sysui process tracks group
-        ctx.timeline.expandGroupsByPredicate((groupRef) => {
-          return groupRef.displayName?.startsWith(SYSTEM_UI_PROCESS) ?? false;
+        ctx.timeline.workspace.flatGroups.forEach((group) => {
+          if (group.displayName.startsWith(SYSTEM_UI_PROCESS)) {
+            group.expand();
+          }
         });
       },
     });
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
index a8a612f..bdafb8a 100644
--- a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -12,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import {Optional} from '../../base/utils';
+import {GroupNode, TrackNode} from '../../public/workspace';
 import {
   PerfettoPlugin,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
-  TrackRef,
 } from '../../public';
 
 const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
@@ -54,12 +55,11 @@
   }
 
   private saveTracks() {
-    const pinnedTracks = this.ctx.timeline.tracks.filter(
-      (trackRef) => trackRef.isPinned,
-    );
-    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((trackRef) => ({
-      groupName: trackRef.groupName,
-      trackName: trackRef.title,
+    const workspace = this.ctx.timeline.workspace;
+    const pinnedTracks = workspace.pinnedTracks;
+    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((track) => ({
+      groupName: groupName(track),
+      trackName: track.displayName,
     }));
     window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(tracksToSave));
   }
@@ -71,19 +71,19 @@
       return;
     }
     const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
-    const tracks: TrackRef[] = this.ctx.timeline.tracks;
+    const workspace = this.ctx.timeline.workspace;
+    const tracks = workspace.flatTracks;
     tracksToRestore.forEach((trackToRestore) => {
       // Check for an exact match
       const exactMatch = tracks.find((track) => {
         return (
-          track.key &&
-          trackToRestore.trackName === track.title &&
-          trackToRestore.groupName === track.groupName
+          trackToRestore.trackName === track.displayName &&
+          trackToRestore.groupName === groupName(track)
         );
       });
 
       if (exactMatch) {
-        this.ctx.timeline.pinTrack(exactMatch.key!);
+        exactMatch.pin();
       } else {
         // We attempt a match after removing numbers to potentially pin a
         // "similar" track from a different trace. Removing numbers allows
@@ -96,16 +96,15 @@
         // other process matching the package name is attempted.
         const fuzzyMatch = tracks.find((track) => {
           return (
-            track.key &&
             this.removeNumbers(trackToRestore.trackName) ===
-              this.removeNumbers(track.title) &&
+              this.removeNumbers(track.displayName) &&
             this.removeNumbers(trackToRestore.groupName) ===
-              this.removeNumbers(track.groupName)
+              this.removeNumbers(groupName(track))
           );
         });
 
         if (fuzzyMatch) {
-          this.ctx.timeline.pinTrack(fuzzyMatch.key!);
+          fuzzyMatch.pin();
         } else {
           console.warn(
             '[RestorePinnedTracks] No track found that matches',
@@ -121,6 +120,16 @@
   }
 }
 
+// Return the displayname of the containing group
+// If the track is a child of a workspace, return undefined...
+function groupName(track: TrackNode): Optional<string> {
+  const parent = track.parent;
+  if (parent instanceof GroupNode) {
+    return parent.displayName;
+  }
+  return undefined;
+}
+
 interface SavedPinnedTrack {
   // Optional: group name for the track. Usually matches with process name.
   groupName?: string;
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 65ce8dd..a5a7fcc 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -29,11 +29,14 @@
     if (row.cnt === 0) {
       return;
     }
-    ctx.registerStaticTrack({
-      uri: `/clock_snapshots`,
+    const uri = `/clock_snapshots`;
+    ctx.registerTrackAndShowOnTraceLoad({
+      uri,
       title: 'Clock Snapshots',
-      trackFactory: (trackCtx) => {
-        return new SimpleSliceTrack(ctx.engine, trackCtx, {
+      track: new SimpleSliceTrack(
+        ctx.engine,
+        {trackUri: uri},
+        {
           data: {
             sqlSource: `
               select ts, 0 as dur, 'Snapshot' as name
@@ -43,8 +46,8 @@
           },
           columns: {ts: 'ts', dur: 'dur', name: 'name'},
           argColumns: [],
-        });
-      },
+        },
+      ),
     });
   }
 }
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index 4f1ac0e..613aa08 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -44,24 +44,23 @@
       const trackId = it.trackId;
       const displayName = it.name ?? `${trackId}`;
 
-      ctx.registerStaticTrack({
-        uri: `/kernel_devices/${displayName}`,
+      const uri = `/kernel_devices/${displayName}`;
+      ctx.registerTrackAndShowOnTraceLoad({
+        uri,
         title: displayName,
-        trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrack(
-            {
-              engine: ctx.engine,
-              trackKey,
-            },
-            0,
-            [trackId],
-          );
-        },
+        track: new AsyncSliceTrack(
+          {
+            engine: ctx.engine,
+            uri,
+          },
+          0,
+          [trackId],
+        ),
         tags: {
           kind: ASYNC_SLICE_TRACK_KIND,
           trackIds: [trackId],
+          groupName: `Linux Kernel Devices`,
         },
-        groupName: `Linux Kernel Devices`,
       });
     }
   }
diff --git a/ui/src/public/exposed_commands.ts b/ui/src/public/exposed_commands.ts
new file mode 100644
index 0000000..d75275e
--- /dev/null
+++ b/ui/src/public/exposed_commands.ts
@@ -0,0 +1,26 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This file contains constants for some command IDs that are used directly
+// from frontend code (e.g. the details panel that has buttons for critical
+// path). They exist to deal with all cases where some feature cannot be done
+// just with the existing API (e.g. the command palette), and a more direct
+// coupling between frontend and commands is necessary.
+// Adding entries to this file usually is the symptom of a missing API in the
+// plugin surface (e.g. the ability to customize context menus).
+// These constants exist just to make the dependency evident at code
+// search time, rather than copy-pasting the string in two places.
+
+export const CRITICAL_PATH_CMD = 'perfetto.CriticalPath';
+export const CRITICAL_PATH_LITE_CMD = 'perfetto.CriticalPathLite';
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index fbb42a0..63eeb30 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -18,11 +18,11 @@
 import {TimeSpan, duration, time} from '../base/time';
 import {Migrate, Store} from '../base/store';
 import {ColorScheme} from '../core/colorizer';
-import {PrimaryTrackSortKey} from '../common/state';
 import {Engine} from '../trace_processor/engine';
 import {PromptOption} from '../frontend/omnibox_manager';
 import {LegacyDetailsPanel, TrackDescriptor} from './tracks';
 import {TraceContext} from '../frontend/trace_context';
+import {Workspace} from './workspace';
 
 export {Engine} from '../trace_processor/engine';
 export {
@@ -36,7 +36,6 @@
 export {BottomTabToSCSAdapter} from './utils';
 export {createStore, Migrate, Store} from '../base/store';
 export {PromptOption} from '../frontend/omnibox_manager';
-export {PrimaryTrackSortKey} from '../common/state';
 
 export {addDebugSliceTrack} from '../frontend/debug_tracks/debug_tracks';
 export * from '../core/track_kinds';
@@ -164,41 +163,6 @@
   addSidebarMenuItem(menuItem: SidebarMenuItem): void;
 }
 
-export interface SliceTrackColNames {
-  ts: string;
-  name: string;
-  dur: string;
-}
-
-export interface DebugSliceTrackArgs {
-  // Title of the track. If omitted a placeholder name will be chosen instead.
-  trackName?: string;
-
-  // Mapping definitions of the 'ts', 'dur', and 'name' columns.
-  // By default, columns called ts, dur and name will be used.
-  // If dur is assigned the value '0', all slices shall be instant events.
-  columnMapping?: Partial<SliceTrackColNames>;
-
-  // Any extra columns to be used as args.
-  args?: string[];
-
-  // Optional renaming of columns.
-  columns?: string[];
-}
-
-export interface CounterTrackColNames {
-  ts: string;
-  value: string;
-}
-
-export interface DebugCounterTrackArgs {
-  // Title of the track. If omitted a placeholder name will be chosen instead.
-  trackName?: string;
-
-  // Mapping definitions of the ts and value columns.
-  columnMapping?: Partial<CounterTrackColNames>;
-}
-
 export interface Tab {
   render(): m.Children;
   getTitle(): string;
@@ -220,37 +184,6 @@
 
   // Control over the main timeline.
   timeline: {
-    // Add a new track to the scrolling track section, returning the newly
-    // created track key.
-    addTrack(uri: string, displayName: string, params?: unknown): string;
-
-    // Remove a single track from the timeline.
-    removeTrack(key: string): void;
-
-    // Pin a single track.
-    pinTrack(key: string): void;
-
-    // Unpin a single track.
-    unpinTrack(key: string): void;
-
-    // Pin all tracks that match a predicate.
-    pinTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Unpin all tracks that match a predicate.
-    unpinTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Remove all tracks that match a predicate.
-    removeTracksByPredicate(predicate: TrackPredicate): void;
-
-    // Expand all groups that match a predicate.
-    expandGroupsByPredicate(predicate: GroupPredicate): void;
-
-    // Collapse all groups that match a predicate.
-    collapseGroupsByPredicate(predicate: GroupPredicate): void;
-
-    // Retrieve a list of tracks on the timeline.
-    tracks: TrackRef[];
-
     // Bring a timestamp into view.
     panToTimestamp(ts: time): void;
 
@@ -259,6 +192,10 @@
 
     // A span representing the current viewport location
     readonly viewport: TimeSpan;
+
+    // Access the default workspace - used for adding, removing and reorganizing
+    // tracks
+    readonly workspace: Workspace;
   };
 
   // Control over the bottom details pane.
@@ -273,23 +210,16 @@
     hideTab(uri: string): void;
   };
 
-  // Register a new track against a unique key known as a URI.
-  // Once a track is registered it can be referenced multiple times on the
-  // timeline with different params to allow customising each instance.
+  // Register a new track against a unique key known as a URI. The track is not
+  // shown by default and callers need to either manually add it to a
+  // Workspace or use registerTrackAndShowOnTraceLoad() below.
   registerTrack(trackDesc: TrackDescriptor): void;
 
-  // Add a new entry to the pool of default tracks. Default tracks are a list
-  // of track references that describe the list of tracks that should be added
-  // to the main timeline on startup.
-  // Default tracks are only used when a trace is first loaded, not when
-  // loading from a permalink, where the existing list of tracks from the
+  // Register a track and mark it as "automatically show on trace load".
+  // These tracks are shown only when the trace is loaded from scratch, not
+  // when loading from a permalink, where the existing list of tracks from the
   // shared state is used instead.
-  addDefaultTrack(track: TrackRef): void;
-
-  // Simultaneously register a track and add it as a default track in one go.
-  // This is simply a helper which calls registerTrack() and addDefaultTrack()
-  // with the same URI.
-  registerStaticTrack(track: TrackDescriptor & TrackRef): void;
+  registerTrackAndShowOnTraceLoad(track: TrackDescriptor): void;
 
   // Register a new tab for this plugin. Will be unregistered when the plugin
   // is deactivated or when the trace is unloaded.
@@ -342,43 +272,6 @@
   new (): PerfettoPlugin;
 }
 
-// Describes a reference to a registered track.
-export interface TrackRef {
-  // URI of the registered track.
-  readonly uri: string;
-
-  // A human readable name for this track - displayed in the track shell.
-  readonly title: string;
-
-  // Optional: Used to define default sort order for new traces.
-  // Note: This will be deprecated soon in favour of tags & sort rules.
-  readonly sortKey?: PrimaryTrackSortKey;
-
-  // Optional: Add tracks to a group with this name.
-  readonly groupName?: string;
-
-  // Optional: Track key
-  readonly key?: string;
-
-  // Optional: Whether the track is pinned
-  readonly isPinned?: boolean;
-}
-
-// A predicate for selecting a subset of tracks.
-export type TrackPredicate = (info: TrackDescriptor) => boolean;
-
-// Describes a reference to a group of tracks.
-export interface GroupRef {
-  // A human readable name for this track group.
-  displayName: string;
-
-  // True if the track is open else false.
-  collapsed: boolean;
-}
-
-// A predicate for selecting a subset of groups.
-export type GroupPredicate = (info: GroupRef) => boolean;
-
 // Plugins can be class refs or concrete plugin implementations.
 export type PluginFactory = PluginClass | PerfettoPlugin;
 
diff --git a/ui/src/public/tracks.ts b/ui/src/public/tracks.ts
index b45ab34..fb0c53f 100644
--- a/ui/src/public/tracks.ts
+++ b/ui/src/public/tracks.ts
@@ -22,8 +22,8 @@
 import {HighPrecisionTimeSpan} from '../common/high_precision_time_span';
 
 export interface TrackContext {
-  // This track's key, used for making selections et al.
-  readonly trackKey: string;
+  // This track's URI, used for making selections et al.
+  readonly trackUri: string;
 }
 
 /**
@@ -69,7 +69,7 @@
   readonly uri: string;
 
   // A factory function returning a new track instance.
-  readonly trackFactory: (ctx: TrackContext) => Track;
+  readonly track: Track;
 
   // Human readable title. Always displayed.
   readonly title: string;
diff --git a/ui/src/public/workspace.ts b/ui/src/public/workspace.ts
new file mode 100644
index 0000000..d286b57
--- /dev/null
+++ b/ui/src/public/workspace.ts
@@ -0,0 +1,287 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Optional} from '../base/utils';
+import {uuidv4} from '../base/uuid';
+import {raf} from '../core/raf_scheduler';
+
+export class TrackNode {
+  // This is the URI of the track this node references.
+  public readonly uri: string;
+  public displayName: string;
+  public parent?: ContainerNode;
+
+  constructor(uri: string, displayName: string) {
+    this.uri = uri;
+    this.displayName = displayName;
+  }
+
+  // Expand all ancestors
+  reveal(): void {
+    let parent = this.parent;
+    while (parent && parent instanceof GroupNode) {
+      parent.expand();
+      parent = parent.parent;
+    }
+  }
+
+  get workspace(): Optional<Workspace> {
+    let parent = this.parent;
+    while (parent && !(parent instanceof Workspace)) {
+      parent = parent.parent;
+    }
+    return parent;
+  }
+
+  remove(): void {
+    this.workspace?.unpinTrack(this);
+    this.parent?.removeChild(this);
+  }
+
+  pin(): void {
+    this.workspace?.pinTrack(this);
+  }
+
+  unpin(): void {
+    this.workspace?.unpinTrack(this);
+  }
+
+  get isPinned(): boolean {
+    return Boolean(this.workspace?.pinnedTracks.includes(this));
+  }
+
+  get closestVisibleAncestor(): Optional<GroupNode> {
+    // Build a path back up to the root.
+    const path: ContainerNode[] = [];
+    let group = this.parent;
+    while (group) {
+      path.unshift(group);
+      group = group.parent;
+    }
+
+    // Find the first collapsed group in the path starting from the root.
+    // This will be the last ancestor which isn't collapsed behind a group.
+    for (const p of path) {
+      if (p instanceof GroupNode && p.collapsed) {
+        return p;
+      }
+    }
+
+    return undefined;
+  }
+}
+
+/**
+ * A base class for any node with children (i.e. a group or a workspace).
+ */
+export class ContainerNode {
+  public displayName: string;
+  public parent?: ContainerNode;
+  private _children: Array<Node>;
+
+  clear(): void {
+    this._children = [];
+  }
+
+  get children(): ReadonlyArray<Node> {
+    return this._children;
+  }
+
+  constructor(displayName: string) {
+    this.displayName = displayName;
+    this._children = [];
+  }
+
+  private adopt(child: Node): void {
+    if (child.parent) {
+      child.parent.removeChild(child);
+    }
+    child.parent = this;
+  }
+
+  addChild(child: Node): void {
+    this.adopt(child);
+    this._children.push(child);
+    raf.scheduleFullRedraw();
+  }
+
+  prependChild(child: Node): void {
+    this.adopt(child);
+    this._children.unshift(child);
+    raf.scheduleFullRedraw();
+  }
+
+  removeChild(child: Node): void {
+    this._children = this.children.filter((x) => child !== x);
+    child.parent = undefined;
+    raf.scheduleFullRedraw();
+  }
+
+  insertBefore(newNode: Node, referenceNode: Node): void {
+    const indexOfReference = this.children.indexOf(referenceNode);
+    if (indexOfReference === -1) {
+      throw new Error('Reference node is not a child of this node');
+    }
+
+    if (newNode.parent) {
+      newNode.parent.removeChild(newNode);
+    }
+    newNode.parent = this;
+
+    this._children.splice(indexOfReference, 0, newNode);
+    raf.scheduleFullRedraw();
+  }
+
+  insertAfter(newNode: Node, referenceNode: Node): void {
+    const indexOfReference = this.children.indexOf(referenceNode);
+    if (indexOfReference === -1) {
+      throw new Error('Reference node is not a child of this node');
+    }
+
+    if (newNode.parent) {
+      newNode.parent.removeChild(newNode);
+    }
+    newNode.parent = this;
+
+    this._children.splice(indexOfReference + 1, 0, newNode);
+    raf.scheduleFullRedraw();
+  }
+
+  /**
+   * Returns an array containing the flattened list of all nodes (tracks and
+   * groups) within this node.
+   */
+  get flatNodes(): ReadonlyArray<Node> {
+    const nodes = this.children.flatMap((node) => {
+      if (node instanceof TrackNode) {
+        return node;
+      } else {
+        return [node, ...node.flatNodes];
+      }
+    });
+    return nodes;
+  }
+
+  /**
+   * Returns an array containing the flattened list of tracks within this node.
+   */
+  get flatTracks(): ReadonlyArray<TrackNode> {
+    return this.flatNodes.filter((t) => t instanceof TrackNode);
+  }
+
+  /**
+   * Returns an array containing the flattened list of groups within this
+   * workspace.
+   */
+  get flatGroups(): ReadonlyArray<GroupNode> {
+    return this.flatNodes.filter((t) => t instanceof GroupNode);
+  }
+}
+
+export class GroupNode extends ContainerNode {
+  // A unique URI used to identify this group
+  public readonly uri: string;
+
+  // Optional URI of a track to show on this group's header.
+  public headerTrackUri?: string;
+
+  // If true, this track will not show a header & permanently expanded.
+  public headless: boolean;
+
+  private _collapsed: boolean;
+
+  constructor(displayName: string) {
+    super(displayName);
+    this._collapsed = true;
+    this.headless = false;
+    this.uri = uuidv4();
+  }
+
+  expand(): void {
+    this._collapsed = false;
+    raf.scheduleFullRedraw();
+  }
+
+  collapse(): void {
+    this._collapsed = true;
+    raf.scheduleFullRedraw();
+  }
+
+  toggleCollapsed(): void {
+    this._collapsed = !this._collapsed;
+    raf.scheduleFullRedraw();
+  }
+
+  get collapsed(): boolean {
+    return this._collapsed;
+  }
+
+  get expanded(): boolean {
+    return !this._collapsed;
+  }
+}
+
+export type Node = TrackNode | GroupNode;
+
+/**
+ * Defines a workspace containing a track tree and a pinned area.
+ */
+export class Workspace extends ContainerNode {
+  public pinnedTracks: Array<TrackNode>;
+  public readonly uuid: string;
+
+  constructor(displayName: string) {
+    super(displayName);
+    this.pinnedTracks = [];
+    this.uuid = uuidv4();
+  }
+
+  /**
+   * Reset the entire workspace including the pinned tracks.
+   */
+  clear(): void {
+    this.pinnedTracks = [];
+    super.clear();
+    raf.scheduleFullRedraw();
+  }
+
+  /**
+   * Adds a track node to this workspace's pinned area.
+   */
+  pinTrack(track: TrackNode): void {
+    // TODO(stevegolton): Check if the track exists in this workspace first
+    // otherwise we might get surprises.
+    this.pinnedTracks.push(track);
+    raf.scheduleFullRedraw();
+  }
+
+  /**
+   * Removes a track node from this workspace's pinned area.
+   */
+  unpinTrack(track: TrackNode): void {
+    this.pinnedTracks = this.pinnedTracks.filter((t) => t !== track);
+    raf.scheduleFullRedraw();
+  }
+
+  /**
+   * Get a track node by its URI.
+   *
+   * @param uri - The URI of the track to look up.
+   * @returns The track node if it exists in this workspace, otherwise
+   * undefined.
+   */
+  getTrackByUri(uri: string): Optional<TrackNode> {
+    return this.flatTracks.find((t) => t.uri === uri);
+  }
+}
diff --git a/ui/src/public/workspace_unittest.ts b/ui/src/public/workspace_unittest.ts
new file mode 100644
index 0000000..0e1ddc7
--- /dev/null
+++ b/ui/src/public/workspace_unittest.ts
@@ -0,0 +1,25 @@
+// 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 {GroupNode, TrackNode, Workspace} from './workspace';
+
+test('Workspace', () => {
+  const fooTrack = new TrackNode('foo', 'Foo');
+
+  const fooGroup = new GroupNode('foo');
+  fooGroup.addChild(fooTrack);
+
+  const workspace = new Workspace('Default Workspace');
+  workspace.addChild(fooGroup);
+});
diff --git a/ui/src/trace_processor/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index af573d0..27c1c8b 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -49,7 +49,7 @@
       this.websocket.onclose = (e) => this.onWebsocketClosed(e);
       this.websocket.onerror = (e) =>
         this.errorHandler(
-          `WebSocket error (state=${(e.target as WebSocket)?.readyState})`,
+          `WebSocket error rs=${(e.target as WebSocket)?.readyState} (ERR:ws)`,
         );
     }
 
@@ -78,7 +78,7 @@
       this.connected = false;
       this.rpcSendRequestBytes(new Uint8Array()); // Triggers a reconnection.
     } else {
-      this.errorHandler(`Websocket closed (${e.code}: ${e.reason})`);
+      this.errorHandler(`Websocket closed (${e.code}: ${e.reason}) (ERR:ws)`);
     }
   }
 
diff --git a/ui/src/trace_processor/sql_utils.ts b/ui/src/trace_processor/sql_utils.ts
index 97b5f84..9b2c342 100644
--- a/ui/src/trace_processor/sql_utils.ts
+++ b/ui/src/trace_processor/sql_utils.ts
@@ -196,7 +196,7 @@
  *
  * @param engine - The database engine to execute the query.
  * @param viewName - The name of the view to be created.
- * @param expression - The SQL expression to define the table.
+ * @param as - The SQL expression to define the table.
  * @returns An AsyncDisposable which drops the created table when disposed.
  *
  * @example
@@ -214,9 +214,9 @@
 export async function createView(
   engine: Engine,
   viewName: string,
-  expression: string,
+  as: string,
 ): Promise<AsyncDisposable> {
-  await engine.query(`CREATE VIEW ${viewName} AS ${expression}`);
+  await engine.query(`CREATE VIEW ${viewName} AS ${as}`);
   return {
     [Symbol.asyncDispose]: async () => {
       await engine.tryQuery(`DROP VIEW IF EXISTS ${viewName}`);
diff --git a/ui/src/trace_processor/sql_utils/thread_state.ts b/ui/src/trace_processor/sql_utils/thread_state.ts
index 4836ddf..8f02f52 100644
--- a/ui/src/trace_processor/sql_utils/thread_state.ts
+++ b/ui/src/trace_processor/sql_utils/thread_state.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {duration, Time, time} from '../../base/time';
-import {exists} from '../../base/utils';
 import {translateState} from '../../common/thread_state';
 import {Engine} from '../engine';
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../query_result';
@@ -129,26 +128,17 @@
 }
 
 export function goToSchedSlice(cpu: number, id: SchedSqlId, ts: time) {
-  let trackId: string | undefined;
-  for (const track of Object.values(globals.state.tracks)) {
-    if (exists(track?.uri)) {
-      const trackInfo = globals.trackManager.resolveTrackInfo(track.uri);
-      if (trackInfo?.tags?.kind === CPU_SLICE_TRACK_KIND) {
-        if (trackInfo?.tags?.cpu === cpu) {
-          trackId = track.key;
-          break;
-        }
-      }
-    }
-  }
-  if (trackId === undefined) {
+  const track = globals.trackManager.findTrack(
+    (td) => td.tags?.kind === CPU_SLICE_TRACK_KIND && td.tags.cpu === cpu,
+  );
+  if (track === undefined) {
     return;
   }
   globals.setLegacySelection(
     {
       kind: 'SCHED_SLICE',
       id,
-      trackKey: trackId,
+      trackUri: track.uri,
     },
     {
       clearSearch: true,
@@ -157,5 +147,5 @@
     },
   );
 
-  scrollToTrackAndTs(trackId, ts);
+  scrollToTrackAndTs(track.uri, ts);
 }
diff --git a/ui/src/widgets/menu.ts b/ui/src/widgets/menu.ts
index 9c53c49..c34e891 100644
--- a/ui/src/widgets/menu.ts
+++ b/ui/src/widgets/menu.ts
@@ -120,9 +120,9 @@
 // A siple container for a menu.
 // The menu contents are passed in as children, and are typically MenuItems or
 // MenuDividers, but really they can be any Mithril component.
-export class Menu implements m.ClassComponent {
-  view({children}: m.CVnode) {
-    return m('.pf-menu', children);
+export class Menu implements m.ClassComponent<HTMLAttrs> {
+  view({attrs, children}: m.CVnode<HTMLAttrs>) {
+    return m('.pf-menu', attrs, children);
   }
 }
 
diff --git a/ui/src/widgets/select.ts b/ui/src/widgets/select.ts
index 7d59a79..9e7250a 100644
--- a/ui/src/widgets/select.ts
+++ b/ui/src/widgets/select.ts
@@ -14,86 +14,10 @@
 
 import m from 'mithril';
 
-import {exists} from '../base/utils';
-
 import {HTMLInputAttrs} from './common';
-import {Menu, MenuItem} from './menu';
-import {scheduleFullRedraw} from './raf';
-import {TextInput} from './text_input';
 
 export class Select implements m.ClassComponent<HTMLInputAttrs> {
   view({attrs, children}: m.CVnode<HTMLInputAttrs>) {
     return m('select.pf-select', attrs, children);
   }
 }
-
-export interface FilterableSelectAttrs {
-  // Whether to show a search box. Defaults to false.
-  filterable?: boolean;
-  // The values to show in the select.
-  values: string[];
-  // Called when the user selects an option.
-  onSelected: (value: string) => void;
-  // If set, only the first maxDisplayedItems will be shown.
-  maxDisplayedItems?: number;
-  // Whether the input field should be focused when the widget is created.
-  autofocusInput?: boolean;
-}
-
-// A select widget with a search box, allowing the user to filter the options.
-export class FilterableSelect
-  implements m.ClassComponent<FilterableSelectAttrs>
-{
-  searchText = '';
-
-  view({attrs}: m.CVnode<FilterableSelectAttrs>) {
-    const filteredValues = attrs.values.filter((name) => {
-      return name.toLowerCase().includes(this.searchText.toLowerCase());
-    });
-
-    const displayedValues =
-      attrs.maxDisplayedItems === undefined
-        ? filteredValues
-        : filteredValues.slice(0, attrs.maxDisplayedItems);
-
-    const extraItems =
-      exists(attrs.maxDisplayedItems) &&
-      Math.max(0, filteredValues.length - attrs.maxDisplayedItems);
-
-    // TODO(altimin): when the user presses enter and there is only one item,
-    // select the first one.
-    // MAYBE(altimin): when the user presses enter and there are multiple items,
-    // select the first one.
-    return m(
-      'div',
-      m(
-        '.pf-search-bar',
-        m(TextInput, {
-          oninput: (event: Event) => {
-            const eventTarget = event.target as HTMLTextAreaElement;
-            this.searchText = eventTarget.value;
-            scheduleFullRedraw();
-          },
-          onload: (event: Event) => {
-            if (!attrs.autofocusInput) return;
-            const eventTarget = event.target as HTMLTextAreaElement;
-            eventTarget.focus();
-          },
-          value: this.searchText,
-          placeholder: 'Filter...',
-          className: 'pf-search-box',
-        }),
-        m(
-          Menu,
-          ...displayedValues.map((value) =>
-            m(MenuItem, {
-              label: value,
-              onclick: () => attrs.onSelected(value),
-            }),
-          ),
-          Boolean(extraItems) && m('i', `+${extraItems} more`),
-        ),
-      ),
-    );
-  }
-}
diff --git a/ui/src/widgets/text_input.ts b/ui/src/widgets/text_input.ts
index fe4487c..7aec3c8 100644
--- a/ui/src/widgets/text_input.ts
+++ b/ui/src/widgets/text_input.ts
@@ -16,13 +16,24 @@
 
 import {HTMLInputAttrs} from './common';
 
+type TextInputAttrs = HTMLInputAttrs & {
+  // Whether the input should autofocus when it is created.
+  autofocus?: boolean;
+};
+
 // For now, this component is just a simple wrapper around a plain old input
 // element, which does no more than specify a class. However, in the future we
 // might want to add more features such as an optional icon or button (e.g. a
 // clear button), at which point the benefit of having this as a component would
 // become more apparent.
-export class TextInput implements m.ClassComponent<HTMLInputAttrs> {
-  view({attrs}: m.CVnode<HTMLInputAttrs>) {
+export class TextInput implements m.ClassComponent<TextInputAttrs> {
+  oncreate(vnode: m.CVnodeDOM<TextInputAttrs>) {
+    if (vnode.attrs.autofocus) {
+      (vnode.dom as HTMLElement).focus();
+    }
+  }
+
+  view({attrs}: m.CVnode<TextInputAttrs>) {
     return m('input.pf-text-input', attrs);
   }
 }