Merge "[client] Implement thread functions for windows" into main
diff --git a/Android.bp b/Android.bp
index 9f5a62b..f074854 100644
--- a/Android.bp
+++ b/Android.bp
@@ -4638,6 +4638,7 @@
 genrule {
     name: "perfetto_protos_perfetto_metrics_chrome_descriptor",
     srcs: [
+        "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
@@ -4719,6 +4720,7 @@
 genrule {
     name: "perfetto_protos_perfetto_metrics_descriptor",
     srcs: [
+        "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
@@ -4783,6 +4785,7 @@
 genrule {
     name: "perfetto_protos_perfetto_metrics_webview_descriptor",
     srcs: [
+        "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
@@ -10614,6 +10617,7 @@
 genrule {
     name: "perfetto_src_trace_processor_metrics_sql_gen_amalgamated_sql_metrics",
     srcs: [
+        "src/trace_processor/metrics/sql/android/ad_services_metric.sql",
         "src/trace_processor/metrics/sql/android/android_anr.sql",
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
diff --git a/BUILD b/BUILD
index 26d176a..1ba49a5 100644
--- a/BUILD
+++ b/BUILD
@@ -1790,6 +1790,7 @@
 perfetto_filegroup(
     name = "src_trace_processor_metrics_sql_android_android",
     srcs = [
+        "src/trace_processor/metrics/sql/android/ad_services_metric.sql",
         "src/trace_processor/metrics/sql/android/android_anr.sql",
         "src/trace_processor/metrics/sql/android/android_batt.sql",
         "src/trace_processor/metrics/sql/android/android_binder.sql",
@@ -4034,6 +4035,7 @@
 perfetto_proto_library(
     name = "protos_perfetto_metrics_android_protos",
     srcs = [
+        "protos/perfetto/metrics/android/ad_services_metric.proto",
         "protos/perfetto/metrics/android/android_blocking_call.proto",
         "protos/perfetto/metrics/android/android_blocking_calls_cuj_metric.proto",
         "protos/perfetto/metrics/android/android_boot.proto",
diff --git a/docs/analysis/trace-processor.md b/docs/analysis/trace-processor.md
index 3fb44b5..a459566 100644
--- a/docs/analysis/trace-processor.md
+++ b/docs/analysis/trace-processor.md
@@ -600,53 +600,46 @@
 every change to trace processor code or builtin metrics.
 
 #### Choosing where to add diff tests
-Choosing a folder with a diff tests often can be confusing
-as a test can fall into more than one category. This section is a guide
-to decide which folder to choose.
+`diff_tests/` folder contains four directories corresponding to different
+areas of trace processor.
+1. __stdlib__: Tests focusing on testing Perfetto Standard Library, both
+   prelude and the regular modules. The subdirectories in this folder
+   should generally correspond to directories in `perfetto_sql/stdlib`.
+2. __parser__: Tests focusing on ensuring that different trace files are
+   parsed correctly and the corresponding built-in tables are populated.
+3. __metrics__: Tests focusing on testing metrics located in
+   `trace_processor/metrics/sql`. This organisation is mostly historical
+   and code (and corresponding tests) is expected to move to `stdlib` over time.
+4. __syntax__: Tests focusing on testing the core syntax of PerfettoSQL
+   (i.e. `CREATE PERFETTO TABLE` or `CREATE PERFETTO FUNCTION`).
 
-Broadly, there are two categories which all folders fall into:
-1. __"Area" folders__ which encompass a "vertical" area of interest
-   e.g. startup/ contains Android app startup related tests or chrome/
-   contains all Chrome related tests.
-2. __"Feature" folders__ which encompass a particular feature of
-   trace processor e.g. process_tracking/ tests the lifetime tracking of
-   processes, span_join/ tests the span join operator.
+__Scenario__: A new stdlib module `foo/bar.sql` is being added.
 
-"Area" folders should be preferred for adding tests unless the test is
-applicable to more than one "area"; in this case, one of "feature" folders
-can be used instead.
-
-Here are some common scenarios in which new tests may be added and
-answers on where to add the test:
+_Answer_: Add the test to the `stdlib/foo/bar_tests.py` file.
 
 __Scenario__: A new event is being parsed, the focus of the test is to ensure
-the event is being parsed correctly and the event is focused on a single
-vertical "Area".
+the event is being parsed correctly.
 
-_Answer_: Add the test in one of the "Area" folders.
-
-__Scenario__: A new event is being parsed and the focus of the test is to ensure
-the event is being parsed correctly and the event is applicable to more than one
-vertical "Area".
-
-_Answer_: Add the test to the parsing/ folder.
+_Answer_: Add the test in one of the `parser` subdirectories. Prefer adding a
+test to an existing related directory (i.e. `sched`, `power`) if one exists.
 
 __Scenario__: A new metric is being added and the focus of the test is to
 ensure the metric is being correctly computed.
 
-_Answer_: Add the test in one of the "Area" folders.
+_Answer_: Add the test in one of the `metrics` subdirectories. Prefer adding a
+test to an existing related directory if one exists. Also consider adding the
+code in question to stdlib.
 
 __Scenario__: A new dynamic table is being added and the focus of the test is to
 ensure the dynamic table is being correctly computed...
 
-_Answer_: Add the test to the dynamic/ folder
+_Answer_: Add the test to the `stdlib/dynamic_tables` folder
 
 __Scenario__: The interals of trace processor are being modified and the test
 is to ensure the trace processor is correctly filtering/sorting important
 built-in tables.
 
-_Answer_: Add the test to the tables/ folder.
-
+_Answer_: Add the test to the `parser/core_tables` folder.
 
 ## Appendix: table inheritance
 
diff --git a/protos/perfetto/config/interceptor_config.proto b/protos/perfetto/config/interceptor_config.proto
index 7ecc13b..9263bb5 100644
--- a/protos/perfetto/config/interceptor_config.proto
+++ b/protos/perfetto/config/interceptor_config.proto
@@ -27,5 +27,5 @@
   // Matches the name given to RegisterInterceptor().
   optional string name = 1;
 
-  optional ConsoleConfig console_config = 100 [lazy = true];
+  optional ConsoleConfig console_config = 100;
 }
diff --git a/protos/perfetto/config/perfetto_config.proto b/protos/perfetto/config/perfetto_config.proto
index bf19bbb..ade9e6b 100644
--- a/protos/perfetto/config/perfetto_config.proto
+++ b/protos/perfetto/config/perfetto_config.proto
@@ -901,7 +901,7 @@
   // Matches the name given to RegisterInterceptor().
   optional string name = 1;
 
-  optional ConsoleConfig console_config = 100 [lazy = true];
+  optional ConsoleConfig console_config = 100;
 }
 
 // End of protos/perfetto/config/interceptor_config.proto
diff --git a/protos/perfetto/metrics/android/BUILD.gn b/protos/perfetto/metrics/android/BUILD.gn
index a2ae264..2504f4c 100644
--- a/protos/perfetto/metrics/android/BUILD.gn
+++ b/protos/perfetto/metrics/android/BUILD.gn
@@ -20,6 +20,7 @@
     "source_set",
   ]
   sources = [
+    "ad_services_metric.proto",
     "android_blocking_call.proto",
     "android_blocking_calls_cuj_metric.proto",
     "android_boot.proto",
diff --git a/protos/perfetto/metrics/android/ad_services_metric.proto b/protos/perfetto/metrics/android/ad_services_metric.proto
new file mode 100644
index 0000000..c2ddcfd
--- /dev/null
+++ b/protos/perfetto/metrics/android/ad_services_metric.proto
@@ -0,0 +1,41 @@
+/*
+ * 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.
+ */
+
+syntax = "proto2";
+
+package perfetto.protos;
+
+// Next: 2
+message AdServicesUiMetric {
+  optional double latency = 1;
+}
+
+// Next: 2
+message AdServicesAdIdMetric {
+  optional double latency = 1;
+}
+
+// Next: 2
+message AdServicesAppSetIdMetric {
+  optional double latency = 1;
+}
+
+// Next: 4
+message AdServicesMetric {
+  repeated AdServicesUiMetric ui_metric = 1;
+  repeated AdServicesAdIdMetric ad_id_metric = 2;
+  repeated AdServicesAppSetIdMetric app_set_id_metric = 3;
+}
\ No newline at end of file
diff --git a/protos/perfetto/metrics/metrics.proto b/protos/perfetto/metrics/metrics.proto
index 95fd3b4..8330a3f 100644
--- a/protos/perfetto/metrics/metrics.proto
+++ b/protos/perfetto/metrics/metrics.proto
@@ -18,6 +18,7 @@
 
 package perfetto.protos;
 
+import "protos/perfetto/metrics/android/ad_services_metric.proto";
 import "protos/perfetto/metrics/android/android_boot.proto";
 import "protos/perfetto/metrics/android/android_frame_timeline_metric.proto";
 import "protos/perfetto/metrics/android/anr_metric.proto";
@@ -109,7 +110,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 58
+// Next id: 59
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -261,11 +262,15 @@
 
   // Metrics for App Not Responding (ANR) errors.
   optional AndroidAnrMetric android_anr = 55;
+
   // Aggregated Android Monitor Contention metrics
   optional AndroidMonitorContentionAggMetric android_monitor_contention_agg = 56;
 
   optional AndroidBootMetric android_boot = 57;
 
+  // Metric for AdServices module.
+  optional AdServicesMetric ad_services_metric = 58;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index e940682..3f270f6 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -13,6 +13,31 @@
 
 option go_package = "github.com/google/perfetto/perfetto_proto";
 
+// Begin of protos/perfetto/metrics/android/ad_services_metric.proto
+
+// Next: 2
+message AdServicesUiMetric {
+  optional double latency = 1;
+}
+
+// Next: 2
+message AdServicesAdIdMetric {
+  optional double latency = 1;
+}
+
+// Next: 2
+message AdServicesAppSetIdMetric {
+  optional double latency = 1;
+}
+
+// Next: 4
+message AdServicesMetric {
+  repeated AdServicesUiMetric ui_metric = 1;
+  repeated AdServicesAdIdMetric ad_id_metric = 2;
+  repeated AdServicesAppSetIdMetric app_set_id_metric = 3;
+}
+// End of protos/perfetto/metrics/android/ad_services_metric.proto
+
 // Begin of protos/perfetto/metrics/android/android_blocking_call.proto
 
 // Blocking call on the main thread.
@@ -2283,7 +2308,7 @@
 
 // Root message for all Perfetto-based metrics.
 //
-// Next id: 58
+// Next id: 59
 message TraceMetrics {
   reserved 4, 10, 13, 14, 16, 19;
 
@@ -2435,11 +2460,15 @@
 
   // Metrics for App Not Responding (ANR) errors.
   optional AndroidAnrMetric android_anr = 55;
+
   // Aggregated Android Monitor Contention metrics
   optional AndroidMonitorContentionAggMetric android_monitor_contention_agg = 56;
 
   optional AndroidBootMetric android_boot = 57;
 
+  // Metric for AdServices module.
+  optional AdServicesMetric ad_services_metric = 58;
+
   // Demo extensions.
   extensions 450 to 499;
 
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index a2c34a3..6470fdb 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -901,7 +901,7 @@
   // Matches the name given to RegisterInterceptor().
   optional string name = 1;
 
-  optional ConsoleConfig console_config = 100 [lazy = true];
+  optional ConsoleConfig console_config = 100;
 }
 
 // End of protos/perfetto/config/interceptor_config.proto
@@ -12495,7 +12495,7 @@
     optional uint64 user_ns = 2;
 
     // Time spent in user mode (low prio).
-    optional uint64 user_ice_ns = 3;
+    optional uint64 user_nice_ns = 3;
 
     // Time spent in system mode.
     optional uint64 system_mode_ns = 4;
diff --git a/protos/perfetto/trace/sys_stats/sys_stats.proto b/protos/perfetto/trace/sys_stats/sys_stats.proto
index 2aad34c..4bc538d 100644
--- a/protos/perfetto/trace/sys_stats/sys_stats.proto
+++ b/protos/perfetto/trace/sys_stats/sys_stats.proto
@@ -45,7 +45,7 @@
     optional uint64 user_ns = 2;
 
     // Time spent in user mode (low prio).
-    optional uint64 user_ice_ns = 3;
+    optional uint64 user_nice_ns = 3;
 
     // Time spent in system mode.
     optional uint64 system_mode_ns = 4;
diff --git a/python/generators/diff_tests/testing.py b/python/generators/diff_tests/testing.py
index 89aea8a..9bd6d84 100644
--- a/python/generators/diff_tests/testing.py
+++ b/python/generators/diff_tests/testing.py
@@ -177,7 +177,7 @@
     self.name = name
     self.blueprint = blueprint
     self.index_dir = index_dir
-    self.test_dir = os.path.dirname(os.path.dirname(os.path.dirname(index_dir)))
+    self.test_dir = os.path.abspath(os.path.join(__file__, '../../../../test'))
 
     if blueprint.is_metric():
       self.type = TestType.METRIC
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 926dee9..7af1fde 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/base/time_unittest.cc b/src/base/time_unittest.cc
index 6536bc8..a5b9d78 100644
--- a/src/base/time_unittest.cc
+++ b/src/base/time_unittest.cc
@@ -73,26 +73,31 @@
   EXPECT_LE(elapsed_cputime.count(), 50 * ns_in_ms);
 }
 
+// This test can work only on Posix platforms which respect the TZ env var.
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) ||   \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_APPLE)
 TEST(TimeTest, GetTimezoneOffsetMins) {
   const char* tz = getenv("TZ");
   std::string tz_save(tz ? tz : "");
   auto reset_tz_on_exit = OnScopeExit([&] {
     if (!tz_save.empty())
-      setenv("TZ", tz_save.c_str(), 1);
+      base::SetEnv("TZ", tz_save.c_str());
   });
 
   // Note: the sign is reversed in the semantic of the TZ env var.
   // UTC+2 means "2 hours to reach UTC", not "2 hours ahead of UTC".
 
-  setenv("TZ", "UTC+2", true);
+  base::SetEnv("TZ", "UTC+2");
   EXPECT_EQ(GetTimezoneOffsetMins(), -2 * 60);
 
-  setenv("TZ", "UTC-2", true);
+  base::SetEnv("TZ", "UTC-2");
   EXPECT_EQ(GetTimezoneOffsetMins(), 2 * 60);
 
-  setenv("TZ", "UTC-07:45", true);
+  base::SetEnv("TZ", "UTC-07:45");
   EXPECT_EQ(GetTimezoneOffsetMins(), 7 * 60 + 45);
 }
+#endif
 
 }  // namespace
 }  // namespace base
diff --git a/src/perfetto_cmd/perfetto_cmd.cc b/src/perfetto_cmd/perfetto_cmd.cc
index 2ddbf04..323d7bc 100644
--- a/src/perfetto_cmd/perfetto_cmd.cc
+++ b/src/perfetto_cmd/perfetto_cmd.cc
@@ -236,8 +236,9 @@
                              (e.g., file.0, file.1, file.2).
   --txt                    : Parse config as pbtxt. Not for production use.
                              Not a stable API.
-  --query                  : Queries the service state and prints it as
-                             human-readable text.
+  --query [--long]         : Queries the service state and prints it as
+                             human-readable text. --long allows the output to
+                             extend past 80 chars.
   --query-raw              : Like --query, but prints raw proto-encoded bytes
                              of tracing_service_state.proto.
   --help           -h
@@ -298,6 +299,7 @@
     OPT_IS_DETACHED,
     OPT_STOP,
     OPT_QUERY,
+    OPT_LONG,
     OPT_QUERY_RAW,
     OPT_VERSION,
   };
@@ -326,6 +328,7 @@
       {"is_detached", required_argument, nullptr, OPT_IS_DETACHED},
       {"stop", no_argument, nullptr, OPT_STOP},
       {"query", no_argument, nullptr, OPT_QUERY},
+      {"long", no_argument, nullptr, OPT_LONG},
       {"query-raw", no_argument, nullptr, OPT_QUERY_RAW},
       {"version", no_argument, nullptr, OPT_VERSION},
       {"save-for-bugreport", no_argument, nullptr, OPT_BUGREPORT},
@@ -520,6 +523,11 @@
       continue;
     }
 
+    if (option == OPT_LONG) {
+      query_service_long_ = true;
+      continue;
+    }
+
     if (option == OPT_QUERY_RAW) {
       query_service_ = true;
       query_service_output_raw_ = true;
@@ -550,6 +558,11 @@
     return 1;
   }
 
+  if (query_service_long_ && !query_service_) {
+    PERFETTO_ELOG("--long can only be used with --query");
+    return 1;
+  }
+
   if (is_detach() && is_attach()) {
     PERFETTO_ELOG("--attach and --detach are mutually exclusive");
     return 1;
@@ -1396,15 +1409,17 @@
       }
     }
 
-    printf("%-40s %-40s ", ds.ds_descriptor().name().c_str(),
+    printf("%-40s %-28s ", ds.ds_descriptor().name().c_str(),
            producer_id_and_name);
     // Print the category names for clients using the track event SDK.
+    std::string cats;
     if (!ds.ds_descriptor().track_event_descriptor_raw().empty()) {
       const std::string& raw = ds.ds_descriptor().track_event_descriptor_raw();
       protos::gen::TrackEventDescriptor desc;
       if (desc.ParseFromArray(raw.data(), raw.size())) {
         for (const auto& cat : desc.available_categories()) {
-          printf("%s,", cat.name().c_str());
+          cats.append(cats.empty() ? "" : ",");
+          cats.append(cat.name());
         }
       }
     } else if (!ds.ds_descriptor().ftrace_descriptor_raw().empty()) {
@@ -1412,11 +1427,17 @@
       protos::gen::FtraceDescriptor desc;
       if (desc.ParseFromArray(raw.data(), raw.size())) {
         for (const auto& cat : desc.atrace_categories()) {
-          printf("%s,", cat.name().c_str());
+          cats.append(cats.empty() ? "" : ",");
+          cats.append(cat.name());
         }
       }
     }
-    printf("\n");
+    const size_t kCatsShortLen = 40;
+    if (!query_service_long_ && cats.length() > kCatsShortLen) {
+      cats = cats.substr(0, kCatsShortLen);
+      cats.append("... (use --long to expand)");
+    }
+    printf("%s\n", cats.c_str());
   }  // for data_sources()
 
   if (svc_state.supports_tracing_sessions()) {
diff --git a/src/perfetto_cmd/perfetto_cmd.h b/src/perfetto_cmd/perfetto_cmd.h
index b55cbc8..7ac090f 100644
--- a/src/perfetto_cmd/perfetto_cmd.h
+++ b/src/perfetto_cmd/perfetto_cmd.h
@@ -153,6 +153,7 @@
   bool redetach_once_attached_ = false;
   bool query_service_ = false;
   bool query_service_output_raw_ = false;
+  bool query_service_long_ = false;
   bool bugreport_ = false;
   bool background_ = false;
   bool background_wait_ = false;
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 0eefa57..aa46613 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -312,7 +312,7 @@
     track = context_->track_tracker->InternCpuCounterTrack(
         cpu_times_user_nice_ns_id_, ct.cpu_id());
     context_->event_tracker->PushCounter(
-        ts, static_cast<double>(ct.user_ice_ns()), track);
+        ts, static_cast<double>(ct.user_nice_ns()), track);
 
     track = context_->track_tracker->InternCpuCounterTrack(
         cpu_times_system_mode_ns_id_, ct.cpu_id());
diff --git a/src/trace_processor/metrics/sql/android/BUILD.gn b/src/trace_processor/metrics/sql/android/BUILD.gn
index 6c31943..a2e64da 100644
--- a/src/trace_processor/metrics/sql/android/BUILD.gn
+++ b/src/trace_processor/metrics/sql/android/BUILD.gn
@@ -20,6 +20,7 @@
 perfetto_sql_source_set("android") {
   sources = [
     "android_anr.sql",
+    "ad_services_metric.sql",
     "android_batt.sql",
     "android_binder.sql",
     "android_blocking_calls_cuj_metric.sql",
diff --git a/src/trace_processor/metrics/sql/android/ad_services_metric.sql b/src/trace_processor/metrics/sql/android/ad_services_metric.sql
new file mode 100644
index 0000000..36885fd
--- /dev/null
+++ b/src/trace_processor/metrics/sql/android/ad_services_metric.sql
@@ -0,0 +1,59 @@
+--
+-- Copyright 2023 The Android Open Source Project
+--
+-- Licensed under the Apache License, Version 2.0 (the "License");
+-- you may not use this file except in compliance with the License.
+-- You may obtain a copy of the License at
+--
+--     https://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+
+CREATE OR REPLACE PERFETTO FUNCTION GET_EVENT_LATENCY_TABLE(event_name STRING)
+RETURNS TABLE (latency LONG) AS
+SELECT
+  dur / 1e6 as latency
+FROM
+  slices
+WHERE
+  name = $event_name;
+
+DROP VIEW IF EXISTS ad_services_metric_output;
+CREATE VIEW ad_services_metric_output AS
+SELECT
+  AdServicesMetric(
+    'ui_metric',
+    (
+      SELECT
+        RepeatedField(
+          AdServicesUiMetric('latency', latency)
+        )
+      FROM
+        GET_EVENT_LATENCY_TABLE("NotificationTriggerEvent")
+    ),
+    'app_set_id_metric',
+    (
+      SELECT
+        RepeatedField(
+          AdServicesAppSetIdMetric(
+            'latency', latency
+          )
+        )
+      FROM
+        GET_EVENT_LATENCY_TABLE("AdIdCacheEvent")
+    ),
+    'ad_id_metric',
+    (
+      SELECT
+        RepeatedField(
+          AdServicesAdIdMetric('latency', latency)
+        )
+      FROM
+        GET_EVENT_LATENCY_TABLE("AppSetIdEvent")
+    )
+);
diff --git a/src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql b/src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql
index 9257d84..6c246e8 100644
--- a/src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql
+++ b/src/trace_processor/metrics/sql/chrome/scroll_jank_cause_queuing_delay.sql
@@ -357,6 +357,8 @@
     "TabGroupUiToolbarView"
   WHEN $name GLOB "*TabGridThumbnailView*" THEN
     "TabGridThumbnailView"
+  WHEN $name GLOB "*TabThumbnailView" THEN
+    "TabThumbnailView"
   WHEN $name GLOB "*TabGridDialogView*" THEN
     "TabGridDialogView"
   WHEN $name GLOB "*BottomContainer*" THEN
diff --git a/src/traced/probes/sys_stats/sys_stats_data_source.cc b/src/traced/probes/sys_stats/sys_stats_data_source.cc
index e5c02a9..32cd67f 100644
--- a/src/traced/probes/sys_stats/sys_stats_data_source.cc
+++ b/src/traced/probes/sys_stats/sys_stats_data_source.cc
@@ -438,7 +438,7 @@
       auto* cpu_stat = sys_stats->add_cpu_stat();
       cpu_stat->set_cpu_id(static_cast<uint32_t>(cpu_id));
       cpu_stat->set_user_ns(cpu_times[0] * ns_per_user_hz_);
-      cpu_stat->set_user_ice_ns(cpu_times[1] * ns_per_user_hz_);
+      cpu_stat->set_user_nice_ns(cpu_times[1] * ns_per_user_hz_);
       cpu_stat->set_system_mode_ns(cpu_times[2] * ns_per_user_hz_);
       cpu_stat->set_idle_ns(cpu_times[3] * ns_per_user_hz_);
       cpu_stat->set_io_wait_ns(cpu_times[4] * ns_per_user_hz_);
diff --git a/src/tracing/BUILD.gn b/src/tracing/BUILD.gn
index 19b3205..f0a04eb 100644
--- a/src/tracing/BUILD.gn
+++ b/src/tracing/BUILD.gn
@@ -92,7 +92,7 @@
     "../../include/perfetto/tracing/core",
     "../../protos/perfetto/common:zero",
     "../../protos/perfetto/config:cpp",
-    "../../protos/perfetto/config/interceptors:zero",
+    "../../protos/perfetto/config/interceptors:cpp",
     "../../protos/perfetto/config/track_event:cpp",
     "../base",
     "core",
diff --git a/src/tracing/console_interceptor.cc b/src/tracing/console_interceptor.cc
index 2c3221f..a594409 100644
--- a/src/tracing/console_interceptor.cc
+++ b/src/tracing/console_interceptor.cc
@@ -33,7 +33,7 @@
 #include "protos/perfetto/common/interceptor_descriptor.gen.h"
 #include "protos/perfetto/config/data_source_config.gen.h"
 #include "protos/perfetto/config/interceptor_config.gen.h"
-#include "protos/perfetto/config/interceptors/console_config.pbzero.h"
+#include "protos/perfetto/config/interceptors/console_config.gen.h"
 #include "protos/perfetto/trace/interned_data/interned_data.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
@@ -275,13 +275,13 @@
 #else
   bool use_colors = false;
 #endif
-  protos::pbzero::ConsoleConfig::Decoder config(
-      args.config.interceptor_config().console_config_raw());
+  const protos::gen::ConsoleConfig& config =
+      args.config.interceptor_config().console_config();
   if (config.has_enable_colors())
     use_colors = config.enable_colors();
-  if (config.output() == protos::pbzero::ConsoleConfig::OUTPUT_STDOUT) {
+  if (config.output() == protos::gen::ConsoleConfig::OUTPUT_STDOUT) {
     fd = STDOUT_FILENO;
-  } else if (config.output() == protos::pbzero::ConsoleConfig::OUTPUT_STDERR) {
+  } else if (config.output() == protos::gen::ConsoleConfig::OUTPUT_STDERR) {
     fd = STDERR_FILENO;
   }
   fd_ = fd;
diff --git a/test/trace_processor/diff_tests/android/ad_services_metric.py b/test/trace_processor/diff_tests/android/ad_services_metric.py
new file mode 100644
index 0000000..efa827f
--- /dev/null
+++ b/test/trace_processor/diff_tests/android/ad_services_metric.py
@@ -0,0 +1,40 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT 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 os import sys, path
+
+import synth_common
+
+UI_NOTIFICATION_TRIGGER_EVENT = "NotificationTriggerEvent"
+AD_ID_CACHE_EVENT = "AdIdCacheEvent"
+APP_SET_ID_EVENT = "AppSetIdEvent"
+
+trace = synth_common.create_trace()
+
+trace.add_ftrace_packet(cpu=0)
+
+trace.add_sys_enter(ts=100, tid=42, id=64)
+trace.add_sys_exit(ts=200, tid=42, id=64, ret=0)
+
+trace.add_atrace_begin(ts=350, tid=42, pid=42, buf=UI_NOTIFICATION_TRIGGER_EVENT)
+trace.add_atrace_end(ts=650, tid=42, pid=42)
+
+trace.add_atrace_begin(ts=750, tid=42, pid=42, buf=AD_ID_CACHE_EVENT)
+trace.add_atrace_end(ts=850, tid=42, pid=42)
+
+trace.add_atrace_begin(ts=900, tid=42, pid=42, buf=APP_SET_ID_EVENT)
+trace.add_atrace_end(ts=1200, tid=42, pid=42)
+
+sys.stdout.buffer.write(trace.trace.SerializeToString())
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/android/tests.py b/test/trace_processor/diff_tests/android/tests.py
index 8bbbeb7..58410ce 100644
--- a/test/trace_processor/diff_tests/android/tests.py
+++ b/test/trace_processor/diff_tests/android/tests.py
@@ -1186,3 +1186,22 @@
           }
         }
         """))
+
+  def test_ad_services_metric(self):
+    return DiffTestBlueprint(
+        trace=Path('ad_services_metric.py'),
+        query=Metric('ad_services_metric'),
+        out=TextProto(r"""
+         ad_services_metric {
+           ui_metric {
+             latency: 0.0003
+           }
+           app_set_id_metric {
+             latency: 0.0001
+           }
+           ad_id_metric {
+             latency:0.0003
+           }
+         }
+        """)
+    )
diff --git a/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py b/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
index ef8ec6a..deb365a 100644
--- a/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
+++ b/test/trace_processor/diff_tests/chrome/tests_scroll_jank.py
@@ -355,7 +355,7 @@
 
   def test_chrome_thread_slice_repeated(self):
     return DiffTestBlueprint(
-        trace=Path('../track_event/track_event_counters.textproto'),
+        trace=Path('../parser/track_event/track_event_counters.textproto'),
         query="""
         SELECT RUN_METRIC('chrome/chrome_thread_slice.sql');
 
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index b131e43..a0bcb08 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -30,9 +30,6 @@
 from diff_tests.android.tests_games import AndroidGames
 from diff_tests.android.tests_surfaceflinger_layers import SurfaceFlingerLayers
 from diff_tests.android.tests_surfaceflinger_transactions import SurfaceFlingerTransactions
-from diff_tests.atrace.tests import Atrace
-from diff_tests.atrace.tests_error_handling import AtraceErrorHandling
-from diff_tests.camera.tests import Camera
 from diff_tests.chrome.tests import Chrome
 from diff_tests.chrome.tests_args import ChromeArgs
 from diff_tests.chrome.tests_memory_snapshots import ChromeMemorySnapshots
@@ -40,74 +37,151 @@
 from diff_tests.chrome.tests_rail_modes import ChromeRailModes
 from diff_tests.chrome.tests_scroll_jank import ChromeScrollJank
 from diff_tests.chrome.tests_touch_gesture import ChromeTouchGesture
-from diff_tests.codecs.tests import Codecs
-from diff_tests.cros.tests import Cros
-from diff_tests.dynamic.tests import Dynamic
-from diff_tests.fs.tests import Fs
-from diff_tests.fuchsia.tests import Fuchsia
-from diff_tests.functions.tests import Functions
 from diff_tests.graphics.tests import Graphics
-from diff_tests.graphics.tests_drm_related_ftrace_events import \
-    GraphicsDrmRelatedFtraceEvents
+from diff_tests.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
 from diff_tests.graphics.tests_gpu_trace import GraphicsGpuTrace
-from diff_tests.memory.tests import Memory
-from diff_tests.memory.tests_metrics import MemoryMetrics
-from diff_tests.network.tests import Network
-from diff_tests.parsing.tests import Parsing
-from diff_tests.parsing.tests_debug_annotation import ParsingDebugAnnotation
-from diff_tests.parsing.tests_memory_counters import ParsingMemoryCounters
-from diff_tests.parsing.tests_rss_stats import ParsingRssStats
-from diff_tests.perfetto_sql.tests import PerfettoSql
-from diff_tests.performance.tests import Performance
-from diff_tests.pkvm.tests import Pkvm
+from diff_tests.metrics.camera.tests import Camera
+from diff_tests.metrics.codecs.tests import Codecs
+from diff_tests.metrics.frame_timeline.tests import FrameTimeline
+from diff_tests.metrics.irq.tests import IRQ
+from diff_tests.metrics.memory.tests import MemoryMetrics
+from diff_tests.metrics.network.tests import NetworkMetrics
+from diff_tests.metrics.profiling.tests import ProfilingMetrics
+from diff_tests.metrics.startup.tests import Startup
+from diff_tests.metrics.startup.tests_broadcasts import StartupBroadcasts
+from diff_tests.metrics.startup.tests_lock_contention import StartupLockContention
+from diff_tests.metrics.startup.tests_metrics import StartupMetrics
+from diff_tests.metrics.webview.tests import WebView
+from diff_tests.parser.android_fs.tests import AndroidFs
+from diff_tests.parser.atrace.tests import Atrace
+from diff_tests.parser.atrace.tests_error_handling import AtraceErrorHandling
+from diff_tests.parser.cros.tests import Cros
+from diff_tests.parser.fs.tests import Fs
+from diff_tests.parser.fuchsia.tests import Fuchsia
+from diff_tests.parser.memory.tests import MemoryParser
+from diff_tests.parser.network.tests import NetworkParser
+from diff_tests.parser.parsing.tests import Parsing
+from diff_tests.parser.parsing.tests_debug_annotation import ParsingDebugAnnotation
+from diff_tests.parser.parsing.tests_memory_counters import ParsingMemoryCounters
+from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
+from diff_tests.parser.process_tracking.tests import ProcessTracking
+from diff_tests.parser.profiling.tests import Profiling
+from diff_tests.parser.profiling.tests_heap_graph import ProfilingHeapGraph
+from diff_tests.parser.profiling.tests_heap_profiling import ProfilingHeapProfiling
+from diff_tests.parser.profiling.tests_llvm_symbolizer import ProfilingLlvmSymbolizer
+from diff_tests.parser.sched.tests import SchedParser
+from diff_tests.parser.smoke.tests import Smoke
+from diff_tests.parser.smoke.tests_compute_metrics import SmokeComputeMetrics
+from diff_tests.parser.smoke.tests_json import SmokeJson
+from diff_tests.parser.smoke.tests_sched_events import SmokeSchedEvents
+from diff_tests.parser.track_event.tests import TrackEvent
+from diff_tests.parser.translated_args.tests import TranslatedArgs
+from diff_tests.parser.ufs.tests import Ufs
 from diff_tests.power.tests import Power
 from diff_tests.power.tests_energy_breakdown import PowerEnergyBreakdown
 from diff_tests.power.tests_entity_state_residency import EntityStateResidency
 from diff_tests.power.tests_linux_sysfs_power import LinuxSysfsPower
 from diff_tests.power.tests_power_rails import PowerPowerRails
 from diff_tests.power.tests_voltage_and_scaling import PowerVoltageAndScaling
-from diff_tests.process_tracking.tests import ProcessTracking
-from diff_tests.profiling.tests import Profiling
-from diff_tests.profiling.tests_heap_graph import ProfilingHeapGraph
-from diff_tests.profiling.tests_heap_profiling import ProfilingHeapProfiling
-from diff_tests.profiling.tests_llvm_symbolizer import ProfilingLlvmSymbolizer
-from diff_tests.profiling.tests_metrics import ProfilingMetrics
-from diff_tests.scheduler.tests import Scheduler
-from diff_tests.slices.tests import Slices
-from diff_tests.smoke.tests import Smoke
-from diff_tests.smoke.tests_compute_metrics import SmokeComputeMetrics
-from diff_tests.smoke.tests_json import SmokeJson
-from diff_tests.smoke.tests_sched_events import SmokeSchedEvents
-from diff_tests.span_join.tests_left_join import SpanJoinLeftJoin
-from diff_tests.span_join.tests_outer_join import SpanJoinOuterJoin
-from diff_tests.span_join.tests_regression import SpanJoinRegression
-from diff_tests.span_join.tests_smoke import SpanJoinSmoke
-from diff_tests.startup.tests import Startup
-from diff_tests.startup.tests_broadcasts import StartupBroadcasts
-from diff_tests.startup.tests_lock_contention import StartupLockContention
-from diff_tests.startup.tests_metrics import StartupMetrics
+from diff_tests.stdlib.dynamic_tables.tests import DynamicTables
+from diff_tests.stdlib.pkvm.tests import Pkvm
+from diff_tests.stdlib.slices.tests import Slices
+from diff_tests.stdlib.span_join.tests_left_join import SpanJoinLeftJoin
+from diff_tests.stdlib.span_join.tests_outer_join import SpanJoinOuterJoin
+from diff_tests.stdlib.span_join.tests_regression import SpanJoinRegression
+from diff_tests.stdlib.span_join.tests_smoke import SpanJoinSmoke
+from diff_tests.stdlib.timestamps.tests import Timestamps
+from diff_tests.syntax.functions.tests import Functions
+from diff_tests.syntax.perfetto_sql.tests import PerfettoSql
 from diff_tests.tables.tests import Tables
 from diff_tests.tables.tests_counters import TablesCounters
 from diff_tests.tables.tests_sched import TablesSched
-from diff_tests.time.tests import Time
-from diff_tests.track_event.tests import TrackEvent
-from diff_tests.translation.tests import Translation
-from diff_tests.ufs.tests import Ufs
-from diff_tests.webview.tests import WebView
-from diff_tests.android_fs.tests import AndroidFs
 
 sys.path.pop()
 
 
 def fetch_all_diff_tests(index_path: str) -> List['testing.TestCase']:
-  return [
+  parser_tests = [
+      *AndroidFs(index_path, 'parser/android_fs', 'AndroidFs').fetch(),
+      *Atrace(index_path, 'parser/atrace', 'Atrace').fetch(),
+      *AtraceErrorHandling(index_path, 'parser/atrace',
+                           'AtraceErrorHandling').fetch(),
+      *Cros(index_path, 'parser/cros', 'Cros').fetch(),
+      *Fs(index_path, 'parser/fs', 'Fs').fetch(),
+      *Fuchsia(index_path, 'parser/fuchsia', 'Fuchsia').fetch(),
+      *MemoryParser(index_path, 'parser/memory', 'MemoryParser').fetch(),
+      *NetworkParser(index_path, 'parser/network', 'NetworkParser').fetch(),
+      *ProcessTracking(index_path, 'parser/process_tracking',
+                       'ProcessTracking').fetch(),
+      *Profiling(index_path, 'parser/profiling', 'Profiling').fetch(),
+      *ProfilingHeapProfiling(index_path, 'parser/profiling',
+                              'ProfilingHeapProfiling').fetch(),
+      *ProfilingHeapGraph(index_path, 'parser/profiling',
+                          'ProfilingHeapGraph').fetch(),
+      *ProfilingLlvmSymbolizer(index_path, 'parser/profiling',
+                               'ProfilingLlvmSymbolizer').fetch(),
+      *Smoke(index_path, 'parser/smoke', 'Smoke').fetch(),
+      *SchedParser(index_path, 'parser/sched', 'SchedParser').fetch(),
+      *SmokeComputeMetrics(index_path, 'parser/smoke',
+                           'SmokeComputeMetrics').fetch(),
+      *SmokeJson(index_path, 'parser/smoke', 'SmokeJson').fetch(),
+      *SmokeSchedEvents(index_path, 'parser/smoke', 'SmokeSchedEvents').fetch(),
+      *TrackEvent(index_path, 'parser/track_event', 'TrackEvent').fetch(),
+      *TranslatedArgs(index_path, 'parser/translated_args',
+                      'TranslatedArgs').fetch(),
+      *Ufs(index_path, 'parser/ufs', 'Ufs').fetch(),
+      # TODO(altimin, lalitm): "parsing" should be split into more specific directories.
+      *Parsing(index_path, 'parser/parsing', 'Parsing').fetch(),
+      *ParsingDebugAnnotation(index_path, 'parser/parsing',
+                              'ParsingDebugAnnotation').fetch(),
+      *ParsingRssStats(index_path, 'parser/parsing', 'ParsingRssStats').fetch(),
+      *ParsingMemoryCounters(index_path, 'parser/parsing',
+                             'ParsingMemoryCounters').fetch(),
+  ]
+
+  metrics_tests = [
+      *Camera(index_path, 'metrics/camera', 'Camera').fetch(),
+      *Codecs(index_path, 'metrics/codecs', 'Codecs').fetch(),
+      *MemoryMetrics(index_path, 'metrics/memory', 'MemoryMetrics').fetch(),
+      *NetworkMetrics(index_path, 'metrics/network', 'NetworkMetrics').fetch(),
+      *FrameTimeline(index_path, 'metrics/frame_timeline',
+                     'FrameTimeline').fetch(),
+      *IRQ(index_path, 'metrics/irq', 'IRQ').fetch(),
+      *ProfilingMetrics(index_path, 'metrics/profiling',
+                        'ProfilingMetrics').fetch(),
+      *Startup(index_path, 'metrics/startup', 'Startup').fetch(),
+      *StartupBroadcasts(index_path, 'metrics/startup',
+                         'StartupBroadcasts').fetch(),
+      *StartupMetrics(index_path, 'metrics/startup', 'StartupMetrics').fetch(),
+      *StartupLockContention(index_path, 'metrics/startup',
+                             'StartupLockContention').fetch(),
+      *WebView(index_path, 'metrics/webview', 'WebView').fetch(),
+  ]
+
+  stdlib_tests = [
+      *DynamicTables(index_path, 'stdlib/dynamic_tables',
+                     'DynamicTables').fetch(),
+      *Pkvm(index_path, 'stdlib/pkvm', 'Pkvm').fetch(),
+      *Slices(index_path, 'stdlib/slices', 'Slices').fetch(),
+      *SpanJoinLeftJoin(index_path, 'stdlib/span_join',
+                        'SpanJoinLeftJoin').fetch(),
+      *SpanJoinOuterJoin(index_path, 'stdlib/span_join',
+                         'SpanJoinOuterJoin').fetch(),
+      *SpanJoinSmoke(index_path, 'stdlib/span_join', 'SpanJoinSmoke').fetch(),
+      *SpanJoinRegression(index_path, 'stdlib/span_join',
+                          'SpanJoinRegression').fetch(),
+      *Timestamps(index_path, 'stdlib/timestamps', 'Timestamps').fetch(),
+  ]
+
+  syntax_tests = [
+      *Functions(index_path, 'syntax/functions', 'Functions').fetch(),
+      *PerfettoSql(index_path, 'syntax/perfetto_sql', 'PerfettoSql').fetch(),
+  ]
+
+  return parser_tests + metrics_tests + stdlib_tests + syntax_tests + [
       *Android(index_path, 'android', 'Android').fetch(),
       *AndroidBugreport(index_path, 'android', 'AndroidBugreport').fetch(),
-      *AndroidFs(index_path, 'android_fs', 'AndroidFs').fetch(),
       *AndroidGames(index_path, 'android', 'AndroidGames').fetch(),
-      *Atrace(index_path, 'atrace', 'Atrace').fetch(),
-      *AtraceErrorHandling(index_path, 'atrace', 'AtraceErrorHandling').fetch(),
-      *Camera(index_path, 'camera', 'Camera').fetch(),
       *ChromeScrollJank(index_path, 'chrome', 'ChromeScrollJank').fetch(),
       *ChromeTouchGesture(index_path, 'chrome', 'ChromeTouchGesture').fetch(),
       *ChromeMemorySnapshots(index_path, 'chrome',
@@ -116,64 +190,19 @@
       *ChromeProcesses(index_path, 'chrome', 'ChromeProcesses').fetch(),
       *ChromeArgs(index_path, 'chrome', 'ChromeArgs').fetch(),
       *Chrome(index_path, 'chrome', 'Chrome').fetch(),
-      *Codecs(index_path, 'codecs', 'Codecs').fetch(),
-      *Cros(index_path, 'cros', 'Cros').fetch(),
-      *Dynamic(index_path, 'dynamic', 'Dynamic').fetch(),
       *EntityStateResidency(index_path, 'power',
                             'EntityStateResidency').fetch(),
-      *Fs(index_path, 'fs', 'Fs').fetch(),
-      *Fuchsia(index_path, 'fuchsia', 'Fuchsia').fetch(),
-      *Functions(index_path, 'functions', 'Functions').fetch(),
       *Graphics(index_path, 'graphics', 'Graphics').fetch(),
       *GraphicsGpuTrace(index_path, 'graphics', 'GraphicsGpuTrace').fetch(),
       *GraphicsDrmRelatedFtraceEvents(index_path, 'graphics',
                                       'GraphicsDrmRelatedFtraceEvents').fetch(),
-      *Ufs(index_path, 'ufs', 'Ufs').fetch(),
       *LinuxSysfsPower(index_path, 'power', 'LinuxSysfsPower').fetch(),
-      *Memory(index_path, 'memory', 'Memory').fetch(),
-      *MemoryMetrics(index_path, 'memory', 'MemoryMetrics').fetch(),
-      *Network(index_path, 'network', 'Network').fetch(),
-      *Parsing(index_path, 'parsing', 'Parsing').fetch(),
-      *ParsingDebugAnnotation(index_path, 'parsing',
-                              'ParsingDebugAnnotation').fetch(),
-      *ParsingRssStats(index_path, 'parsing', 'ParsingRssStats').fetch(),
-      *ParsingMemoryCounters(index_path, 'parsing',
-                             'ParsingMemoryCounters').fetch(),
-      *PerfettoSql(index_path, 'perfetto_sql', 'PerfettoSql').fetch(),
-      *Performance(index_path, 'performance', 'Performance').fetch(),
-      *Pkvm(index_path, 'pkvm', 'Pkvm').fetch(),
       *Power(index_path, 'power', 'Power').fetch(),
       *PowerPowerRails(index_path, 'power', 'PowerPowerRails').fetch(),
       *PowerVoltageAndScaling(index_path, 'power',
                               'PowerVoltageAndScaling').fetch(),
       *PowerEnergyBreakdown(index_path, 'power',
                             'PowerEnergyBreakdown').fetch(),
-      *ProcessTracking(index_path, 'process_tracking',
-                       'ProcessTracking').fetch(),
-      *Profiling(index_path, 'profiling', 'Profiling').fetch(),
-      *ProfilingHeapProfiling(index_path, 'profiling',
-                              'ProfilingHeapProfiling').fetch(),
-      *ProfilingHeapGraph(index_path, 'profiling',
-                          'ProfilingHeapGraph').fetch(),
-      *ProfilingMetrics(index_path, 'profiling', 'ProfilingMetrics').fetch(),
-      *ProfilingLlvmSymbolizer(index_path, 'profiling',
-                               'ProfilingLlvmSymbolizer').fetch(),
-      *Scheduler(index_path, 'scheduler', 'Scheduler').fetch(),
-      *Slices(index_path, 'slices', 'Slices').fetch(),
-      *Smoke(index_path, 'smoke', 'Smoke').fetch(),
-      *SmokeComputeMetrics(index_path, 'smoke', 'SmokeComputeMetrics').fetch(),
-      *SmokeJson(index_path, 'smoke', 'SmokeJson').fetch(),
-      *SmokeSchedEvents(index_path, 'smoke', 'SmokeSchedEvents').fetch(),
-      *SpanJoinLeftJoin(index_path, 'span_join', 'SpanJoinLeftJoin').fetch(),
-      *SpanJoinOuterJoin(index_path, 'span_join', 'SpanJoinOuterJoin').fetch(),
-      *SpanJoinSmoke(index_path, 'span_join', 'SpanJoinSmoke').fetch(),
-      *SpanJoinRegression(index_path, 'span_join',
-                          'SpanJoinRegression').fetch(),
-      *Startup(index_path, 'startup', 'Startup').fetch(),
-      *StartupBroadcasts(index_path, 'startup', 'StartupBroadcasts').fetch(),
-      *StartupMetrics(index_path, 'startup', 'StartupMetrics').fetch(),
-      *StartupLockContention(index_path, 'startup',
-                             'StartupLockContention').fetch(),
       *SurfaceFlingerLayers(index_path, 'android',
                             'SurfaceFlingerLayers').fetch(),
       *SurfaceFlingerTransactions(index_path, 'android',
@@ -181,8 +210,4 @@
       *Tables(index_path, 'tables', 'Tables').fetch(),
       *TablesCounters(index_path, 'tables', 'TablesCounters').fetch(),
       *TablesSched(index_path, 'tables', 'TablesSched').fetch(),
-      *Time(index_path, 'time', 'Time').fetch(),
-      *TrackEvent(index_path, 'track_event', 'TrackEvent').fetch(),
-      *Translation(index_path, 'translation', 'Translation').fetch(),
-      *WebView(index_path, 'webview', 'WebView').fetch(),
   ]
diff --git a/test/trace_processor/diff_tests/memory/tests_metrics.py b/test/trace_processor/diff_tests/memory/tests_metrics.py
deleted file mode 100644
index 8a45185..0000000
--- a/test/trace_processor/diff_tests/memory/tests_metrics.py
+++ /dev/null
@@ -1,112 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License a
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class MemoryMetrics(TestSuite):
-
-  def test_android_mem_counters(self):
-    return DiffTestBlueprint(
-        trace=DataPath('memory_counters.pb'),
-        query=Metric('android_mem'),
-        out=Path('android_mem_counters.out'))
-
-  def test_trace_metadata(self):
-    return DiffTestBlueprint(
-        trace=DataPath('memory_counters.pb'),
-        query=Metric('trace_metadata'),
-        out=Path('trace_metadata.out'))
-
-  def test_android_mem_by_priority(self):
-    return DiffTestBlueprint(
-        trace=Path('android_mem_by_priority.py'),
-        query=Metric('android_mem'),
-        out=Path('android_mem_by_priority.out'))
-
-  def test_android_mem_lmk(self):
-    return DiffTestBlueprint(
-        trace=Path('android_systrace_lmk.py'),
-        query=Metric('android_lmk'),
-        out=TextProto(r"""
-        android_lmk {
-          total_count: 1
-            by_oom_score {
-            oom_score_adj: 900
-            count: 1
-          }
-          oom_victim_count: 0
-        }
-        """))
-
-  def test_android_lmk_oom(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          process_tree {
-            processes {
-              pid: 1000
-              ppid: 1
-              cmdline: "com.google.android.gm"
-            }
-            threads {
-              tid: 1001
-              tgid: 1000
-            }
-          }
-        }
-        packet {
-          ftrace_events {
-            cpu: 4
-            event {
-              timestamp: 1234
-              pid: 4321
-              mark_victim {
-                pid: 1001
-              }
-            }
-          }
-        }
-        """),
-        query=Metric('android_lmk'),
-        out=TextProto(r"""
-        android_lmk {
-          total_count: 0
-          oom_victim_count: 1
-        }
-        """))
-
-  def test_android_mem_delta(self):
-    return DiffTestBlueprint(
-        trace=Path('android_mem_delta.py'),
-        query=Metric('android_mem'),
-        out=TextProto(r"""
-        android_mem {
-          process_metrics {
-            process_name: "com.my.pkg"
-            total_counters {
-              file_rss {
-                min: 2000.0
-                max: 10000.0
-                avg: 6666.666666666667
-                delta: 7000.0
-              }
-            }
-          }
-        }
-        """))
diff --git a/test/trace_processor/diff_tests/camera/camera-ion-mem-trace_android_camera_unagg.out b/test/trace_processor/diff_tests/metrics/camera/camera-ion-mem-trace_android_camera_unagg.out
similarity index 100%
rename from test/trace_processor/diff_tests/camera/camera-ion-mem-trace_android_camera_unagg.out
rename to test/trace_processor/diff_tests/metrics/camera/camera-ion-mem-trace_android_camera_unagg.out
diff --git a/test/trace_processor/diff_tests/camera/tests.py b/test/trace_processor/diff_tests/metrics/camera/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/camera/tests.py
rename to test/trace_processor/diff_tests/metrics/camera/tests.py
diff --git a/test/trace_processor/diff_tests/codecs/codec-framedecoder-trace.out b/test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out
similarity index 100%
rename from test/trace_processor/diff_tests/codecs/codec-framedecoder-trace.out
rename to test/trace_processor/diff_tests/metrics/codecs/codec-framedecoder-trace.out
diff --git a/test/trace_processor/diff_tests/codecs/tests.py b/test/trace_processor/diff_tests/metrics/codecs/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/codecs/tests.py
rename to test/trace_processor/diff_tests/metrics/codecs/tests.py
diff --git a/test/trace_processor/diff_tests/performance/frame_timeline_metric.out b/test/trace_processor/diff_tests/metrics/frame_timeline/frame_timeline_metric.out
similarity index 100%
rename from test/trace_processor/diff_tests/performance/frame_timeline_metric.out
rename to test/trace_processor/diff_tests/metrics/frame_timeline/frame_timeline_metric.out
diff --git a/test/trace_processor/diff_tests/performance/frame_timeline_metric.py b/test/trace_processor/diff_tests/metrics/frame_timeline/frame_timeline_metric.py
similarity index 99%
rename from test/trace_processor/diff_tests/performance/frame_timeline_metric.py
rename to test/trace_processor/diff_tests/metrics/frame_timeline/frame_timeline_metric.py
index 05cfee3..c7c1885 100755
--- a/test/trace_processor/diff_tests/performance/frame_timeline_metric.py
+++ b/test/trace_processor/diff_tests/metrics/frame_timeline/frame_timeline_metric.py
@@ -181,7 +181,6 @@
     prediction_type=PredictionType.PREDICTION_VALID)
 trace.add_frame_end_event(ts=14000000, cookie=25)
 
-
 trace.add_actual_surface_frame_start_event(
     ts=14500000,
     cookie=30,
@@ -196,7 +195,6 @@
     prediction_type=PredictionType.PREDICTION_VALID)
 trace.add_frame_end_event(ts=15000000, cookie=30)
 
-
 trace.add_actual_surface_frame_start_event(
     ts=15500000,
     cookie=35,
@@ -211,7 +209,6 @@
     prediction_type=PredictionType.PREDICTION_VALID)
 trace.add_frame_end_event(ts=16000000, cookie=35)
 
-
 trace.add_actual_surface_frame_start_event(
     ts=16500000,
     cookie=40,
diff --git a/test/trace_processor/diff_tests/metrics/frame_timeline/tests.py b/test/trace_processor/diff_tests/metrics/frame_timeline/tests.py
new file mode 100644
index 0000000..24ef27a
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/frame_timeline/tests.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class FrameTimeline(TestSuite):
+  # frame_timeline_metric collects App_Deadline_Missed metrics
+  def test_frame_timeline_metric(self):
+    return DiffTestBlueprint(
+        trace=Path('frame_timeline_metric.py'),
+        query=Metric('android_frame_timeline_metric'),
+        out=Path('frame_timeline_metric.out'))
diff --git a/test/trace_processor/diff_tests/performance/irq_runtime_metric.out b/test/trace_processor/diff_tests/metrics/irq/irq_runtime_metric.out
similarity index 100%
rename from test/trace_processor/diff_tests/performance/irq_runtime_metric.out
rename to test/trace_processor/diff_tests/metrics/irq/irq_runtime_metric.out
diff --git a/test/trace_processor/diff_tests/performance/irq_runtime_metric.textproto b/test/trace_processor/diff_tests/metrics/irq/irq_runtime_metric.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/performance/irq_runtime_metric.textproto
rename to test/trace_processor/diff_tests/metrics/irq/irq_runtime_metric.textproto
diff --git a/test/trace_processor/diff_tests/metrics/irq/tests.py b/test/trace_processor/diff_tests/metrics/irq/tests.py
new file mode 100644
index 0000000..f7b11fd
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/irq/tests.py
@@ -0,0 +1,28 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Path, DataPath, Metric
+from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class IRQ(TestSuite):
+  # IRQ max runtime and count over 1ms
+  def test_irq_runtime_metric(self):
+    return DiffTestBlueprint(
+        trace=Path('irq_runtime_metric.textproto'),
+        query=Metric('android_irq_runtime'),
+        out=Path('irq_runtime_metric.out'))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/memory/android_ion.py b/test/trace_processor/diff_tests/metrics/memory/android_ion.py
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_ion.py
rename to test/trace_processor/diff_tests/metrics/memory/android_ion.py
diff --git a/test/trace_processor/diff_tests/memory/android_lmk_reason.out b/test/trace_processor/diff_tests/metrics/memory/android_lmk_reason.out
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_lmk_reason.out
rename to test/trace_processor/diff_tests/metrics/memory/android_lmk_reason.out
diff --git a/test/trace_processor/diff_tests/memory/android_mem_by_priority.out b/test/trace_processor/diff_tests/metrics/memory/android_mem_by_priority.out
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_mem_by_priority.out
rename to test/trace_processor/diff_tests/metrics/memory/android_mem_by_priority.out
diff --git a/test/trace_processor/diff_tests/memory/android_mem_by_priority.py b/test/trace_processor/diff_tests/metrics/memory/android_mem_by_priority.py
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_mem_by_priority.py
rename to test/trace_processor/diff_tests/metrics/memory/android_mem_by_priority.py
diff --git a/test/trace_processor/diff_tests/memory/android_mem_counters.out b/test/trace_processor/diff_tests/metrics/memory/android_mem_counters.out
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_mem_counters.out
rename to test/trace_processor/diff_tests/metrics/memory/android_mem_counters.out
diff --git a/test/trace_processor/diff_tests/memory/android_mem_delta.py b/test/trace_processor/diff_tests/metrics/memory/android_mem_delta.py
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_mem_delta.py
rename to test/trace_processor/diff_tests/metrics/memory/android_mem_delta.py
diff --git a/test/trace_processor/diff_tests/memory/android_systrace_lmk.py b/test/trace_processor/diff_tests/metrics/memory/android_systrace_lmk.py
similarity index 100%
rename from test/trace_processor/diff_tests/memory/android_systrace_lmk.py
rename to test/trace_processor/diff_tests/metrics/memory/android_systrace_lmk.py
diff --git a/test/trace_processor/diff_tests/memory/tests.py b/test/trace_processor/diff_tests/metrics/memory/tests.py
similarity index 78%
rename from test/trace_processor/diff_tests/memory/tests.py
rename to test/trace_processor/diff_tests/metrics/memory/tests.py
index aca4edf..7ea5fda 100644
--- a/test/trace_processor/diff_tests/memory/tests.py
+++ b/test/trace_processor/diff_tests/metrics/memory/tests.py
@@ -19,7 +19,7 @@
 from python.generators.diff_tests.testing import TestSuite
 
 
-class Memory(TestSuite):
+class MemoryMetrics(TestSuite):
   # Contains test for Android memory metrics. ION metric
   def test_android_ion(self):
     return DiffTestBlueprint(
@@ -132,48 +132,6 @@
         }
         """))
 
-  def test_android_dma_buffer_tracks(self):
-    return DiffTestBlueprint(
-        trace=TextProto(r"""
-        packet {
-          ftrace_events {
-            cpu: 0
-            event {
-              timestamp: 100
-              pid: 1
-              dma_heap_stat {
-                inode: 123
-                len: 1024
-                total_allocated: 2048
-              }
-            }
-          }
-        }
-        packet {
-          ftrace_events {
-            cpu: 0
-            event {
-              timestamp: 200
-              pid: 1
-              dma_heap_stat {
-                inode: 123
-                len: -1024
-                total_allocated: 1024
-              }
-            }
-          }
-        }
-        """),
-        query="""
-        SELECT track.name, slice.ts, slice.dur, slice.name
-        FROM slice JOIN track ON slice.track_id = track.id
-        WHERE track.name = 'mem.dma_buffer';
-        """,
-        out=Csv("""
-        "name","ts","dur","name"
-        "mem.dma_buffer",100,100,"1 kB"
-        """))
-
   # fastrpc metric
   def test_android_fastrpc_dma_stat(self):
     return DiffTestBlueprint(
@@ -220,55 +178,94 @@
         }
         """))
 
-  # shrink slab
-  def test_shrink_slab(self):
+  def test_android_mem_counters(self):
+    return DiffTestBlueprint(
+        trace=DataPath('memory_counters.pb'),
+        query=Metric('android_mem'),
+        out=Path('android_mem_counters.out'))
+
+  def test_trace_metadata(self):
+    return DiffTestBlueprint(
+        trace=DataPath('memory_counters.pb'),
+        query=Metric('trace_metadata'),
+        out=Path('trace_metadata.out'))
+
+  def test_android_mem_by_priority(self):
+    return DiffTestBlueprint(
+        trace=Path('android_mem_by_priority.py'),
+        query=Metric('android_mem'),
+        out=Path('android_mem_by_priority.out'))
+
+  def test_android_mem_lmk(self):
+    return DiffTestBlueprint(
+        trace=Path('android_systrace_lmk.py'),
+        query=Metric('android_lmk'),
+        out=TextProto(r"""
+        android_lmk {
+          total_count: 1
+            by_oom_score {
+            oom_score_adj: 900
+            count: 1
+          }
+          oom_victim_count: 0
+        }
+        """))
+
+  def test_android_lmk_oom(self):
     return DiffTestBlueprint(
         trace=TextProto(r"""
         packet {
-          ftrace_events {
-            cpu: 7
-            event {
-              timestamp: 36448185787847
-              pid: 156
-              mm_shrink_slab_start {
-                cache_items: 1
-                delta: 0
-                gfp_flags: 3264
-                nr_objects_to_shrink: 0
-                shr: 18446743882920355600
-                shrink: 90
-                total_scan: 0
-                nid: 0
-                priority: 12
-              }
+          process_tree {
+            processes {
+              pid: 1000
+              ppid: 1
+              cmdline: "com.google.android.gm"
+            }
+            threads {
+              tid: 1001
+              tgid: 1000
             }
           }
         }
         packet {
           ftrace_events {
-            cpu: 7
+            cpu: 4
             event {
-              timestamp: 36448185788539
-              pid: 156
-              mm_shrink_slab_end {
-                new_scan: 0
-                retval: 0
-                shr: 18446743882920355600
-                shrink: 90
-                total_scan: 0
-                unused_scan: 0
-                nid: 0
+              timestamp: 1234
+              pid: 4321
+              mark_victim {
+                pid: 1001
               }
             }
           }
         }
         """),
-        query="""
-        SELECT ts, dur, name FROM slice WHERE name = 'mm_vmscan_shrink_slab';
-        """,
-        out=Csv("""
-        "ts","dur","name"
-        36448185787847,692,"mm_vmscan_shrink_slab"
+        query=Metric('android_lmk'),
+        out=TextProto(r"""
+        android_lmk {
+          total_count: 0
+          oom_victim_count: 1
+        }
+        """))
+
+  def test_android_mem_delta(self):
+    return DiffTestBlueprint(
+        trace=Path('android_mem_delta.py'),
+        query=Metric('android_mem'),
+        out=TextProto(r"""
+        android_mem {
+          process_metrics {
+            process_name: "com.my.pkg"
+            total_counters {
+              file_rss {
+                min: 2000.0
+                max: 10000.0
+                avg: 6666.666666666667
+                delta: 7000.0
+              }
+            }
+          }
+        }
         """))
 
   # cma alloc
@@ -321,3 +318,45 @@
         "ts","dur","name"
         74288080958099,110151652,"mm_cma_alloc"
         """))
+
+  def test_android_dma_buffer_tracks(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 100
+              pid: 1
+              dma_heap_stat {
+                inode: 123
+                len: 1024
+                total_allocated: 2048
+              }
+            }
+          }
+        }
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 200
+              pid: 1
+              dma_heap_stat {
+                inode: 123
+                len: -1024
+                total_allocated: 1024
+              }
+            }
+          }
+        }
+        """),
+        query="""
+        SELECT track.name, slice.ts, slice.dur, slice.name
+        FROM slice JOIN track ON slice.track_id = track.id
+        WHERE track.name = 'mem.dma_buffer';
+        """,
+        out=Csv("""
+        "name","ts","dur","name"
+        "mem.dma_buffer",100,100,"1 kB"
+        """))
diff --git a/test/trace_processor/diff_tests/memory/trace_metadata.out b/test/trace_processor/diff_tests/metrics/memory/trace_metadata.out
similarity index 100%
rename from test/trace_processor/diff_tests/memory/trace_metadata.out
rename to test/trace_processor/diff_tests/metrics/memory/trace_metadata.out
diff --git a/test/trace_processor/diff_tests/network/netperf_metric.out b/test/trace_processor/diff_tests/metrics/network/netperf_metric.out
similarity index 100%
rename from test/trace_processor/diff_tests/network/netperf_metric.out
rename to test/trace_processor/diff_tests/metrics/network/netperf_metric.out
diff --git a/test/trace_processor/diff_tests/network/netperf_metric.textproto b/test/trace_processor/diff_tests/metrics/network/netperf_metric.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/network/netperf_metric.textproto
rename to test/trace_processor/diff_tests/metrics/network/netperf_metric.textproto
diff --git a/test/trace_processor/diff_tests/metrics/network/tests.py b/test/trace_processor/diff_tests/metrics/network/tests.py
new file mode 100644
index 0000000..d59879f
--- /dev/null
+++ b/test/trace_processor/diff_tests/metrics/network/tests.py
@@ -0,0 +1,27 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Path, Metric
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class NetworkMetrics(TestSuite):
+
+  def test_netperf_metric(self):
+    return DiffTestBlueprint(
+        trace=Path('netperf_metric.textproto'),
+        query=Metric('android_netperf'),
+        out=Path('netperf_metric.out'))
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph.textproto b/test/trace_processor/diff_tests/metrics/profiling/heap_graph.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph.textproto
rename to test/trace_processor/diff_tests/metrics/profiling/heap_graph.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_closest_proc.textproto b/test/trace_processor/diff_tests/metrics/profiling/heap_graph_closest_proc.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_closest_proc.textproto
rename to test/trace_processor/diff_tests/metrics/profiling/heap_graph_closest_proc.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_no_symbols.textproto b/test/trace_processor/diff_tests/metrics/profiling/heap_profile_no_symbols.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_no_symbols.textproto
rename to test/trace_processor/diff_tests/metrics/profiling/heap_profile_no_symbols.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_stats_closest_proc.out b/test/trace_processor/diff_tests/metrics/profiling/heap_stats_closest_proc.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_stats_closest_proc.out
rename to test/trace_processor/diff_tests/metrics/profiling/heap_stats_closest_proc.out
diff --git a/test/trace_processor/diff_tests/profiling/java_heap_histogram.out b/test/trace_processor/diff_tests/metrics/profiling/java_heap_histogram.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/java_heap_histogram.out
rename to test/trace_processor/diff_tests/metrics/profiling/java_heap_histogram.out
diff --git a/test/trace_processor/diff_tests/profiling/simpleperf_event.out b/test/trace_processor/diff_tests/metrics/profiling/simpleperf_event.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/simpleperf_event.out
rename to test/trace_processor/diff_tests/metrics/profiling/simpleperf_event.out
diff --git a/test/trace_processor/diff_tests/profiling/simpleperf_event.py b/test/trace_processor/diff_tests/metrics/profiling/simpleperf_event.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/simpleperf_event.py
rename to test/trace_processor/diff_tests/metrics/profiling/simpleperf_event.py
diff --git a/test/trace_processor/diff_tests/profiling/tests_metrics.py b/test/trace_processor/diff_tests/metrics/profiling/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/tests_metrics.py
rename to test/trace_processor/diff_tests/metrics/profiling/tests.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup.out b/test/trace_processor/diff_tests/metrics/startup/android_startup.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup.py b/test/trace_processor/diff_tests/metrics/startup/android_startup.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_attribution.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_attribution.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_attribution.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
similarity index 98%
rename from test/trace_processor/diff_tests/startup/android_startup_attribution.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
index 59301e0..a48edcc 100644
--- a/test/trace_processor/diff_tests/startup/android_startup_attribution.py
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution.py
@@ -78,7 +78,7 @@
 
 trace.add_atrace_begin(
     ts=170, pid=APP_PID, tid=APP_TID, buf='OpenDexFilesFromOat(something else)')
-trace.add_atrace_end(ts=5*10**8, pid=APP_PID, tid=APP_TID)
+trace.add_atrace_end(ts=5 * 10**8, pid=APP_PID, tid=APP_TID)
 
 # OpenDex slice outside the startup.
 trace.add_atrace_begin(
diff --git a/test/trace_processor/diff_tests/startup/android_startup_attribution_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_attribution_slow.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_attribution_slow.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_attribution_slow.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_attribution_slow.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_battery.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_battery.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_battery.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_breakdown.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_breakdown.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_breakdown.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_breakdown.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_breakdown_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_breakdown_slow.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_breakdown_slow.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.py
similarity index 97%
rename from test/trace_processor/diff_tests/startup/android_startup_breakdown_slow.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.py
index f4ea962..b8f473f 100644
--- a/test/trace_processor/diff_tests/startup/android_startup_breakdown_slow.py
+++ b/test/trace_processor/diff_tests/metrics/startup/android_startup_breakdown_slow.py
@@ -29,7 +29,10 @@
 trace.add_process(3, 1, 'com.google.android.calendar', uid=10001)
 
 trace.add_package_list(
-    ts=to_s(100), name='com.google.android.calendar', uid=10001, version_code=123)
+    ts=to_s(100),
+    name='com.google.android.calendar',
+    uid=10001,
+    version_code=123)
 
 trace.add_ftrace_packet(cpu=0)
 
diff --git a/test/trace_processor/diff_tests/startup/android_startup_broadcast.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_broadcast.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_broadcast.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_broadcast.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_broadcast_multiple.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_broadcast_multiple.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_broadcast_multiple.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_broadcast_multiple.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_broadcast_multiple.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_cpu.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_cpu.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_cpu.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_cpu.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_cpu.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_cpu.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_cpu.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_cpu.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat_slow.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat_slow.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_installd_dex2oat_slow.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_installd_dex2oat_slow.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_lock_contention.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_lock_contention.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_lock_contention.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_lock_contention.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_lock_contention_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_lock_contention_slow.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_lock_contention_slow.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_lock_contention_slow.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_lock_contention_slow.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_minsdk33.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_minsdk33.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_minsdk33.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_minsdk33.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_minsdk33.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_powrails.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_powrails.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_powrails.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_powrails.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_powrails.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_powrails.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_powrails.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_powrails.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_process_track.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_process_track.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_process_track.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_process_track.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_process_track.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_slow.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_slow.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_slow.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_slow.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_slow.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_slow.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_slow.py
diff --git a/test/trace_processor/diff_tests/startup/android_startup_unlock.out b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_unlock.out
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.out
diff --git a/test/trace_processor/diff_tests/startup/android_startup_unlock.py b/test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/android_startup_unlock.py
rename to test/trace_processor/diff_tests/metrics/startup/android_startup_unlock.py
diff --git a/test/trace_processor/diff_tests/startup/tests.py b/test/trace_processor/diff_tests/metrics/startup/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/tests.py
rename to test/trace_processor/diff_tests/metrics/startup/tests.py
diff --git a/test/trace_processor/diff_tests/startup/tests_broadcasts.py b/test/trace_processor/diff_tests/metrics/startup/tests_broadcasts.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/tests_broadcasts.py
rename to test/trace_processor/diff_tests/metrics/startup/tests_broadcasts.py
diff --git a/test/trace_processor/diff_tests/startup/tests_lock_contention.py b/test/trace_processor/diff_tests/metrics/startup/tests_lock_contention.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/tests_lock_contention.py
rename to test/trace_processor/diff_tests/metrics/startup/tests_lock_contention.py
diff --git a/test/trace_processor/diff_tests/startup/tests_metrics.py b/test/trace_processor/diff_tests/metrics/startup/tests_metrics.py
similarity index 100%
rename from test/trace_processor/diff_tests/startup/tests_metrics.py
rename to test/trace_processor/diff_tests/metrics/startup/tests_metrics.py
diff --git a/test/trace_processor/diff_tests/webview/tests.py b/test/trace_processor/diff_tests/metrics/webview/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/webview/tests.py
rename to test/trace_processor/diff_tests/metrics/webview/tests.py
diff --git a/test/trace_processor/diff_tests/android_fs/tests.py b/test/trace_processor/diff_tests/parser/android_fs/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/android_fs/tests.py
rename to test/trace_processor/diff_tests/parser/android_fs/tests.py
diff --git a/test/trace_processor/diff_tests/atrace/android_b2b_async_begin.textproto b/test/trace_processor/diff_tests/parser/atrace/android_b2b_async_begin.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/android_b2b_async_begin.textproto
rename to test/trace_processor/diff_tests/parser/atrace/android_b2b_async_begin.textproto
diff --git a/test/trace_processor/diff_tests/atrace/async_track_atrace.py b/test/trace_processor/diff_tests/parser/atrace/async_track_atrace.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/async_track_atrace.py
rename to test/trace_processor/diff_tests/parser/atrace/async_track_atrace.py
diff --git a/test/trace_processor/diff_tests/atrace/bad_print.systrace b/test/trace_processor/diff_tests/parser/atrace/bad_print.systrace
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/bad_print.systrace
rename to test/trace_processor/diff_tests/parser/atrace/bad_print.systrace
diff --git a/test/trace_processor/diff_tests/atrace/bad_print.textproto b/test/trace_processor/diff_tests/parser/atrace/bad_print.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/bad_print.textproto
rename to test/trace_processor/diff_tests/parser/atrace/bad_print.textproto
diff --git a/test/trace_processor/diff_tests/atrace/instant_async_atrace.py b/test/trace_processor/diff_tests/parser/atrace/instant_async_atrace.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/instant_async_atrace.py
rename to test/trace_processor/diff_tests/parser/atrace/instant_async_atrace.py
diff --git a/test/trace_processor/diff_tests/atrace/instant_atrace.py b/test/trace_processor/diff_tests/parser/atrace/instant_atrace.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/instant_atrace.py
rename to test/trace_processor/diff_tests/parser/atrace/instant_atrace.py
diff --git a/test/trace_processor/diff_tests/atrace/process_track_slices_android_async_slice.out b/test/trace_processor/diff_tests/parser/atrace/process_track_slices_android_async_slice.out
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/process_track_slices_android_async_slice.out
rename to test/trace_processor/diff_tests/parser/atrace/process_track_slices_android_async_slice.out
diff --git a/test/trace_processor/diff_tests/atrace/sys_write_and_atrace.py b/test/trace_processor/diff_tests/parser/atrace/sys_write_and_atrace.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/sys_write_and_atrace.py
rename to test/trace_processor/diff_tests/parser/atrace/sys_write_and_atrace.py
diff --git a/test/trace_processor/diff_tests/atrace/tests.py b/test/trace_processor/diff_tests/parser/atrace/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/tests.py
rename to test/trace_processor/diff_tests/parser/atrace/tests.py
diff --git a/test/trace_processor/diff_tests/atrace/tests_error_handling.py b/test/trace_processor/diff_tests/parser/atrace/tests_error_handling.py
similarity index 100%
rename from test/trace_processor/diff_tests/atrace/tests_error_handling.py
rename to test/trace_processor/diff_tests/parser/atrace/tests_error_handling.py
diff --git a/test/trace_processor/diff_tests/cros/cros_ec_sensorhub_data.out b/test/trace_processor/diff_tests/parser/cros/cros_ec_sensorhub_data.out
similarity index 100%
rename from test/trace_processor/diff_tests/cros/cros_ec_sensorhub_data.out
rename to test/trace_processor/diff_tests/parser/cros/cros_ec_sensorhub_data.out
diff --git a/test/trace_processor/diff_tests/cros/tests.py b/test/trace_processor/diff_tests/parser/cros/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/cros/tests.py
rename to test/trace_processor/diff_tests/parser/cros/tests.py
diff --git a/test/trace_processor/diff_tests/fs/f2fs_iostat.out b/test/trace_processor/diff_tests/parser/fs/f2fs_iostat.out
similarity index 100%
rename from test/trace_processor/diff_tests/fs/f2fs_iostat.out
rename to test/trace_processor/diff_tests/parser/fs/f2fs_iostat.out
diff --git a/test/trace_processor/diff_tests/fs/f2fs_iostat.textproto b/test/trace_processor/diff_tests/parser/fs/f2fs_iostat.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/fs/f2fs_iostat.textproto
rename to test/trace_processor/diff_tests/parser/fs/f2fs_iostat.textproto
diff --git a/test/trace_processor/diff_tests/fs/f2fs_iostat_latency.out b/test/trace_processor/diff_tests/parser/fs/f2fs_iostat_latency.out
similarity index 100%
rename from test/trace_processor/diff_tests/fs/f2fs_iostat_latency.out
rename to test/trace_processor/diff_tests/parser/fs/f2fs_iostat_latency.out
diff --git a/test/trace_processor/diff_tests/fs/f2fs_iostat_latency.textproto b/test/trace_processor/diff_tests/parser/fs/f2fs_iostat_latency.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/fs/f2fs_iostat_latency.textproto
rename to test/trace_processor/diff_tests/parser/fs/f2fs_iostat_latency.textproto
diff --git a/test/trace_processor/diff_tests/fs/tests.py b/test/trace_processor/diff_tests/parser/fs/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/fs/tests.py
rename to test/trace_processor/diff_tests/parser/fs/tests.py
diff --git a/test/trace_processor/diff_tests/fuchsia/fuchsia_workstation_smoke_slices.out b/test/trace_processor/diff_tests/parser/fuchsia/fuchsia_workstation_smoke_slices.out
similarity index 100%
rename from test/trace_processor/diff_tests/fuchsia/fuchsia_workstation_smoke_slices.out
rename to test/trace_processor/diff_tests/parser/fuchsia/fuchsia_workstation_smoke_slices.out
diff --git a/test/trace_processor/diff_tests/fuchsia/tests.py b/test/trace_processor/diff_tests/parser/fuchsia/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/fuchsia/tests.py
rename to test/trace_processor/diff_tests/parser/fuchsia/tests.py
diff --git a/test/trace_processor/diff_tests/parser/memory/tests.py b/test/trace_processor/diff_tests/parser/memory/tests.py
new file mode 100644
index 0000000..f7bdf1c
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/memory/tests.py
@@ -0,0 +1,113 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class MemoryParser(TestSuite):
+  # cma alloc
+  def test_cma(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          system_info {
+            utsname {
+              sysname: "Linux"
+              release: "5.10.0"
+            }
+          }
+        }
+        packet {
+          ftrace_events {
+            cpu: 4
+            event {
+              timestamp: 74288080958099
+              pid: 537
+              cma_alloc_start {
+                align: 4
+                count: 6592
+                name: "farawimg"
+              }
+            }
+            event {
+              timestamp: 74288191109751
+              pid: 537
+              cma_alloc_info {
+                align: 4
+                count: 6592
+                err_iso: 0
+                err_mig: 0
+                err_test: 0
+                name: "farawimg"
+                nr_mapped: 832596
+                nr_migrated: 6365
+                nr_reclaimed: 7
+                pfn: 10365824
+              }
+            }
+          }
+        }
+        """),
+        query="""
+        SELECT ts, dur, name FROM slice WHERE name = 'mm_cma_alloc';
+        """,
+        out=Csv("""
+        "ts","dur","name"
+        74288080958099,110151652,"mm_cma_alloc"
+        """))
+
+  def test_android_dma_buffer_tracks(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 100
+              pid: 1
+              dma_heap_stat {
+                inode: 123
+                len: 1024
+                total_allocated: 2048
+              }
+            }
+          }
+        }
+        packet {
+          ftrace_events {
+            cpu: 0
+            event {
+              timestamp: 200
+              pid: 1
+              dma_heap_stat {
+                inode: 123
+                len: -1024
+                total_allocated: 1024
+              }
+            }
+          }
+        }
+        """),
+        query="""
+        SELECT track.name, slice.ts, slice.dur, slice.name
+        FROM slice JOIN track ON slice.track_id = track.id
+        WHERE track.name = 'mem.dma_buffer';
+        """,
+        out=Csv("""
+        "name","ts","dur","name"
+        "mem.dma_buffer",100,100,"1 kB"
+        """))
diff --git a/test/trace_processor/diff_tests/network/inet_sock_set_state.textproto b/test/trace_processor/diff_tests/parser/network/inet_sock_set_state.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/network/inet_sock_set_state.textproto
rename to test/trace_processor/diff_tests/parser/network/inet_sock_set_state.textproto
diff --git a/test/trace_processor/diff_tests/network/napi_gro_receive.textproto b/test/trace_processor/diff_tests/parser/network/napi_gro_receive.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/network/napi_gro_receive.textproto
rename to test/trace_processor/diff_tests/parser/network/napi_gro_receive.textproto
diff --git a/test/trace_processor/diff_tests/network/net_dev_xmit.textproto b/test/trace_processor/diff_tests/parser/network/net_dev_xmit.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/network/net_dev_xmit.textproto
rename to test/trace_processor/diff_tests/parser/network/net_dev_xmit.textproto
diff --git a/test/trace_processor/diff_tests/network/netif_receive_skb.textproto b/test/trace_processor/diff_tests/parser/network/netif_receive_skb.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/network/netif_receive_skb.textproto
rename to test/trace_processor/diff_tests/parser/network/netif_receive_skb.textproto
diff --git a/test/trace_processor/diff_tests/network/tests.py b/test/trace_processor/diff_tests/parser/network/tests.py
similarity index 94%
rename from test/trace_processor/diff_tests/network/tests.py
rename to test/trace_processor/diff_tests/parser/network/tests.py
index a8bdd72..76f2446 100644
--- a/test/trace_processor/diff_tests/network/tests.py
+++ b/test/trace_processor/diff_tests/parser/network/tests.py
@@ -13,13 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
+from python.generators.diff_tests.testing import Path, Metric
+from python.generators.diff_tests.testing import Csv, TextProto
 from python.generators.diff_tests.testing import DiffTestBlueprint
 from python.generators.diff_tests.testing import TestSuite
 
 
-class Network(TestSuite):
+class NetworkParser(TestSuite):
   # Network performance
   def test_netif_receive_skb(self):
     return DiffTestBlueprint(
@@ -74,12 +74,6 @@
         12000,"wlan0",4,1300
         """))
 
-  def test_netperf_metric(self):
-    return DiffTestBlueprint(
-        trace=Path('netperf_metric.textproto'),
-        query=Metric('android_netperf'),
-        out=Path('netperf_metric.out'))
-
   def test_inet_sock_set_state(self):
     return DiffTestBlueprint(
         trace=Path('inet_sock_set_state.textproto'),
diff --git a/test/trace_processor/diff_tests/parsing/all_atoms_test.sql b/test/trace_processor/diff_tests/parser/parsing/all_atoms_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/all_atoms_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/all_atoms_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/android_binder.py b/test/trace_processor/diff_tests/parser/parsing/android_binder.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_binder.py
rename to test/trace_processor/diff_tests/parser/parsing/android_binder.py
diff --git a/test/trace_processor/diff_tests/parsing/android_log_counts_test.sql b/test/trace_processor/diff_tests/parser/parsing/android_log_counts_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_log_counts_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/android_log_counts_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/android_log_msgs.out b/test/trace_processor/diff_tests/parser/parsing/android_log_msgs.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_log_msgs.out
rename to test/trace_processor/diff_tests/parser/parsing/android_log_msgs.out
diff --git a/test/trace_processor/diff_tests/parsing/android_log_msgs_test.sql b/test/trace_processor/diff_tests/parser/parsing/android_log_msgs_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_log_msgs_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/android_log_msgs_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/android_multiuser_switch.textproto b/test/trace_processor/diff_tests/parser/parsing/android_multiuser_switch.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_multiuser_switch.textproto
rename to test/trace_processor/diff_tests/parser/parsing/android_multiuser_switch.textproto
diff --git a/test/trace_processor/diff_tests/parsing/android_package_list.py b/test/trace_processor/diff_tests/parser/parsing/android_package_list.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_package_list.py
rename to test/trace_processor/diff_tests/parser/parsing/android_package_list.py
diff --git a/test/trace_processor/diff_tests/parsing/android_sched_and_ps_stats.out b/test/trace_processor/diff_tests/parser/parsing/android_sched_and_ps_stats.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/android_sched_and_ps_stats.out
rename to test/trace_processor/diff_tests/parser/parsing/android_sched_and_ps_stats.out
diff --git a/test/trace_processor/diff_tests/parsing/args_string_filter_null_test.sql b/test/trace_processor/diff_tests/parser/parsing/args_string_filter_null_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/args_string_filter_null_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/args_string_filter_null_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/b120487929_test.sql b/test/trace_processor/diff_tests/parser/parsing/b120487929_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/b120487929_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/b120487929_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/cgroup_attach_task_post_s_print_systrace.out b/test/trace_processor/diff_tests/parser/parsing/cgroup_attach_task_post_s_print_systrace.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/cgroup_attach_task_post_s_print_systrace.out
rename to test/trace_processor/diff_tests/parser/parsing/cgroup_attach_task_post_s_print_systrace.out
diff --git a/test/trace_processor/diff_tests/parsing/cgroup_attach_task_pre_s_print_systrace.out b/test/trace_processor/diff_tests/parser/parsing/cgroup_attach_task_pre_s_print_systrace.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/cgroup_attach_task_pre_s_print_systrace.out
rename to test/trace_processor/diff_tests/parser/parsing/cgroup_attach_task_pre_s_print_systrace.out
diff --git a/test/trace_processor/diff_tests/parsing/chrome_metadata.out b/test/trace_processor/diff_tests/parser/parsing/chrome_metadata.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/chrome_metadata.out
rename to test/trace_processor/diff_tests/parser/parsing/chrome_metadata.out
diff --git a/test/trace_processor/diff_tests/parsing/cpu_counters_b120487929.out b/test/trace_processor/diff_tests/parser/parsing/cpu_counters_b120487929.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/cpu_counters_b120487929.out
rename to test/trace_processor/diff_tests/parser/parsing/cpu_counters_b120487929.out
diff --git a/test/trace_processor/diff_tests/parsing/cpu_freq.out b/test/trace_processor/diff_tests/parser/parsing/cpu_freq.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/cpu_freq.out
rename to test/trace_processor/diff_tests/parser/parsing/cpu_freq.out
diff --git a/test/trace_processor/diff_tests/parsing/cpu_info.textproto b/test/trace_processor/diff_tests/parser/parsing/cpu_info.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/cpu_info.textproto
rename to test/trace_processor/diff_tests/parser/parsing/cpu_info.textproto
diff --git a/test/trace_processor/diff_tests/parsing/flow_events_json_v1.json b/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/flow_events_json_v1.json
rename to test/trace_processor/diff_tests/parser/parsing/flow_events_json_v1.json
diff --git a/test/trace_processor/diff_tests/parsing/flow_events_json_v2.json b/test/trace_processor/diff_tests/parser/parsing/flow_events_json_v2.json
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/flow_events_json_v2.json
rename to test/trace_processor/diff_tests/parser/parsing/flow_events_json_v2.json
diff --git a/test/trace_processor/diff_tests/parsing/ftrace_with_tracing_start.py b/test/trace_processor/diff_tests/parser/parsing/ftrace_with_tracing_start.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/ftrace_with_tracing_start.py
rename to test/trace_processor/diff_tests/parser/parsing/ftrace_with_tracing_start.py
diff --git a/test/trace_processor/diff_tests/parsing/funcgraph_trace.textproto b/test/trace_processor/diff_tests/parser/parsing/funcgraph_trace.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/funcgraph_trace.textproto
rename to test/trace_processor/diff_tests/parser/parsing/funcgraph_trace.textproto
diff --git a/test/trace_processor/diff_tests/parsing/kernel_dpu_tmw_counter_thread_counter_and_track.out b/test/trace_processor/diff_tests/parser/parsing/kernel_dpu_tmw_counter_thread_counter_and_track.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/kernel_dpu_tmw_counter_thread_counter_and_track.out
rename to test/trace_processor/diff_tests/parser/parsing/kernel_dpu_tmw_counter_thread_counter_and_track.out
diff --git a/test/trace_processor/diff_tests/parsing/kernel_tmw_counter.textproto b/test/trace_processor/diff_tests/parser/parsing/kernel_tmw_counter.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/kernel_tmw_counter.textproto
rename to test/trace_processor/diff_tests/parser/parsing/kernel_tmw_counter.textproto
diff --git a/test/trace_processor/diff_tests/parsing/kernel_tmw_counter_thread_counter_and_track.out b/test/trace_processor/diff_tests/parser/parsing/kernel_tmw_counter_thread_counter_and_track.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/kernel_tmw_counter_thread_counter_and_track.out
rename to test/trace_processor/diff_tests/parser/parsing/kernel_tmw_counter_thread_counter_and_track.out
diff --git a/test/trace_processor/diff_tests/parsing/mm_event.out b/test/trace_processor/diff_tests/parser/parsing/mm_event.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/mm_event.out
rename to test/trace_processor/diff_tests/parser/parsing/mm_event.out
diff --git a/test/trace_processor/diff_tests/parsing/oom_query_test.sql b/test/trace_processor/diff_tests/parser/parsing/oom_query_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/oom_query_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/oom_query_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/otheruuids.textproto b/test/trace_processor/diff_tests/parser/parsing/otheruuids.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/otheruuids.textproto
rename to test/trace_processor/diff_tests/parser/parsing/otheruuids.textproto
diff --git a/test/trace_processor/diff_tests/parsing/print_systrace_lmk_userspace.out b/test/trace_processor/diff_tests/parser/parsing/print_systrace_lmk_userspace.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/print_systrace_lmk_userspace.out
rename to test/trace_processor/diff_tests/parser/parsing/print_systrace_lmk_userspace.out
diff --git a/test/trace_processor/diff_tests/parsing/print_systrace_unsigned.out b/test/trace_processor/diff_tests/parser/parsing/print_systrace_unsigned.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/print_systrace_unsigned.out
rename to test/trace_processor/diff_tests/parser/parsing/print_systrace_unsigned.out
diff --git a/test/trace_processor/diff_tests/parsing/print_systrace_unsigned.py b/test/trace_processor/diff_tests/parser/parsing/print_systrace_unsigned.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/print_systrace_unsigned.py
rename to test/trace_processor/diff_tests/parser/parsing/print_systrace_unsigned.py
diff --git a/test/trace_processor/diff_tests/parsing/process_stats_poll_oom_score.out b/test/trace_processor/diff_tests/parser/parsing/process_stats_poll_oom_score.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/process_stats_poll_oom_score.out
rename to test/trace_processor/diff_tests/parser/parsing/process_stats_poll_oom_score.out
diff --git a/test/trace_processor/diff_tests/parsing/rss_stat_after_free.py b/test/trace_processor/diff_tests/parser/parsing/rss_stat_after_free.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/rss_stat_after_free.py
rename to test/trace_processor/diff_tests/parser/parsing/rss_stat_after_free.py
diff --git a/test/trace_processor/diff_tests/parsing/rss_stat_legacy.py b/test/trace_processor/diff_tests/parser/parsing/rss_stat_legacy.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/rss_stat_legacy.py
rename to test/trace_processor/diff_tests/parser/parsing/rss_stat_legacy.py
diff --git a/test/trace_processor/diff_tests/parsing/rss_stat_mm_id.py b/test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/rss_stat_mm_id.py
rename to test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id.py
diff --git a/test/trace_processor/diff_tests/parsing/rss_stat_mm_id_clone.py b/test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id_clone.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/rss_stat_mm_id_clone.py
rename to test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id_clone.py
diff --git a/test/trace_processor/diff_tests/parsing/rss_stat_mm_id_reuse.py b/test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id_reuse.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/rss_stat_mm_id_reuse.py
rename to test/trace_processor/diff_tests/parser/parsing/rss_stat_mm_id_reuse.py
diff --git a/test/trace_processor/diff_tests/parsing/sched_blocked_proto.py b/test/trace_processor/diff_tests/parser/parsing/sched_blocked_proto.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_blocked_proto.py
rename to test/trace_processor/diff_tests/parser/parsing/sched_blocked_proto.py
diff --git a/test/trace_processor/diff_tests/parsing/sched_blocked_reason_symbolized.textproto b/test/trace_processor/diff_tests/parser/parsing/sched_blocked_reason_symbolized.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_blocked_reason_symbolized.textproto
rename to test/trace_processor/diff_tests/parser/parsing/sched_blocked_reason_symbolized.textproto
diff --git a/test/trace_processor/diff_tests/parsing/sched_blocked_reason_symbolized_to_systrace.out b/test/trace_processor/diff_tests/parser/parsing/sched_blocked_reason_symbolized_to_systrace.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_blocked_reason_symbolized_to_systrace.out
rename to test/trace_processor/diff_tests/parser/parsing/sched_blocked_reason_symbolized_to_systrace.out
diff --git a/test/trace_processor/diff_tests/parsing/sched_blocked_systrace.systrace b/test/trace_processor/diff_tests/parser/parsing/sched_blocked_systrace.systrace
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_blocked_systrace.systrace
rename to test/trace_processor/diff_tests/parser/parsing/sched_blocked_systrace.systrace
diff --git a/test/trace_processor/diff_tests/parsing/sched_slices_sched_switch_compact.out b/test/trace_processor/diff_tests/parser/parsing/sched_slices_sched_switch_compact.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_slices_sched_switch_compact.out
rename to test/trace_processor/diff_tests/parser/parsing/sched_slices_sched_switch_compact.out
diff --git a/test/trace_processor/diff_tests/parsing/sched_slices_sched_switch_original.out b/test/trace_processor/diff_tests/parser/parsing/sched_slices_sched_switch_original.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_slices_sched_switch_original.out
rename to test/trace_processor/diff_tests/parser/parsing/sched_slices_sched_switch_original.out
diff --git a/test/trace_processor/diff_tests/parsing/sched_waking_instants_compact_sched.out b/test/trace_processor/diff_tests/parser/parsing/sched_waking_instants_compact_sched.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_waking_instants_compact_sched.out
rename to test/trace_processor/diff_tests/parser/parsing/sched_waking_instants_compact_sched.out
diff --git a/test/trace_processor/diff_tests/parsing/sched_waking_raw_compact_sched.out b/test/trace_processor/diff_tests/parser/parsing/sched_waking_raw_compact_sched.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_waking_raw_compact_sched.out
rename to test/trace_processor/diff_tests/parser/parsing/sched_waking_raw_compact_sched.out
diff --git a/test/trace_processor/diff_tests/parsing/sched_waking_raw_test.sql b/test/trace_processor/diff_tests/parser/parsing/sched_waking_raw_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/sched_waking_raw_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/sched_waking_raw_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/statsd_atoms_all_atoms.out b/test/trace_processor/diff_tests/parser/parsing/statsd_atoms_all_atoms.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/statsd_atoms_all_atoms.out
rename to test/trace_processor/diff_tests/parser/parsing/statsd_atoms_all_atoms.out
diff --git a/test/trace_processor/diff_tests/parsing/synth_oom.py b/test/trace_processor/diff_tests/parser/parsing/synth_oom.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/synth_oom.py
rename to test/trace_processor/diff_tests/parser/parsing/synth_oom.py
diff --git a/test/trace_processor/diff_tests/parsing/synth_oom_oom_query.out b/test/trace_processor/diff_tests/parser/parsing/synth_oom_oom_query.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/synth_oom_oom_query.out
rename to test/trace_processor/diff_tests/parser/parsing/synth_oom_oom_query.out
diff --git a/test/trace_processor/diff_tests/parsing/syscall.py b/test/trace_processor/diff_tests/parser/parsing/syscall.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/syscall.py
rename to test/trace_processor/diff_tests/parser/parsing/syscall.py
diff --git a/test/trace_processor/diff_tests/parsing/systrace_html.out b/test/trace_processor/diff_tests/parser/parsing/systrace_html.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/systrace_html.out
rename to test/trace_processor/diff_tests/parser/parsing/systrace_html.out
diff --git a/test/trace_processor/diff_tests/parsing/tests.py b/test/trace_processor/diff_tests/parser/parsing/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/tests.py
rename to test/trace_processor/diff_tests/parser/parsing/tests.py
diff --git a/test/trace_processor/diff_tests/parsing/tests_debug_annotation.py b/test/trace_processor/diff_tests/parser/parsing/tests_debug_annotation.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/tests_debug_annotation.py
rename to test/trace_processor/diff_tests/parser/parsing/tests_debug_annotation.py
diff --git a/test/trace_processor/diff_tests/parsing/tests_memory_counters.py b/test/trace_processor/diff_tests/parser/parsing/tests_memory_counters.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/tests_memory_counters.py
rename to test/trace_processor/diff_tests/parser/parsing/tests_memory_counters.py
diff --git a/test/trace_processor/diff_tests/parsing/tests_rss_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_rss_stats.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/tests_rss_stats.py
rename to test/trace_processor/diff_tests/parser/parsing/tests_rss_stats.py
diff --git a/test/trace_processor/diff_tests/parsing/thread_counter_and_track_test.sql b/test/trace_processor/diff_tests/parser/parsing/thread_counter_and_track_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/thread_counter_and_track_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/thread_counter_and_track_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/thread_time_in_state.out b/test/trace_processor/diff_tests/parser/parsing/thread_time_in_state.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/thread_time_in_state.out
rename to test/trace_processor/diff_tests/parser/parsing/thread_time_in_state.out
diff --git a/test/trace_processor/diff_tests/parsing/thread_time_in_state_event.out b/test/trace_processor/diff_tests/parser/parsing/thread_time_in_state_event.out
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/thread_time_in_state_event.out
rename to test/trace_processor/diff_tests/parser/parsing/thread_time_in_state_event.out
diff --git a/test/trace_processor/diff_tests/parsing/thread_time_in_state_event.py b/test/trace_processor/diff_tests/parser/parsing/thread_time_in_state_event.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/thread_time_in_state_event.py
rename to test/trace_processor/diff_tests/parser/parsing/thread_time_in_state_event.py
diff --git a/test/trace_processor/diff_tests/parsing/triggers_packets_test.sql b/test/trace_processor/diff_tests/parser/parsing/triggers_packets_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/triggers_packets_test.sql
rename to test/trace_processor/diff_tests/parser/parsing/triggers_packets_test.sql
diff --git a/test/trace_processor/diff_tests/parsing/very_long_sched.py b/test/trace_processor/diff_tests/parser/parsing/very_long_sched.py
similarity index 100%
rename from test/trace_processor/diff_tests/parsing/very_long_sched.py
rename to test/trace_processor/diff_tests/parser/parsing/very_long_sched.py
diff --git a/test/trace_processor/diff_tests/process_tracking/process_parent_pid_tracking_1.py b/test/trace_processor/diff_tests/parser/process_tracking/process_parent_pid_tracking_1.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/process_parent_pid_tracking_1.py
rename to test/trace_processor/diff_tests/parser/process_tracking/process_parent_pid_tracking_1.py
diff --git a/test/trace_processor/diff_tests/process_tracking/process_parent_pid_tracking_2.py b/test/trace_processor/diff_tests/parser/process_tracking/process_parent_pid_tracking_2.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/process_parent_pid_tracking_2.py
rename to test/trace_processor/diff_tests/parser/process_tracking/process_parent_pid_tracking_2.py
diff --git a/test/trace_processor/diff_tests/process_tracking/process_tracking_exec.py b/test/trace_processor/diff_tests/parser/process_tracking/process_tracking_exec.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/process_tracking_exec.py
rename to test/trace_processor/diff_tests/parser/process_tracking/process_tracking_exec.py
diff --git a/test/trace_processor/diff_tests/process_tracking/process_tracking_short_lived_1.py b/test/trace_processor/diff_tests/parser/process_tracking/process_tracking_short_lived_1.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/process_tracking_short_lived_1.py
rename to test/trace_processor/diff_tests/parser/process_tracking/process_tracking_short_lived_1.py
diff --git a/test/trace_processor/diff_tests/process_tracking/process_tracking_short_lived_2.py b/test/trace_processor/diff_tests/parser/process_tracking/process_tracking_short_lived_2.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/process_tracking_short_lived_2.py
rename to test/trace_processor/diff_tests/parser/process_tracking/process_tracking_short_lived_2.py
diff --git a/test/trace_processor/diff_tests/process_tracking/reused_thread_print.py b/test/trace_processor/diff_tests/parser/process_tracking/reused_thread_print.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/reused_thread_print.py
rename to test/trace_processor/diff_tests/parser/process_tracking/reused_thread_print.py
diff --git a/test/trace_processor/diff_tests/process_tracking/synth_process_tracking.py b/test/trace_processor/diff_tests/parser/process_tracking/synth_process_tracking.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/synth_process_tracking.py
rename to test/trace_processor/diff_tests/parser/process_tracking/synth_process_tracking.py
diff --git a/test/trace_processor/diff_tests/process_tracking/tests.py b/test/trace_processor/diff_tests/parser/process_tracking/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/tests.py
rename to test/trace_processor/diff_tests/parser/process_tracking/tests.py
diff --git a/test/trace_processor/diff_tests/process_tracking/unknown_thread_name.systrace b/test/trace_processor/diff_tests/parser/process_tracking/unknown_thread_name.systrace
similarity index 100%
rename from test/trace_processor/diff_tests/process_tracking/unknown_thread_name.systrace
rename to test/trace_processor/diff_tests/parser/process_tracking/unknown_thread_name.systrace
diff --git a/test/trace_processor/diff_tests/profiling/callstack_sampling_flamegraph.out b/test/trace_processor/diff_tests/parser/profiling/callstack_sampling_flamegraph.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/callstack_sampling_flamegraph.out
rename to test/trace_processor/diff_tests/parser/profiling/callstack_sampling_flamegraph.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph.textproto
similarity index 100%
copy from test/trace_processor/diff_tests/profiling/heap_graph.textproto
copy to test/trace_processor/diff_tests/parser/profiling/heap_graph.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_baseapk.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_baseapk.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_baseapk.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_baseapk.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_branching.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_branching.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_branching.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_branching.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_deobfuscate_pkg.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_deobfuscate_pkg.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_deobfuscate_pkg.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_deobfuscate_pkg.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_duplicate_flamegraph.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_duplicate_flamegraph.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_duplicate_flamegraph.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_duplicate_flamegraph.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_flamegraph.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_flamegraph.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_flamegraph_focused.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph_focused.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_flamegraph_focused.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph_focused.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_flamegraph_system-server-heap-graph.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph_system-server-heap-graph.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_flamegraph_system-server-heap-graph.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_flamegraph_system-server-heap-graph.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_huge_size.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_huge_size.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_huge_size.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_huge_size.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_interleaved.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_interleaved.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_interleaved_object.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved_object.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_interleaved_object.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved_object.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_interleaved_reference.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved_reference.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_interleaved_reference.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_interleaved_reference.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_legacy.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_legacy.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_legacy.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_legacy.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_native_size.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_native_size.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_native_size.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_native_size.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_object.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_object.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_object.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_object.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_reference.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_reference.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_reference.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_reference.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_superclass.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_superclass.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_superclass.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_superclass.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_two_locations.out b/test/trace_processor/diff_tests/parser/profiling/heap_graph_two_locations.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_two_locations.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_two_locations.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_graph_two_locations.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_graph_two_locations.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_graph_two_locations.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_graph_two_locations.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_data_local_tmp.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_data_local_tmp.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_data_local_tmp.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_data_local_tmp.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate_memfd.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate_memfd.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate_memfd.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate_memfd.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate_test.sql b/test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_deobfuscate_test.sql
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_deobfuscate_test.sql
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_dump_max.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_dump_max.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_dump_max.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_dump_max.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_dump_max_legacy.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_dump_max_legacy.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_dump_max_legacy.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_dump_max_legacy.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_flamegraph_system-server-native-profile.out b/test/trace_processor/diff_tests/parser/profiling/heap_profile_flamegraph_system-server-native-profile.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_flamegraph_system-server-native-profile.out
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_flamegraph_system-server-native-profile.out
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_jit.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_jit.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_jit.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_jit.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_tracker_new_stack.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_new_stack.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_tracker_new_stack.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_new_stack.textproto
diff --git a/test/trace_processor/diff_tests/profiling/heap_profile_tracker_twoheaps.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/heap_profile_tracker_twoheaps.textproto
rename to test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto
diff --git a/test/trace_processor/diff_tests/profiling/perf_sample_rvc.out b/test/trace_processor/diff_tests/parser/profiling/perf_sample_rvc.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/perf_sample_rvc.out
rename to test/trace_processor/diff_tests/parser/profiling/perf_sample_rvc.out
diff --git a/test/trace_processor/diff_tests/profiling/perf_sample_sc.out b/test/trace_processor/diff_tests/parser/profiling/perf_sample_sc.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/perf_sample_sc.out
rename to test/trace_processor/diff_tests/parser/profiling/perf_sample_sc.out
diff --git a/test/trace_processor/diff_tests/profiling/perf_sample_switch_interp.textproto b/test/trace_processor/diff_tests/parser/profiling/perf_sample_switch_interp.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/perf_sample_switch_interp.textproto
rename to test/trace_processor/diff_tests/parser/profiling/perf_sample_switch_interp.textproto
diff --git a/test/trace_processor/diff_tests/profiling/stack_profile_symbols.out b/test/trace_processor/diff_tests/parser/profiling/stack_profile_symbols.out
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/stack_profile_symbols.out
rename to test/trace_processor/diff_tests/parser/profiling/stack_profile_symbols.out
diff --git a/test/trace_processor/diff_tests/profiling/tests.py b/test/trace_processor/diff_tests/parser/profiling/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/tests.py
rename to test/trace_processor/diff_tests/parser/profiling/tests.py
diff --git a/test/trace_processor/diff_tests/profiling/tests_heap_graph.py b/test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/tests_heap_graph.py
rename to test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py
diff --git a/test/trace_processor/diff_tests/profiling/tests_heap_profiling.py b/test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/tests_heap_profiling.py
rename to test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py
diff --git a/test/trace_processor/diff_tests/profiling/tests_llvm_symbolizer.py b/test/trace_processor/diff_tests/parser/profiling/tests_llvm_symbolizer.py
similarity index 100%
rename from test/trace_processor/diff_tests/profiling/tests_llvm_symbolizer.py
rename to test/trace_processor/diff_tests/parser/profiling/tests_llvm_symbolizer.py
diff --git a/test/trace_processor/diff_tests/performance/cpu_frequency_limits.textproto b/test/trace_processor/diff_tests/parser/sched/cpu_frequency_limits.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/performance/cpu_frequency_limits.textproto
rename to test/trace_processor/diff_tests/parser/sched/cpu_frequency_limits.textproto
diff --git a/test/trace_processor/diff_tests/scheduler/sched_cpu_util_cfs.textproto b/test/trace_processor/diff_tests/parser/sched/sched_cpu_util_cfs.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/scheduler/sched_cpu_util_cfs.textproto
rename to test/trace_processor/diff_tests/parser/sched/sched_cpu_util_cfs.textproto
diff --git a/test/trace_processor/diff_tests/scheduler/sched_cpu_util_cfs_test.sql b/test/trace_processor/diff_tests/parser/sched/sched_cpu_util_cfs_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/scheduler/sched_cpu_util_cfs_test.sql
rename to test/trace_processor/diff_tests/parser/sched/sched_cpu_util_cfs_test.sql
diff --git a/test/trace_processor/diff_tests/performance/tests.py b/test/trace_processor/diff_tests/parser/sched/tests.py
similarity index 74%
rename from test/trace_processor/diff_tests/performance/tests.py
rename to test/trace_processor/diff_tests/parser/sched/tests.py
index 7358373..ea04126 100644
--- a/test/trace_processor/diff_tests/performance/tests.py
+++ b/test/trace_processor/diff_tests/parser/sched/tests.py
@@ -19,14 +19,7 @@
 from python.generators.diff_tests.testing import TestSuite
 
 
-class Performance(TestSuite):
-  # IRQ max runtime and count over 1ms
-  def test_irq_runtime_metric(self):
-    return DiffTestBlueprint(
-        trace=Path('irq_runtime_metric.textproto'),
-        query=Metric('android_irq_runtime'),
-        out=Path('irq_runtime_metric.out'))
-
+class SchedParser(TestSuite):
   # CPU frequency maximum & minimum limits change
   def test_cpu_frequency_limits(self):
     return DiffTestBlueprint(
@@ -61,9 +54,22 @@
         130000000,800000.000000,"Cpu 4 Min"
         """))
 
-  # frame_timeline_metric collects App_Deadline_Missed metrics
-  def test_frame_timeline_metric(self):
+  def test_sched_cpu_util_cfs(self):
     return DiffTestBlueprint(
-        trace=Path('frame_timeline_metric.py'),
-        query=Metric('android_frame_timeline_metric'),
-        out=Path('frame_timeline_metric.out'))
+        trace=Path('sched_cpu_util_cfs.textproto'),
+        query=Path('sched_cpu_util_cfs_test.sql'),
+        out=Csv("""
+        "name","ts","value"
+        "Cpu 6 Util",10000,1.000000
+        "Cpu 6 Cap",10000,1004.000000
+        "Cpu 6 Nr Running",10000,0.000000
+        "Cpu 7 Util",11000,1.000000
+        "Cpu 7 Cap",11000,1007.000000
+        "Cpu 7 Nr Running",11000,0.000000
+        "Cpu 4 Util",12000,43.000000
+        "Cpu 4 Cap",12000,760.000000
+        "Cpu 4 Nr Running",12000,0.000000
+        "Cpu 5 Util",13000,125.000000
+        "Cpu 5 Cap",13000,757.000000
+        "Cpu 5 Nr Running",13000,1.000000
+        """))
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/smoke/proxy_power.out b/test/trace_processor/diff_tests/parser/smoke/proxy_power.out
similarity index 100%
rename from test/trace_processor/diff_tests/smoke/proxy_power.out
rename to test/trace_processor/diff_tests/parser/smoke/proxy_power.out
diff --git a/test/trace_processor/diff_tests/smoke/tests.py b/test/trace_processor/diff_tests/parser/smoke/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/smoke/tests.py
rename to test/trace_processor/diff_tests/parser/smoke/tests.py
diff --git a/test/trace_processor/diff_tests/smoke/tests_compute_metrics.py b/test/trace_processor/diff_tests/parser/smoke/tests_compute_metrics.py
similarity index 100%
rename from test/trace_processor/diff_tests/smoke/tests_compute_metrics.py
rename to test/trace_processor/diff_tests/parser/smoke/tests_compute_metrics.py
diff --git a/test/trace_processor/diff_tests/smoke/tests_json.py b/test/trace_processor/diff_tests/parser/smoke/tests_json.py
similarity index 100%
rename from test/trace_processor/diff_tests/smoke/tests_json.py
rename to test/trace_processor/diff_tests/parser/smoke/tests_json.py
diff --git a/test/trace_processor/diff_tests/smoke/tests_sched_events.py b/test/trace_processor/diff_tests/parser/smoke/tests_sched_events.py
similarity index 97%
rename from test/trace_processor/diff_tests/smoke/tests_sched_events.py
rename to test/trace_processor/diff_tests/parser/smoke/tests_sched_events.py
index 86e39fd..fec730f 100644
--- a/test/trace_processor/diff_tests/smoke/tests_sched_events.py
+++ b/test/trace_processor/diff_tests/parser/smoke/tests_sched_events.py
@@ -56,7 +56,7 @@
   # Sched events from sythetic trace
   def test_synth_1_smoke(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         SELECT
           ts,
diff --git a/test/trace_processor/diff_tests/smoke/thread_cpu_time_example_android_trace_30s.out b/test/trace_processor/diff_tests/parser/smoke/thread_cpu_time_example_android_trace_30s.out
similarity index 100%
rename from test/trace_processor/diff_tests/smoke/thread_cpu_time_example_android_trace_30s.out
rename to test/trace_processor/diff_tests/parser/smoke/thread_cpu_time_example_android_trace_30s.out
diff --git a/test/trace_processor/diff_tests/track_event/experimental_slice_layout_depth.py b/test/trace_processor/diff_tests/parser/track_event/experimental_slice_layout_depth.py
similarity index 99%
rename from test/trace_processor/diff_tests/track_event/experimental_slice_layout_depth.py
rename to test/trace_processor/diff_tests/parser/track_event/experimental_slice_layout_depth.py
index 3baf368..25b7668 100644
--- a/test/trace_processor/diff_tests/track_event/experimental_slice_layout_depth.py
+++ b/test/trace_processor/diff_tests/parser/track_event/experimental_slice_layout_depth.py
@@ -21,6 +21,7 @@
 import synth_common
 
 from synth_common import ms_to_ns
+
 trace = synth_common.create_trace()
 
 track1 = 1234
diff --git a/test/trace_processor/diff_tests/track_event/flow_events_proto_v1.textproto b/test/trace_processor/diff_tests/parser/track_event/flow_events_proto_v1.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/flow_events_proto_v1.textproto
rename to test/trace_processor/diff_tests/parser/track_event/flow_events_proto_v1.textproto
diff --git a/test/trace_processor/diff_tests/track_event/flow_events_proto_v2.textproto b/test/trace_processor/diff_tests/parser/track_event/flow_events_proto_v2.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/flow_events_proto_v2.textproto
rename to test/trace_processor/diff_tests/parser/track_event/flow_events_proto_v2.textproto
diff --git a/test/trace_processor/diff_tests/track_event/flow_events_track_event.textproto b/test/trace_processor/diff_tests/parser/track_event/flow_events_track_event.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/flow_events_track_event.textproto
rename to test/trace_processor/diff_tests/parser/track_event/flow_events_track_event.textproto
diff --git a/test/trace_processor/diff_tests/track_event/legacy_async_event.out b/test/trace_processor/diff_tests/parser/track_event/legacy_async_event.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/legacy_async_event.out
rename to test/trace_processor/diff_tests/parser/track_event/legacy_async_event.out
diff --git a/test/trace_processor/diff_tests/track_event/legacy_async_event.textproto b/test/trace_processor/diff_tests/parser/track_event/legacy_async_event.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/legacy_async_event.textproto
rename to test/trace_processor/diff_tests/parser/track_event/legacy_async_event.textproto
diff --git a/test/trace_processor/diff_tests/track_event/range_of_interest.textproto b/test/trace_processor/diff_tests/parser/track_event/range_of_interest.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/range_of_interest.textproto
rename to test/trace_processor/diff_tests/parser/track_event/range_of_interest.textproto
diff --git a/test/trace_processor/diff_tests/track_event/tests.py b/test/trace_processor/diff_tests/parser/track_event/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/tests.py
rename to test/trace_processor/diff_tests/parser/track_event/tests.py
diff --git a/test/trace_processor/diff_tests/track_event/track_event_args_test.sql b/test/trace_processor/diff_tests/parser/track_event/track_event_args_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_args_test.sql
rename to test/trace_processor/diff_tests/parser/track_event/track_event_args_test.sql
diff --git a/test/trace_processor/diff_tests/track_event/track_event_chrome_histogram_sample.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_chrome_histogram_sample.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_chrome_histogram_sample.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_chrome_histogram_sample.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_chrome_histogram_sample_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_chrome_histogram_sample_args.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_chrome_histogram_sample_args.out
rename to test/trace_processor/diff_tests/parser/track_event/track_event_chrome_histogram_sample_args.out
diff --git a/test/trace_processor/diff_tests/track_event/track_event_counters.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_counters.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_counters.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_counters.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_counters_counters.out b/test/trace_processor/diff_tests/parser/track_event/track_event_counters_counters.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_counters_counters.out
rename to test/trace_processor/diff_tests/parser/track_event/track_event_counters_counters.out
diff --git a/test/trace_processor/diff_tests/track_event/track_event_merged_debug_annotations.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_merged_debug_annotations.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_merged_debug_annotations_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations_args.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_merged_debug_annotations_args.out
rename to test/trace_processor/diff_tests/parser/track_event/track_event_merged_debug_annotations_args.out
diff --git a/test/trace_processor/diff_tests/track_event/track_event_tracks.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_tracks.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_tracks.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_tracks_slices.out b/test/trace_processor/diff_tests/parser/track_event/track_event_tracks_slices.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_tracks_slices.out
rename to test/trace_processor/diff_tests/parser/track_event/track_event_tracks_slices.out
diff --git a/test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_typed_args.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_typed_args.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out b/test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_typed_args_args.out
rename to test/trace_processor/diff_tests/parser/track_event/track_event_typed_args_args.out
diff --git a/test/trace_processor/diff_tests/track_event/track_event_with_atrace.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_with_atrace.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_with_atrace.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_with_atrace.textproto
diff --git a/test/trace_processor/diff_tests/track_event/track_event_with_atrace_separate_tracks.textproto b/test/trace_processor/diff_tests/parser/track_event/track_event_with_atrace_separate_tracks.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/track_event/track_event_with_atrace_separate_tracks.textproto
rename to test/trace_processor/diff_tests/parser/track_event/track_event_with_atrace_separate_tracks.textproto
diff --git a/test/trace_processor/diff_tests/translation/chrome_args_test.sql b/test/trace_processor/diff_tests/parser/translated_args/chrome_args_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_args_test.sql
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_args_test.sql
diff --git a/test/trace_processor/diff_tests/translation/chrome_histogram.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_histogram.out
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.out
diff --git a/test/trace_processor/diff_tests/translation/chrome_histogram.textproto b/test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_histogram.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_histogram.textproto
diff --git a/test/trace_processor/diff_tests/translation/chrome_performance_mark.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_performance_mark.out
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_performance_mark.out
diff --git a/test/trace_processor/diff_tests/translation/chrome_user_event.out b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_user_event.out
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.out
diff --git a/test/trace_processor/diff_tests/translation/chrome_user_event.textproto b/test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/chrome_user_event.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/chrome_user_event.textproto
diff --git a/test/trace_processor/diff_tests/translation/java_class_name_arg.out b/test/trace_processor/diff_tests/parser/translated_args/java_class_name_arg.out
similarity index 100%
rename from test/trace_processor/diff_tests/translation/java_class_name_arg.out
rename to test/trace_processor/diff_tests/parser/translated_args/java_class_name_arg.out
diff --git a/test/trace_processor/diff_tests/translation/java_class_name_arg.textproto b/test/trace_processor/diff_tests/parser/translated_args/java_class_name_arg.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/java_class_name_arg.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/java_class_name_arg.textproto
diff --git a/test/trace_processor/diff_tests/translation/native_symbol_arg.out b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
similarity index 100%
rename from test/trace_processor/diff_tests/translation/native_symbol_arg.out
rename to test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.out
diff --git a/test/trace_processor/diff_tests/translation/native_symbol_arg.textproto b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/native_symbol_arg.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg.textproto
diff --git a/test/trace_processor/diff_tests/translation/native_symbol_arg_incomplete.textproto b/test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg_incomplete.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/native_symbol_arg_incomplete.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/native_symbol_arg_incomplete.textproto
diff --git a/test/trace_processor/diff_tests/translation/slice_name.textproto b/test/trace_processor/diff_tests/parser/translated_args/slice_name.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/slice_name.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/slice_name.textproto
diff --git a/test/trace_processor/diff_tests/translation/slice_name_negative_timestamp.textproto b/test/trace_processor/diff_tests/parser/translated_args/slice_name_negative_timestamp.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/translation/slice_name_negative_timestamp.textproto
rename to test/trace_processor/diff_tests/parser/translated_args/slice_name_negative_timestamp.textproto
diff --git a/test/trace_processor/diff_tests/translation/tests.py b/test/trace_processor/diff_tests/parser/translated_args/tests.py
similarity index 98%
rename from test/trace_processor/diff_tests/translation/tests.py
rename to test/trace_processor/diff_tests/parser/translated_args/tests.py
index 9856205..7e28f38 100644
--- a/test/trace_processor/diff_tests/translation/tests.py
+++ b/test/trace_processor/diff_tests/parser/translated_args/tests.py
@@ -19,7 +19,7 @@
 from python.generators.diff_tests.testing import TestSuite
 
 
-class Translation(TestSuite):
+class TranslatedArgs(TestSuite):
 
   def test_java_class_name_arg(self):
     return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/ufs/tests.py b/test/trace_processor/diff_tests/parser/ufs/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/ufs/tests.py
rename to test/trace_processor/diff_tests/parser/ufs/tests.py
diff --git a/test/trace_processor/diff_tests/ufs/ufshcd_command.textproto b/test/trace_processor/diff_tests/parser/ufs/ufshcd_command.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/ufs/ufshcd_command.textproto
rename to test/trace_processor/diff_tests/parser/ufs/ufshcd_command.textproto
diff --git a/test/trace_processor/diff_tests/ufs/ufshcd_command_tag.textproto b/test/trace_processor/diff_tests/parser/ufs/ufshcd_command_tag.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/ufs/ufshcd_command_tag.textproto
rename to test/trace_processor/diff_tests/parser/ufs/ufshcd_command_tag.textproto
diff --git a/test/trace_processor/diff_tests/scheduler/tests.py b/test/trace_processor/diff_tests/scheduler/tests.py
deleted file mode 100644
index b935c6a..0000000
--- a/test/trace_processor/diff_tests/scheduler/tests.py
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/env python3
-# Copyright (C) 2023 The Android Open Source Project
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License a
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from python.generators.diff_tests.testing import Path, DataPath, Metric
-from python.generators.diff_tests.testing import Csv, Json, TextProto
-from python.generators.diff_tests.testing import DiffTestBlueprint
-from python.generators.diff_tests.testing import TestSuite
-
-
-class Scheduler(TestSuite):
-  # Scheduler
-  def test_sched_cpu_util_cfs(self):
-    return DiffTestBlueprint(
-        trace=Path('sched_cpu_util_cfs.textproto'),
-        query=Path('sched_cpu_util_cfs_test.sql'),
-        out=Csv("""
-        "name","ts","value"
-        "Cpu 6 Util",10000,1.000000
-        "Cpu 6 Cap",10000,1004.000000
-        "Cpu 6 Nr Running",10000,0.000000
-        "Cpu 7 Util",11000,1.000000
-        "Cpu 7 Cap",11000,1007.000000
-        "Cpu 7 Nr Running",11000,0.000000
-        "Cpu 4 Util",12000,43.000000
-        "Cpu 4 Cap",12000,760.000000
-        "Cpu 4 Nr Running",12000,0.000000
-        "Cpu 5 Util",13000,125.000000
-        "Cpu 5 Cap",13000,757.000000
-        "Cpu 5 Nr Running",13000,1.000000
-        """))
diff --git a/test/trace_processor/diff_tests/dynamic/ancestor_slice.out b/test/trace_processor/diff_tests/stdlib/dynamic_tables/ancestor_slice.out
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/ancestor_slice.out
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/ancestor_slice.out
diff --git a/test/trace_processor/diff_tests/dynamic/connected_flow.out b/test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow.out
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/connected_flow.out
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow.out
diff --git a/test/trace_processor/diff_tests/dynamic/connected_flow_data.json b/test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow_data.json
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/connected_flow_data.json
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow_data.json
diff --git a/test/trace_processor/diff_tests/dynamic/connected_flow_test.sql b/test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/connected_flow_test.sql
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/connected_flow_test.sql
diff --git a/test/trace_processor/diff_tests/dynamic/descendant_slice.out b/test/trace_processor/diff_tests/stdlib/dynamic_tables/descendant_slice.out
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/descendant_slice.out
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/descendant_slice.out
diff --git a/test/trace_processor/diff_tests/dynamic/perf_sample_sc_annotated_callstack.out b/test/trace_processor/diff_tests/stdlib/dynamic_tables/perf_sample_sc_annotated_callstack.out
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/perf_sample_sc_annotated_callstack.out
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/perf_sample_sc_annotated_callstack.out
diff --git a/test/trace_processor/diff_tests/dynamic/relationship_tables.textproto b/test/trace_processor/diff_tests/stdlib/dynamic_tables/relationship_tables.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/relationship_tables.textproto
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/relationship_tables.textproto
diff --git a/test/trace_processor/diff_tests/dynamic/slice_stacks.textproto b/test/trace_processor/diff_tests/stdlib/dynamic_tables/slice_stacks.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/slice_stacks.textproto
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/slice_stacks.textproto
diff --git a/test/trace_processor/diff_tests/dynamic/tests.py b/test/trace_processor/diff_tests/stdlib/dynamic_tables/tests.py
similarity index 99%
rename from test/trace_processor/diff_tests/dynamic/tests.py
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/tests.py
index 3c79262..60b170c 100644
--- a/test/trace_processor/diff_tests/dynamic/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/dynamic_tables/tests.py
@@ -19,7 +19,7 @@
 from python.generators.diff_tests.testing import TestSuite
 
 
-class Dynamic(TestSuite):
+class DynamicTables(TestSuite):
   # Tests for custom dynamic tables. Ancestor slice table.
   def test_ancestor_slice(self):
     return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/dynamic/various_clocks.textproto b/test/trace_processor/diff_tests/stdlib/dynamic_tables/various_clocks.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/dynamic/various_clocks.textproto
rename to test/trace_processor/diff_tests/stdlib/dynamic_tables/various_clocks.textproto
diff --git a/test/trace_processor/diff_tests/pkvm/pkvm_hypervisor_events.textproto b/test/trace_processor/diff_tests/stdlib/pkvm/pkvm_hypervisor_events.textproto
similarity index 100%
rename from test/trace_processor/diff_tests/pkvm/pkvm_hypervisor_events.textproto
rename to test/trace_processor/diff_tests/stdlib/pkvm/pkvm_hypervisor_events.textproto
diff --git a/test/trace_processor/diff_tests/pkvm/tests.py b/test/trace_processor/diff_tests/stdlib/pkvm/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/pkvm/tests.py
rename to test/trace_processor/diff_tests/stdlib/pkvm/tests.py
diff --git a/test/trace_processor/diff_tests/slices/tests.py b/test/trace_processor/diff_tests/stdlib/slices/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/slices/tests.py
rename to test/trace_processor/diff_tests/stdlib/slices/tests.py
diff --git a/test/trace_processor/diff_tests/slices/trace.py b/test/trace_processor/diff_tests/stdlib/slices/trace.py
similarity index 100%
rename from test/trace_processor/diff_tests/slices/trace.py
rename to test/trace_processor/diff_tests/stdlib/slices/trace.py
diff --git a/test/trace_processor/diff_tests/span_join/android_sched_and_ps_slice_span_join_b118665515.out b/test/trace_processor/diff_tests/stdlib/span_join/android_sched_and_ps_slice_span_join_b118665515.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/android_sched_and_ps_slice_span_join_b118665515.out
rename to test/trace_processor/diff_tests/stdlib/span_join/android_sched_and_ps_slice_span_join_b118665515.out
diff --git a/test/trace_processor/diff_tests/span_join/slice_span_join_b118665515_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/slice_span_join_b118665515_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/slice_span_join_b118665515_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/slice_span_join_b118665515_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_join_unordered_cols_reverse_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_join_unordered_cols_reverse_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_join_unordered_cols_reverse_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_join_unordered_cols_reverse_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_join_unordered_cols_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_join_unordered_cols_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_join_unordered_cols_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_join_unordered_cols_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_join_zero_negative_dur_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_join_zero_negative_dur_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_join_zero_negative_dur_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_join_zero_negative_dur_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join.out b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join.out
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_left_partitioned.out b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_partitioned.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_left_partitioned.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_partitioned.out
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_left_partitioned_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_partitioned_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_left_partitioned_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_partitioned_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_left_unpartitioned.out b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_unpartitioned.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_left_unpartitioned.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_unpartitioned.out
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_left_unpartitioned_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_unpartitioned_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_left_unpartitioned_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_left_unpartitioned_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_unpartitioned.out b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_unpartitioned.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_unpartitioned.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_unpartitioned.out
diff --git a/test/trace_processor/diff_tests/span_join/span_left_join_unpartitioned_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_left_join_unpartitioned_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_left_join_unpartitioned_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_left_join_unpartitioned_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join.out b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join.out
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join_mixed.out b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_mixed.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join_mixed.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_mixed.out
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join_mixed_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_mixed_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join_mixed_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_mixed_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join_unpartitioned.out b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_unpartitioned.out
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join_unpartitioned.out
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_unpartitioned.out
diff --git a/test/trace_processor/diff_tests/span_join/span_outer_join_unpartitioned_test.sql b/test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_unpartitioned_test.sql
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/span_outer_join_unpartitioned_test.sql
rename to test/trace_processor/diff_tests/stdlib/span_join/span_outer_join_unpartitioned_test.sql
diff --git a/test/trace_processor/diff_tests/span_join/tests_left_join.py b/test/trace_processor/diff_tests/stdlib/span_join/tests_left_join.py
similarity index 91%
rename from test/trace_processor/diff_tests/span_join/tests_left_join.py
rename to test/trace_processor/diff_tests/stdlib/span_join/tests_left_join.py
index 2bd2e58..30fd6ce 100644
--- a/test/trace_processor/diff_tests/span_join/tests_left_join.py
+++ b/test/trace_processor/diff_tests/stdlib/span_join/tests_left_join.py
@@ -23,31 +23,31 @@
 
   def test_span_left_join(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_left_join_test.sql'),
         out=Path('span_left_join.out'))
 
   def test_span_left_join_unpartitioned(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_left_join_unpartitioned_test.sql'),
         out=Path('span_left_join_unpartitioned.out'))
 
   def test_span_left_join_left_unpartitioned(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_left_join_left_unpartitioned_test.sql'),
         out=Path('span_left_join_left_unpartitioned.out'))
 
   def test_span_left_join_left_partitioned(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_left_join_left_partitioned_test.sql'),
         out=Path('span_left_join_left_partitioned.out'))
 
   def test_span_left_join_empty_right(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -79,7 +79,7 @@
 
   def test_span_left_join_unordered_android_sched_and_ps(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
diff --git a/test/trace_processor/diff_tests/span_join/tests_outer_join.py b/test/trace_processor/diff_tests/stdlib/span_join/tests_outer_join.py
similarity index 92%
rename from test/trace_processor/diff_tests/span_join/tests_outer_join.py
rename to test/trace_processor/diff_tests/stdlib/span_join/tests_outer_join.py
index c2ac4f5..d0354ca 100644
--- a/test/trace_processor/diff_tests/span_join/tests_outer_join.py
+++ b/test/trace_processor/diff_tests/stdlib/span_join/tests_outer_join.py
@@ -23,13 +23,13 @@
 
   def test_span_outer_join(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_outer_join_test.sql'),
         out=Path('span_outer_join.out'))
 
   def test_span_outer_join_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -61,7 +61,7 @@
 
   def test_span_outer_join_unpartitioned_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -86,7 +86,7 @@
 
   def test_span_outer_join_unpartitioned_left_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -119,7 +119,7 @@
 
   def test_span_outer_join_unpartitioned_right_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -152,13 +152,13 @@
 
   def test_span_outer_join_mixed(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_outer_join_mixed_test.sql'),
         out=Path('span_outer_join_mixed.out'))
 
   def test_span_outer_join_mixed_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -184,7 +184,7 @@
 
   def test_span_outer_join_mixed_left_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -215,7 +215,7 @@
 
   def test_span_outer_join_mixed_left_empty_rev(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -249,7 +249,7 @@
 
   def test_span_outer_join_mixed_right_empty(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -284,7 +284,7 @@
 
   def test_span_outer_join_mixed_right_empty_rev(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query="""
         CREATE TABLE t1(
           ts BIGINT,
@@ -316,6 +316,6 @@
 
   def test_span_outer_join_mixed_2(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_outer_join_mixed_test.sql'),
         out=Path('span_outer_join_mixed.out'))
diff --git a/test/trace_processor/diff_tests/span_join/tests_regression.py b/test/trace_processor/diff_tests/stdlib/span_join/tests_regression.py
similarity index 100%
rename from test/trace_processor/diff_tests/span_join/tests_regression.py
rename to test/trace_processor/diff_tests/stdlib/span_join/tests_regression.py
diff --git a/test/trace_processor/diff_tests/span_join/tests_smoke.py b/test/trace_processor/diff_tests/stdlib/span_join/tests_smoke.py
similarity index 95%
rename from test/trace_processor/diff_tests/span_join/tests_smoke.py
rename to test/trace_processor/diff_tests/stdlib/span_join/tests_smoke.py
index e94a6c8..bacaa9a 100644
--- a/test/trace_processor/diff_tests/span_join/tests_smoke.py
+++ b/test/trace_processor/diff_tests/stdlib/span_join/tests_smoke.py
@@ -23,7 +23,7 @@
 
   def test_span_join_unordered_cols_synth_1(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_join_unordered_cols_test.sql'),
         out=Csv("""
         "ts","dur","part","b1","b2","b3","a1","a2","a3"
@@ -38,7 +38,7 @@
 
   def test_span_join_unordered_cols_synth_1_2(self):
     return DiffTestBlueprint(
-        trace=Path('../common/synth_1.py'),
+        trace=Path('../../common/synth_1.py'),
         query=Path('span_join_unordered_cols_reverse_test.sql'),
         out=Csv("""
         "ts","dur","part","b1","b2","b3","a1","a2","a3"
diff --git a/test/trace_processor/diff_tests/time/tests.py b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
similarity index 98%
rename from test/trace_processor/diff_tests/time/tests.py
rename to test/trace_processor/diff_tests/stdlib/timestamps/tests.py
index ee8771d..a029ce5 100644
--- a/test/trace_processor/diff_tests/time/tests.py
+++ b/test/trace_processor/diff_tests/stdlib/timestamps/tests.py
@@ -20,7 +20,7 @@
 from google.protobuf import text_format
 
 
-class Time(TestSuite):
+class Timestamps(TestSuite):
 
   def test_ns(self):
     return DiffTestBlueprint(
diff --git a/test/trace_processor/diff_tests/functions/tests.py b/test/trace_processor/diff_tests/syntax/functions/tests.py
similarity index 99%
rename from test/trace_processor/diff_tests/functions/tests.py
rename to test/trace_processor/diff_tests/syntax/functions/tests.py
index 966e87d..f613fb5 100644
--- a/test/trace_processor/diff_tests/functions/tests.py
+++ b/test/trace_processor/diff_tests/syntax/functions/tests.py
@@ -20,6 +20,7 @@
 from python.generators.diff_tests.testing import PrintProfileProto
 from google.protobuf import text_format
 
+
 class Functions(TestSuite):
 
   def test_create_function(self):
diff --git a/test/trace_processor/diff_tests/perfetto_sql/tests.py b/test/trace_processor/diff_tests/syntax/perfetto_sql/tests.py
similarity index 100%
rename from test/trace_processor/diff_tests/perfetto_sql/tests.py
rename to test/trace_processor/diff_tests/syntax/perfetto_sql/tests.py
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 646c8f8..992bc7b 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -30,9 +30,7 @@
   tableColumnEquals,
   toggleEnabled,
 } from '../frontend/pivot_table_types';
-import {PrimaryTrackSortKey, TrackTags} from '../public/index';
-import {CounterDebugTrackConfig} from '../tracks/debug/counter_track';
-import {DebugTrackV2Config} from '../tracks/debug/slice_track';
+import {PrimaryTrackSortKey} from '../public/index';
 
 import {randomColor} from './colorizer';
 import {
@@ -85,15 +83,12 @@
 
 export interface AddTrackArgs {
   id?: string;
-  engineId: string;
-  kind: string;
+  uri: string;
   name: string;
   labels?: string[];
   trackSortKey: TrackSortKey;
   trackGroup?: string;
-  config: {};
-  tags?: TrackTags;
-  uri?: string;  // Only used for new PLUGIN_TRACK tracks
+  initialState?: unknown;
 }
 
 export interface PostedTrace {
@@ -215,9 +210,6 @@
 
   fillUiTrackIdByTraceTrackId(
       state: StateDraft, trackState: TrackState, uiTrackId: string) {
-    const namespace = (trackState.config as {namespace?: string}).namespace;
-    if (namespace !== undefined) return;
-
     const setUiTrackId = (trackId: number, uiTrackId: string) => {
       if (state.uiTrackIdByTraceTrackId[trackId] !== undefined &&
           state.uiTrackIdByTraceTrackId[trackId] !== uiTrackId) {
@@ -228,7 +220,7 @@
       state.uiTrackIdByTraceTrackId[trackId] = uiTrackId;
     };
 
-    const {uri, config} = trackState;
+    const {uri} = trackState;
     if (exists(uri)) {
       // If track is a new "plugin" type track (i.e. it has a uri), resolve the
       // track ids from through the pluginManager.
@@ -238,16 +230,6 @@
           setUiTrackId(trackId, uiTrackId);
         }
       }
-    } else {
-      // Traditional track - resolve track ids through the config.
-      const {trackId, trackIds} = config;
-      if (exists(trackId)) {
-        setUiTrackId(trackId, uiTrackId);
-      } else if (exists(trackIds)) {
-        for (const trackId of trackIds) {
-          setUiTrackId(trackId, uiTrackId);
-        }
-      }
     }
   },
 
@@ -255,18 +237,14 @@
     args.tracks.forEach((track) => {
       const id = track.id === undefined ? generateNextId(state) : track.id;
       const name = track.name;
-      const tags = track.tags ?? {name};
       state.tracks[id] = {
         id,
-        engineId: track.engineId,
-        kind: track.kind,
         name,
         trackSortKey: track.trackSortKey,
         trackGroup: track.trackGroup,
-        tags,
-        config: track.config,
         labels: track.labels,
         uri: track.uri,
+        state: track.initialState,
       };
       this.fillUiTrackIdByTraceTrackId(state, track as TrackState, id);
       if (track.trackGroup === SCROLLING_TRACK_GROUP) {
@@ -280,6 +258,19 @@
     });
   },
 
+  // 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: {trackInstanceIds: string[]}) {
+    for (const trackInstanceId of args.trackInstanceIds) {
+      removeTrack(state, trackInstanceId);
+    }
+  },
+
   setUtidToTrackSortKey(
       state: StateDraft, args: {threadOrderingMetadata: UtidToTrackSortKey}) {
     state.utidToThreadSortKey = args.threadOrderingMetadata;
@@ -294,12 +285,10 @@
       // Define ID in action so a track group can be referred to without running
       // the reducer.
       args: {
-        engineId: string; name: string; id: string; summaryTrackId: string;
-        collapsed: boolean;
+        name: string; id: string; summaryTrackId: string; collapsed: boolean;
         fixedOrdering?: boolean;
       }): void {
     state.trackGroups[args.id] = {
-      engineId: args.engineId,
       name: args.name,
       id: args.id,
       collapsed: args.collapsed,
@@ -308,66 +297,6 @@
     };
   },
 
-  addDebugSliceTrack(
-      state: StateDraft,
-      args: {engineId: string, name: string, config: DebugTrackV2Config}):
-      void {
-        const trackId = generateNextId(state);
-        this.addTrack(state, {
-          id: trackId,
-          engineId: args.engineId,
-          name: args.name,
-          trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-          kind: DEBUG_SLICE_TRACK_KIND,
-          config: args.config,
-        });
-        this.toggleTrackPinned(state, {trackId});
-      },
-
-  addDebugCounterTrack(state: StateDraft, args: {
-    engineId: string,
-    name: string,
-    config: CounterDebugTrackConfig,
-  }): void {
-    const trackId = generateNextId(state);
-    this.addTrack(state, {
-      id: trackId,
-      engineId: args.engineId,
-      name: args.name,
-      trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
-      trackGroup: SCROLLING_TRACK_GROUP,
-      kind: DEBUG_COUNTER_TRACK_KIND,
-      config: args.config,
-    });
-    this.toggleTrackPinned(state, {trackId});
-  },
-
-
-  removeDebugTrack(state: StateDraft, args: {trackId: string}): void {
-    const track = state.tracks[args.trackId];
-    if (track !== undefined) {
-      assertTrue(
-          track.kind === DEBUG_SLICE_TRACK_KIND ||
-          track.kind === DEBUG_COUNTER_TRACK_KIND);
-      removeTrack(state, args.trackId);
-    }
-  },
-
-  removeVisualisedArgTracks(state: StateDraft, args: {trackIds: string[]}) {
-    for (const trackId of args.trackIds) {
-      const track = state.tracks[trackId];
-
-      const namespace = (track.config as {namespace?: string}).namespace;
-      if (namespace === undefined) {
-        throw new Error(
-            'All visualised arg tracks should have non-empty namespace');
-      }
-
-      removeTrack(state, trackId);
-    }
-  },
-
   maybeExpandOnlyTrackGroup(state: StateDraft, _: {}): void {
     const trackGroups = Object.values(state.trackGroups);
     if (trackGroups.length === 1) {
@@ -827,7 +756,7 @@
 
   selectChromeSlice(
       state: StateDraft,
-      args: {id: number, trackId: string, table: string, scroll?: boolean}):
+      args: {id: number, trackId: string, table?: string, scroll?: boolean}):
       void {
         state.currentSelection = {
           kind: 'CHROME_SLICE',
@@ -1164,17 +1093,6 @@
             }));
   },
 
-  addVisualisedArg(state: StateDraft, args: {argName: string}) {
-    if (!state.visualisedArgs.includes(args.argName)) {
-      state.visualisedArgs.push(args.argName);
-    }
-  },
-
-  removeVisualisedArg(state: StateDraft, args: {argName: string}) {
-    state.visualisedArgs =
-        state.visualisedArgs.filter((val) => val !== args.argName);
-  },
-
   setPivotTableArgumentNames(
       state: StateDraft, args: {argumentNames: string[]}) {
     state.nonSerializableState.pivotTable.argumentNames = args.argumentNames;
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index 8713c17..40c6949 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -37,7 +37,7 @@
 
 function fakeTrack(state: State, args: {
   id: string,
-  kind?: string,
+  uri?: string,
   trackGroup?: string,
   trackSortKey?: TrackSortKey,
   name?: string,
@@ -45,15 +45,13 @@
 }): State {
   return produce(state, (draft) => {
     StateActions.addTrack(draft, {
+      uri: args.uri || 'sometrack',
       id: args.id,
-      engineId: '0',
-      kind: args.kind || 'SOME_TRACK_KIND',
       name: args.name || 'A track',
       trackSortKey: args.trackSortKey === undefined ?
           PrimaryTrackSortKey.ORDINARY_TRACK :
           args.trackSortKey,
       trackGroup: args.trackGroup || SCROLLING_TRACK_GROUP,
-      config: {tid: args.tid || '0'},
     });
   });
 }
@@ -64,7 +62,6 @@
     StateActions.addTrackGroup(draft, {
       name: 'A group',
       id: args.id,
-      engineId: '0',
       collapsed: false,
       summaryTrackId: args.summaryTrackId,
     });
@@ -89,22 +86,18 @@
 test('add scrolling tracks', () => {
   const once = produce(createEmptyState(), (draft) => {
     StateActions.addTrack(draft, {
-      engineId: '1',
-      kind: 'cpu',
+      uri: 'cpu',
       name: 'Cpu 1',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
       trackGroup: SCROLLING_TRACK_GROUP,
-      config: {},
     });
   });
   const twice = produce(once, (draft) => {
     StateActions.addTrack(draft, {
-      engineId: '2',
-      kind: 'cpu',
+      uri: 'cpu',
       name: 'Cpu 2',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
       trackGroup: SCROLLING_TRACK_GROUP,
-      config: {},
     });
   });
 
@@ -118,7 +111,6 @@
 
   const afterGroup = produce(state, (draft) => {
     StateActions.addTrackGroup(draft, {
-      engineId: '1',
       name: 'A track group',
       id: '123-123-123',
       summaryTrackId: 's',
@@ -129,12 +121,10 @@
   const afterTrackAdd = produce(afterGroup, (draft) => {
     StateActions.addTrack(draft, {
       id: '1',
-      engineId: '1',
-      kind: 'slices',
+      uri: 'slices',
       name: 'renderer 1',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
       trackGroup: '123-123-123',
-      config: {},
     });
   });
 
@@ -145,18 +135,14 @@
 test('reorder tracks', () => {
   const once = produce(createEmptyState(), (draft) => {
     StateActions.addTrack(draft, {
-      engineId: '1',
-      kind: 'cpu',
+      uri: 'cpu',
       name: 'Cpu 1',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      config: {},
     });
     StateActions.addTrack(draft, {
-      engineId: '2',
-      kind: 'cpu',
+      uri: 'cpu',
       name: 'Cpu 2',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      config: {},
     });
   });
 
@@ -285,11 +271,9 @@
 
   const twice = produce(once, (draft) => {
     StateActions.addTrack(draft, {
-      engineId: '1',
-      kind: 'cpu',
+      uri: 'cpu',
       name: 'Cpu 1',
       trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-      config: {},
     });
   });
 
@@ -332,13 +316,13 @@
   state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
   state = fakeTrack(state, {
     id: 'b',
-    kind: HEAP_PROFILE_TRACK_KIND,
+    uri: HEAP_PROFILE_TRACK_KIND,
     trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
     trackGroup: 'g',
   });
   state = fakeTrack(state, {
     id: 'a',
-    kind: PROCESS_SCHEDULING_TRACK_KIND,
+    uri: PROCESS_SCHEDULING_TRACK_KIND,
     trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
     trackGroup: 'g',
   });
@@ -357,34 +341,34 @@
   state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
   state = fakeTrack(state, {
     id: 'a',
-    kind: PROCESS_SCHEDULING_TRACK_KIND,
+    uri: PROCESS_SCHEDULING_TRACK_KIND,
     trackSortKey: PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK,
     trackGroup: 'g',
   });
   state = fakeTrack(state, {
     id: 'b',
-    kind: SLICE_TRACK_KIND,
+    uri: SLICE_TRACK_KIND,
     trackGroup: 'g',
     trackSortKey: PrimaryTrackSortKey.MAIN_THREAD,
   });
   state = fakeTrack(state, {
     id: 'c',
-    kind: SLICE_TRACK_KIND,
+    uri: SLICE_TRACK_KIND,
     trackGroup: 'g',
     trackSortKey: PrimaryTrackSortKey.RENDER_THREAD,
   });
   state = fakeTrack(state, {
     id: 'd',
-    kind: SLICE_TRACK_KIND,
+    uri: SLICE_TRACK_KIND,
     trackGroup: 'g',
     trackSortKey: PrimaryTrackSortKey.GPU_COMPLETION_THREAD,
   });
   state = fakeTrack(
-      state, {id: 'e', kind: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
+      state, {id: 'e', uri: HEAP_PROFILE_TRACK_KIND, trackGroup: 'g'});
   state = fakeTrack(
-      state, {id: 'f', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T2'});
+      state, {id: 'f', uri: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T2'});
   state = fakeTrack(
-      state, {id: 'g', kind: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T10'});
+      state, {id: 'g', uri: SLICE_TRACK_KIND, trackGroup: 'g', name: 'T10'});
 
   const after = produce(state, (draft) => {
     StateActions.sortThreadTracks(draft, {});
@@ -404,7 +388,7 @@
   state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
   state = fakeTrack(state, {
     id: 'a',
-    kind: SLICE_TRACK_KIND,
+    uri: SLICE_TRACK_KIND,
     trackSortKey: {
       utid: 1,
       priority: InThreadTrackSortKey.ORDINARY,
@@ -415,7 +399,7 @@
   });
   state = fakeTrack(state, {
     id: 'b',
-    kind: SLICE_TRACK_KIND,
+    uri: SLICE_TRACK_KIND,
     trackSortKey: {
       utid: 2,
       priority: InThreadTrackSortKey.ORDINARY,
@@ -426,7 +410,7 @@
   });
   state = fakeTrack(state, {
     id: 'c',
-    kind: THREAD_STATE_TRACK_KIND,
+    uri: THREAD_STATE_TRACK_KIND,
     trackSortKey: {
       utid: 1,
       priority: InThreadTrackSortKey.ORDINARY,
diff --git a/ui/src/common/basic_async_track.ts b/ui/src/common/basic_async_track.ts
index 2faf829..2f62dc6 100644
--- a/ui/src/common/basic_async_track.ts
+++ b/ui/src/common/basic_async_track.ts
@@ -19,7 +19,6 @@
 import {globals} from '../frontend/globals';
 import {PxSpan, TimeScale} from '../frontend/time_scale';
 import {SliceRect} from '../frontend/track';
-import {TrackButtonAttrs} from '../frontend/track_panel';
 import {Track} from '../public';
 
 import {TrackData} from './track_data';
@@ -67,14 +66,10 @@
 
   abstract getHeight(): number;
 
-  getTrackShellButtons(): m.Vnode<TrackButtonAttrs, {}>[] {
+  getTrackShellButtons(): m.Children {
     return [];
   }
 
-  getContextMenu(): m.Vnode<any, {}>|null {
-    return null;
-  }
-
   onMouseMove(_position: {x: number; y: number;}): void {}
 
   onMouseClick(_position: {x: number; y: number;}): boolean {
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index 23644e5..92ab59a 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -104,7 +104,6 @@
     queries: {},
     permalink: {},
     notes: {},
-    visualisedArgs: [],
 
     recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get() ?
         autosaveConfigStore.get() :
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 13a4e10..5ae6851 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -15,13 +15,7 @@
 import {Disposable, Trash} from '../base/disposable';
 import {assertFalse} from '../base/logging';
 import {ViewerImpl, ViewerProxy} from '../common/viewer';
-import {
-  TrackControllerFactory,
-  trackControllerRegistry,
-} from '../controller/track_controller';
 import {globals} from '../frontend/globals';
-import {TrackCreator} from '../frontend/track';
-import {trackRegistry} from '../frontend/track_registry';
 import {
   BasePlugin,
   Command,
@@ -73,18 +67,6 @@
     });
   }
 
-  LEGACY_registerTrackController(track: TrackControllerFactory): void {
-    if (!this.alive) return;
-    const unregister = trackControllerRegistry.register(track);
-    this.trash.add(unregister);
-  }
-
-  LEGACY_registerTrack(track: TrackCreator): void {
-    if (!this.alive) return;
-    const unregister = trackRegistry.register(track);
-    this.trash.add(unregister);
-  }
-
   dispose(): void {
     this.trash.dispose();
     this.alive = false;
@@ -109,18 +91,6 @@
     this.trash.add(store);
   }
 
-  LEGACY_registerTrackController(track: TrackControllerFactory): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-    this.ctx.LEGACY_registerTrackController(track);
-  }
-
-  LEGACY_registerTrack(track: TrackCreator): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
-    this.ctx.LEGACY_registerTrack(track);
-  }
-
   addCommand(cmd: Command): void {
     // Silently ignore if context is dead.
     if (!this.alive) return;
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 798d7dc..2b69f0d 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -23,7 +23,7 @@
   PivotTree,
   TableColumn,
 } from '../frontend/pivot_table_types';
-import {PrimaryTrackSortKey, TrackTags} from '../public/index';
+import {PrimaryTrackSortKey} from '../public/index';
 
 import {Direction} from './event_set';
 
@@ -123,7 +123,8 @@
 //     state entries now require a URI and old track implementations are no
 //     longer registered.
 // 40. Ported counter, process summary/sched, & cpu_freq to plugin tracks.
-export const STATE_VERSION = 40;
+// 41. Ported all remaining tracks.
+export const STATE_VERSION = 41;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -218,25 +219,17 @@
     TraceFileSource|TraceArrayBufferSource|TraceUrlSource|TraceHttpRpcSource;
 
 export interface TrackState {
+  uri: string;
   id: string;
-  engineId: string;
-  kind: string;
   name: string;
   labels?: string[];
   trackSortKey: TrackSortKey;
   trackGroup?: string;
-  tags: TrackTags;
-  config: {
-    trackId?: number;
-    trackIds?: number[];
-  };
-  uri?: string;
   state?: unknown;
 }
 
 export interface TrackGroupState {
   id: string;
-  engineId: string;
   name: string;
   collapsed: boolean;
   tracks: string[];  // Child track ids.
@@ -350,7 +343,7 @@
 export interface ChromeSliceSelection {
   kind: 'CHROME_SLICE';
   id: number;
-  table: string;
+  table?: string;
 }
 
 export interface ThreadStateSelection {
@@ -554,7 +547,6 @@
   ftracePagination: Pagination;
   ftraceFilter: FtraceFilterState;
   traceConversionInProgress: boolean;
-  visualisedArgs: string[];
 
   /**
    * This state is updated on the frontend at 60Hz and eventually syncronised to
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index 34646ba..23832af 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -27,23 +27,17 @@
   const state: State = createEmptyState();
   state.tracks['a'] = {
     id: 'a',
-    engineId: 'engine',
-    kind: 'Foo',
+    uri: 'Foo',
     name: 'a track',
     trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    config: {},
-    tags: {},
   };
 
   state.tracks['b'] = {
     id: 'b',
-    engineId: 'engine',
-    kind: 'Foo',
+    uri: 'Foo',
     name: 'b track',
     trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
-    config: {},
     trackGroup: 'containsB',
-    tags: {},
   };
 
   expect(getContainingTrackId(state, 'z')).toEqual(null);
diff --git a/ui/src/common/track_adapter.ts b/ui/src/common/track_adapter.ts
index 1c145d3..ffd7da8 100644
--- a/ui/src/common/track_adapter.ts
+++ b/ui/src/common/track_adapter.ts
@@ -20,7 +20,6 @@
 import {EngineProxy} from '../common/engine';
 import {PxSpan, TimeScale} from '../frontend/time_scale';
 import {NewTrackArgs, SliceRect} from '../frontend/track';
-import {TrackButtonAttrs} from '../frontend/track_panel';
 
 import {BasicAsyncTrack} from './basic_async_track';
 
@@ -76,14 +75,10 @@
     return this.track.getHeight();
   }
 
-  getTrackShellButtons(): m.Vnode<TrackButtonAttrs, {}>[] {
+  getTrackShellButtons(): m.Children {
     return this.track.getTrackShellButtons();
   }
 
-  getContextMenu(): m.Vnode<any, {}>|null {
-    return this.track.getContextMenu();
-  }
-
   onMouseMove(position: {x: number; y: number;}): void {
     this.track.onMouseMove(position);
   }
@@ -155,14 +150,10 @@
     return 40;
   }
 
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
+  getTrackShellButtons(): m.Children {
     return [];
   }
 
-  getContextMenu(): m.Vnode<any>|null {
-    return null;
-  }
-
   onMouseMove(_position: {x: number, y: number}) {}
 
   // Returns whether the mouse click has selected something.
diff --git a/ui/src/controller/aggregation/frame_aggregation_controller.ts b/ui/src/controller/aggregation/frame_aggregation_controller.ts
index a22a83d..2ad94d0 100644
--- a/ui/src/controller/aggregation/frame_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/frame_aggregation_controller.ts
@@ -14,12 +14,10 @@
 
 import {ColumnDef} from '../../common/aggregation_data';
 import {Engine} from '../../common/engine';
+import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
-import {
-  ACTUAL_FRAMES_SLICE_TRACK_KIND,
-  Config,
-} from '../../tracks/actual_frames';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../../tracks/actual_frames';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -27,13 +25,15 @@
   async createAggregateView(engine: Engine, area: Area) {
     await engine.query(`drop view if exists ${this.kind};`);
 
-    const selectedSqlTrackIds = [];
+    const selectedSqlTrackIds: number[] = [];
     for (const trackId of area.tracks) {
       const track = globals.state.tracks[trackId];
       // Track will be undefined for track groups.
-      if (track !== undefined &&
-          track.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
-        selectedSqlTrackIds.push((track.config as Config).trackIds);
+      if (track?.uri !== undefined) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
+          trackInfo.trackIds && selectedSqlTrackIds.push(...trackInfo.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 8dcaccc..4a2bfa4 100644
--- a/ui/src/controller/aggregation/slice_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/slice_aggregation_controller.ts
@@ -14,33 +14,26 @@
 
 import {ColumnDef} from '../../common/aggregation_data';
 import {Engine} from '../../common/engine';
+import {pluginManager} from '../../common/plugins';
 import {Area, Sorting} from '../../common/state';
 import {globals} from '../../frontend/globals';
-import {
-  ASYNC_SLICE_TRACK_KIND,
-  Config as AsyncSliceConfig,
-} from '../../tracks/async_slices';
-import {
-  Config as SliceConfig,
-  SLICE_TRACK_KIND,
-} from '../../tracks/chrome_slices';
+import {ASYNC_SLICE_TRACK_KIND} from '../../tracks/async_slices';
+import {SLICE_TRACK_KIND} from '../../tracks/chrome_slices';
 
 import {AggregationController} from './aggregation_controller';
 
 export function getSelectedTrackIds(area: Area): number[] {
-  const selectedTrackIds = [];
+  const selectedTrackIds: number[] = [];
   for (const trackId of area.tracks) {
     const track = globals.state.tracks[trackId];
     // Track will be undefined for track groups.
-    if (track !== undefined) {
-      if (track.kind === SLICE_TRACK_KIND) {
-        selectedTrackIds.push((track.config as SliceConfig).trackId);
+    if (track?.uri !== undefined) {
+      const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+      if (trackInfo?.kind === SLICE_TRACK_KIND) {
+        trackInfo.trackIds && selectedTrackIds.push(...trackInfo.trackIds);
       }
-      if (track.kind === ASYNC_SLICE_TRACK_KIND) {
-        const config = track.config as AsyncSliceConfig;
-        for (const id of config.trackIds) {
-          selectedTrackIds.push(id);
-        }
+      if (trackInfo?.kind === ASYNC_SLICE_TRACK_KIND) {
+        trackInfo.trackIds && selectedTrackIds.push(...trackInfo.trackIds);
       }
     }
   }
diff --git a/ui/src/controller/aggregation/thread_aggregation_controller.ts b/ui/src/controller/aggregation/thread_aggregation_controller.ts
index 7436a57..6818cfb 100644
--- a/ui/src/controller/aggregation/thread_aggregation_controller.ts
+++ b/ui/src/controller/aggregation/thread_aggregation_controller.ts
@@ -14,14 +14,12 @@
 
 import {ColumnDef, ThreadStateExtra} from '../../common/aggregation_data';
 import {Engine} from '../../common/engine';
+import {pluginManager} from '../../common/plugins';
 import {NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
 import {Area, Sorting} from '../../common/state';
 import {translateState} from '../../common/thread_state';
 import {globals} from '../../frontend/globals';
-import {
-  Config,
-  THREAD_STATE_TRACK_KIND,
-} from '../../tracks/thread_state';
+import {THREAD_STATE_TRACK_KIND} from '../../tracks/thread_state';
 
 import {AggregationController} from './aggregation_controller';
 
@@ -33,8 +31,11 @@
     for (const trackId of tracks) {
       const track = globals.state.tracks[trackId];
       // Track will be undefined for track groups.
-      if (track !== undefined && track.kind === THREAD_STATE_TRACK_KIND) {
-        this.utids.push((track.config as Config).utid);
+      if (track?.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.kind === THREAD_STATE_TRACK_KIND) {
+          trackInfo.utid && this.utids.push(trackInfo.utid);
+        }
       }
     }
   }
diff --git a/ui/src/controller/flamegraph_controller.ts b/ui/src/controller/flamegraph_controller.ts
index 20bdb04..c9e6f02 100644
--- a/ui/src/controller/flamegraph_controller.ts
+++ b/ui/src/controller/flamegraph_controller.ts
@@ -26,14 +26,12 @@
   PERF_SAMPLES_KEY,
   SPACE_MEMORY_ALLOCATED_NOT_FREED_KEY,
 } from '../common/flamegraph_util';
+import {pluginManager} from '../common/plugins';
 import {NUM, STR} from '../common/query_result';
 import {CallsiteInfo, FlamegraphState, ProfileType} from '../common/state';
 import {FlamegraphDetails, globals} from '../frontend/globals';
 import {publishFlamegraphDetails} from '../frontend/publish';
-import {
-  Config as PerfSampleConfig,
-  PERF_SAMPLES_PROFILE_TRACK_KIND,
-} from '../tracks/perf_samples_profile';
+import {PERF_SAMPLES_PROFILE_TRACK_KIND} from '../tracks/perf_samples_profile';
 
 import {AreaSelectionHandler} from './area_selection_handler';
 import {Controller} from './controller';
@@ -131,12 +129,13 @@
         return;
       }
       for (const trackId of area.tracks) {
-        const trackState = globals.state.tracks[trackId];
-        if (!trackState ||
-            trackState.kind !== PERF_SAMPLES_PROFILE_TRACK_KIND) {
-          continue;
+        const track = globals.state.tracks[trackId];
+        if (track?.uri) {
+          const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+          if (trackInfo?.kind === PERF_SAMPLES_PROFILE_TRACK_KIND) {
+            trackInfo.upid && upids.push(trackInfo.upid);
+          }
         }
-        upids.push((trackState.config as PerfSampleConfig).upid);
       }
       if (upids.length === 0) {
         this.checkCompletionAndPublishFlamegraph(
diff --git a/ui/src/controller/flow_events_controller.ts b/ui/src/controller/flow_events_controller.ts
index a1c4170..fd5afb2 100644
--- a/ui/src/controller/flow_events_controller.ts
+++ b/ui/src/controller/flow_events_controller.ts
@@ -21,14 +21,8 @@
 import {Flow, globals} from '../frontend/globals';
 import {publishConnectedFlows, publishSelectedFlows} from '../frontend/publish';
 import {asSliceSqlId} from '../frontend/sql_types';
-import {
-  ACTUAL_FRAMES_SLICE_TRACK_KIND,
-  Config as ActualConfig,
-} from '../tracks/actual_frames';
-import {
-  Config as SliceConfig,
-  SLICE_TRACK_KIND,
-} from '../tracks/chrome_slices';
+import {ACTUAL_FRAMES_SLICE_TRACK_KIND} from '../tracks/actual_frames';
+import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
 
 import {Controller} from './controller';
 
@@ -237,24 +231,14 @@
       // anything if there is only one TP track in this async track. In
       // that case experimental_slice_layout is just an expensive way
       // to find out depth === layout_depth.
-      const trackIds = track.config.trackIds;
+      const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+      const trackIds = trackInfo?.trackIds;
       if (trackIds === undefined || trackIds.length <= 1) {
         uiTrackIdToInfo.set(uiTrackId, null);
         trackIdToInfo.set(trackId, null);
         return null;
       }
 
-      // Perform the same check for "plugin" style tracks.
-      if (track.uri) {
-        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-        const trackIds = trackInfo?.trackIds;
-        if (trackIds === undefined || trackIds.length <= 1) {
-          uiTrackIdToInfo.set(uiTrackId, null);
-          trackIdToInfo.set(trackId, null);
-          return null;
-        }
-      }
-
       const newInfo = {
         uiTrackId,
         siblingTrackIds: trackIds,
@@ -390,19 +374,16 @@
 
     for (const uiTrackId of area.tracks) {
       const track = globals.state.tracks[uiTrackId];
-      if (track === undefined) {
-        continue;
-      }
-      if (track.kind === SLICE_TRACK_KIND) {
-        trackIds.push((track.config as SliceConfig).trackId);
-      } else if (track.kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
-        const actualConfig = track.config as ActualConfig;
-        for (const trackId of actualConfig.trackIds) {
-          trackIds.push(trackId);
-        }
-      } else if (track.config.trackIds !== undefined) {
-        for (const trackId of track.config.trackIds) {
-          trackIds.push(trackId);
+      if (track?.uri !== undefined) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        const kind = trackInfo?.kind;
+        if (kind === SLICE_TRACK_KIND ||
+            kind === ACTUAL_FRAMES_SLICE_TRACK_KIND) {
+          if (trackInfo?.trackIds) {
+            for (const trackId of trackInfo.trackIds) {
+              trackIds.push(trackId);
+            }
+          }
         }
       }
     }
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index 6055221..306c43f 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -282,8 +282,11 @@
       } else if (it.source === 'track') {
         trackId = globals.state.uiTrackIdByTraceTrackId[it.sourceId];
       } else if (it.source === 'log') {
-        const logTracks = Object.values(globals.state.tracks)
-                              .filter((t) => t.kind === 'AndroidLogTrack');
+        const logTracks =
+            Object.values(globals.state.tracks).filter((track) => {
+              const trackDesc = pluginManager.resolveTrackInfo(track.uri);
+              return (trackDesc && trackDesc.kind === 'AndroidLogTrack');
+            });
         if (logTracks.length > 0) {
           trackId = logTracks[0].id;
         }
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index b2c8ee5..e6df129 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -16,6 +16,7 @@
 import {Time, time} from '../base/time';
 import {Args, ArgValue} from '../common/arg_types';
 import {Engine} from '../common/engine';
+import {pluginManager} from '../common/plugins';
 import {
   durationFromSql,
   LONG,
@@ -308,10 +309,15 @@
     // UI track id for slice tracks this would be unnecessary.
     let trackId = '';
     for (const track of Object.values(globals.state.tracks)) {
-      if (track.kind === SLICE_TRACK_KIND &&
-          (track.config as {trackId: number}).trackId === Number(trackIdTp)) {
-        trackId = track.id;
-        break;
+      if (track.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(track.uri);
+        if (trackInfo?.kind === SLICE_TRACK_KIND) {
+          const trackIds = trackInfo?.trackIds;
+          if (trackIds && trackIds.length > 0 && trackIds[0] === trackIdTp) {
+            trackId = track.id;
+            break;
+          }
+        }
       }
     }
     return trackId;
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 31000d6..438cb1f 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -128,9 +128,7 @@
   TraceHttpStream,
   TraceStream,
 } from './trace_stream';
-import {TrackControllerArgs, trackControllerRegistry} from './track_controller';
 import {decideTracks} from './track_decider';
-import {VisualisedArgController} from './visualised_args_controller';
 
 type States = 'init' | 'loading_trace' | 'ready';
 
@@ -217,6 +215,31 @@
   });
 }
 
+// TODO(stevegolton): Move this into some global "SQL extensions" file and
+// ensure it's only run once.
+async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> {
+  await engine.query(`
+    select create_function(
+      'max_layout_depth(track_count INT, track_ids STRING)',
+      'INT',
+      '
+        select iif(
+          $track_count = 1,
+          (
+            select max(depth)
+            from slice
+            where track_id = cast($track_ids AS int)
+          ),
+          (
+            select max(layout_depth)
+            from experimental_slice_layout($track_ids)
+          )
+        );
+      '
+    );
+  `);
+}
+
 // TraceController handles handshakes with the frontend for everything that
 // concerns a single trace. It owns the WASM trace processor engine, handles
 // tracks data and SQL queries. There is one TraceController instance for each
@@ -262,21 +285,6 @@
         const engine = assertExists(this.engine);
         const childControllers: Children = [];
 
-        // Create a TrackController for each track.
-        for (const trackId of Object.keys(globals.state.tracks)) {
-          const trackCfg = globals.state.tracks[trackId];
-          if (trackCfg.engineId !== this.engineId) continue;
-          if (!trackControllerRegistry.has(trackCfg.kind)) continue;
-          const trackCtlFactory = trackControllerRegistry.get(trackCfg.kind);
-          const trackArgs: TrackControllerArgs = {trackId, engine};
-          childControllers.push(Child(trackId, trackCtlFactory, trackArgs));
-        }
-
-        for (const argName of globals.state.visualisedArgs) {
-          childControllers.push(
-            Child(argName, VisualisedArgController, {argName, engine}));
-        }
-
         const selectionArgs: SelectionControllerArgs = {engine};
         childControllers.push(
           Child('selection', SelectionController, selectionArgs));
@@ -499,6 +507,8 @@
     // Make sure the helper views are available before we start adding tracks.
     await this.initialiseHelperViews();
 
+    await defineMaxLayoutDepthSqlFunction(engine);
+
     pluginManager.onTraceLoad(engine);
 
     {
@@ -722,7 +732,7 @@
   private async listTracks() {
     this.updateStatus('Loading tracks');
     const engine = assertExists<Engine>(this.engine);
-    const actions = await decideTracks(this.engineId, engine);
+    const actions = await decideTracks(engine);
     globals.dispatchMultiple(actions);
   }
 
diff --git a/ui/src/controller/track_controller.ts b/ui/src/controller/track_controller.ts
deleted file mode 100644
index 539e561..0000000
--- a/ui/src/controller/track_controller.ts
+++ /dev/null
@@ -1,202 +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 {BigintMath} from '../base/bigint_math';
-import {assertExists} from '../base/logging';
-import {duration, time, Time, TimeSpan} from '../base/time';
-import {Engine} from '../common/engine';
-import {Registry} from '../common/registry';
-import {RESOLUTION_DEFAULT, TraceTime, TrackState} from '../common/state';
-import {LIMIT, TrackData} from '../common/track_data';
-import {globals} from '../frontend/globals';
-import {publishTrackData} from '../frontend/publish';
-
-import {Controller, ControllerFactory} from './controller';
-
-interface TrackConfig {}
-
-type TrackConfigWithNamespace = TrackConfig&{namespace: string};
-
-// TrackController is a base class overridden by track implementations (e.g.,
-// sched slices, nestable slices, counters).
-export abstract class TrackController<
-    Config extends TrackConfig, Data extends TrackData = TrackData> extends
-    Controller<'main'> {
-  readonly trackId: string;
-  readonly engine: Engine;
-  private data?: TrackData;
-  private requestingData = false;
-  private queuedRequest = false;
-  private isSetup = false;
-  private lastReloadHandled = 0;
-
-  constructor(args: TrackControllerArgs) {
-    super('main');
-    this.trackId = args.trackId;
-    this.engine = args.engine;
-  }
-
-  // Can be overriden by the track implementation to allow one time setup work
-  // to be performed before the first onBoundsChange invcation.
-  async onSetup() {}
-
-  // Can be overriden by the track implementation to allow some one-off work
-  // when requested reload (e.g. recalculating height).
-  async onReload() {}
-
-  // Must be overridden by the track implementation. Is invoked when the track
-  // frontend runs out of cached data. The derived track controller is expected
-  // to publish new track data in response to this call.
-  abstract onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data>;
-
-  get trackState(): TrackState {
-    return assertExists(globals.state.tracks[this.trackId]);
-  }
-
-  get config(): Config {
-    return this.trackState.config as Config;
-  }
-
-  configHasNamespace(config: TrackConfig): config is TrackConfigWithNamespace {
-    return 'namespace' in config;
-  }
-
-  namespaceTable(tableName: string): string {
-    if (this.configHasNamespace(this.config)) {
-      return this.config.namespace + '_' + tableName;
-    } else {
-      return tableName;
-    }
-  }
-
-  publish(data: Data): void {
-    this.data = data;
-    publishTrackData({id: this.trackId, data});
-  }
-
-  // Returns a valid SQL table name with the given prefix that should be unique
-  // for each track.
-  tableName(prefix: string) {
-    // Derive table name from, since that is unique for each track.
-    // Track ID can be UUID but '-' is not valid for sql table name.
-    const idSuffix = this.trackId.split('-').join('_');
-    return `${prefix}_${idSuffix}`;
-  }
-
-  shouldSummarize(resolution: number): boolean {
-    // |resolution| is in s/px (to nearest power of 10) assuming a display
-    // of ~1000px 0.0008 is 0.8s.
-    return resolution >= 0.0008;
-  }
-
-  protected async query(query: string) {
-    const result = await this.engine.query(query);
-    return result;
-  }
-
-  private shouldReload(): boolean {
-    const {lastTrackReloadRequest} = globals.state;
-    return !!lastTrackReloadRequest &&
-        this.lastReloadHandled < lastTrackReloadRequest;
-  }
-
-  private markReloadHandled() {
-    this.lastReloadHandled = globals.state.lastTrackReloadRequest || 0;
-  }
-
-  shouldRequestData(traceTime: TraceTime): boolean {
-    const tspan = new TimeSpan(traceTime.start, traceTime.end);
-    if (this.data === undefined) return true;
-    if (this.shouldReload()) return true;
-
-    // If at the limit only request more data if the view has moved.
-    const atLimit = this.data.length === LIMIT;
-    if (atLimit) {
-      // We request more data than the window, so add window duration to find
-      // the previous window.
-      const prevWindowStart = this.data.start + tspan.duration;
-      return tspan.start !== prevWindowStart;
-    }
-
-    // Otherwise request more data only when out of range of current data or
-    // resolution has changed.
-    const inRange =
-        tspan.start >= this.data.start && tspan.end <= this.data.end;
-    return !inRange ||
-        this.data.resolution !==
-        globals.state.frontendLocalState.visibleState.resolution;
-  }
-
-  run() {
-    const visibleState = globals.state.frontendLocalState.visibleState;
-    if (visibleState === undefined) {
-      return;
-    }
-    const visibleTimeSpan = globals.stateVisibleTime();
-    const dur = visibleTimeSpan.duration;
-    if (globals.state.visibleTracks.includes(this.trackId) &&
-        this.shouldRequestData(visibleState)) {
-      if (this.requestingData) {
-        this.queuedRequest = true;
-      } else {
-        this.requestingData = true;
-        let promise = Promise.resolve();
-        if (!this.isSetup) {
-          promise = this.onSetup();
-        } else if (this.shouldReload()) {
-          promise = this.onReload().then(() => this.markReloadHandled());
-        }
-        promise
-            .then(() => {
-              this.isSetup = true;
-              let resolution = visibleState.resolution;
-
-              // If resolution is not a power of 2, reset to the default value
-              if (BigintMath.popcount(resolution) !== 1) {
-                resolution = RESOLUTION_DEFAULT;
-              }
-
-              return this.onBoundsChange(
-                  Time.sub(visibleTimeSpan.start, dur),
-                  Time.add(visibleTimeSpan.end, dur),
-                  resolution);
-            })
-            .then((data) => {
-              this.publish(data);
-            })
-            .finally(() => {
-              this.requestingData = false;
-              if (this.queuedRequest) {
-                this.queuedRequest = false;
-                this.run();
-              }
-            });
-      }
-    }
-  }
-}
-
-export interface TrackControllerArgs {
-  trackId: string;
-  engine: Engine;
-}
-
-export interface TrackControllerFactory extends
-    ControllerFactory<TrackControllerArgs> {
-  kind: string;
-}
-
-export const trackControllerRegistry =
-    Registry.kindRegistry<TrackControllerFactory>();
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index a7cec8a..b72c20a 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -49,20 +49,12 @@
 } from '../tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track';
 import {SLICE_TRACK_KIND} from '../tracks/chrome_slices';
 import {COUNTER_TRACK_KIND} from '../tracks/counter';
-import {CPU_PROFILE_TRACK_KIND} from '../tracks/cpu_profile';
-import {
-  EXPECTED_FRAMES_SLICE_TRACK_KIND,
-} from '../tracks/expected_frames';
-import {HEAP_PROFILE_TRACK_KIND} from '../tracks/heap_profile';
-import {NULL_TRACK_KIND} from '../tracks/null_track';
-import {
-  PERF_SAMPLES_PROFILE_TRACK_KIND,
-} from '../tracks/perf_samples_profile';
+import {EXPECTED_FRAMES_SLICE_TRACK_KIND} from '../tracks/expected_frames';
+import {NULL_TRACK_URI} from '../tracks/null_track';
 import {
   decideTracks as screenshotDecideTracks,
 } from '../tracks/screenshots';
 import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
-import {THREAD_STATE_TRACK_V2_KIND} from '../tracks/thread_state_v2';
 
 const TRACKS_V2_FLAG = featureFlags.register({
   id: 'tracksV2.1',
@@ -79,11 +71,6 @@
   defaultValue: false,
 });
 
-// Special kind reserved for plugin tracks.
-// There is no significance to this value, it simply something that's unlikely
-// to be used as a key in the trackRegistry.
-const PLUGIN_TRACK_KIND = 'PLUGIN_TRACK';
-
 function showV2(): boolean {
   return TRACKS_V2_FLAG.get();
 }
@@ -124,21 +111,18 @@
 const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
 const MISC_GROUP = 'Misc Global Tracks';
 
-export async function decideTracks(
-    engineId: string, engine: Engine): Promise<DeferredAction[]> {
-  return (new TrackDecider(engineId, engine)).decideTracks();
+export async function decideTracks(engine: Engine): Promise<DeferredAction[]> {
+  return (new TrackDecider(engine)).decideTracks();
 }
 
 class TrackDecider {
-  private engineId: string;
   private engine: Engine;
   private upidToUuid = new Map<number, string>();
   private utidToUuid = new Map<number, string>();
   private tracksToAdd: AddTrackArgs[] = [];
   private addTrackGroupActions: DeferredAction[] = [];
 
-  constructor(engineId: string, engine: Engine) {
-    this.engineId = engineId;
+  constructor(engine: Engine) {
     this.engine = engine;
   }
 
@@ -174,13 +158,10 @@
       const size = cpuToSize.get(cpu);
       const name = size === undefined ? `Cpu ${cpu}` : `Cpu ${cpu} (${size})`;
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.CpuSlices#cpu${cpu}`,
         trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
         name,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {},
-        uri: `perfetto.CpuSlices#cpu${cpu}`,
       });
     }
   }
@@ -210,13 +191,10 @@
 
       if (cpuFreqIdleResult.numRows() > 0) {
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: PLUGIN_TRACK_KIND,
+          uri: `perfetto.CpuFreq#${cpu}`,
           trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
           name: `Cpu ${cpu} Frequency`,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {},
-          uri: `perfetto.CpuFreq#${cpu}`,
         });
       }
     }
@@ -263,7 +241,6 @@
       name: STR_NULL,
       parentName: STR_NULL,
       parentId: NUM_NULL,
-      trackIds: STR,
       maxDepth: NUM_NULL,
     });
 
@@ -274,8 +251,6 @@
       const rawName = it.name === null ? undefined : it.name;
       const rawParentName = it.parentName === null ? undefined : it.parentName;
       const name = getTrackName({name: rawName, kind});
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const parentTrackId = it.parentId;
       const maxDepth = it.maxDepth;
       let trackGroup = SCROLLING_TRACK_GROUP;
@@ -295,17 +270,14 @@
 
           const summaryTrackId = uuidv4();
           this.tracksToAdd.push({
+            uri: NULL_TRACK_URI,
             id: summaryTrackId,
-            engineId: this.engineId,
-            kind: NULL_TRACK_KIND,
             trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
             trackGroup: undefined,
             name: parentName,
-            config: {},
           });
 
           this.addTrackGroupActions.push(Actions.addTrackGroup({
-            engineId: this.engineId,
             summaryTrackId,
             name: parentName,
             id: trackGroup,
@@ -316,16 +288,11 @@
         }
       }
 
-      const track = {
-        engineId: this.engineId,
-        kind,
+      const track: AddTrackArgs = {
+        uri: `perfetto.AsyncSlices#${rawName}`,
         trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
         trackGroup,
         name,
-        config: {
-          maxDepth,
-          trackIds,
-        },
       };
 
       this.tracksToAdd.push(track);
@@ -345,13 +312,10 @@
     `);
       if (freqExistsResult.numRows() > 0) {
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: PLUGIN_TRACK_KIND,
+          uri: `perfetto.Counter#gpu_freq${gpu}`,
           name: `Gpu ${gpu} Frequency`,
           trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
           trackGroup: SCROLLING_TRACK_GROUP,
-          config: {},
-          uri: `perfetto.Counter#gpu_freq${gpu}`,
         });
       }
     }
@@ -395,13 +359,10 @@
       const name = it.name;
       const trackId = it.id;
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.Counter#cpu${trackId}`,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {},
-        uri: `perfetto.Counter#cpu${trackId}`,
       });
     }
   }
@@ -441,7 +402,6 @@
     }
 
     const addGroup = Actions.addTrackGroup({
-      engineId: this.engineId,
       summaryTrackId,
       name: MEM_DMA_COUNTER_NAME,
       id,
@@ -479,17 +439,14 @@
       const summaryTrackId = uuidv4();
 
       this.tracksToAdd.push({
+        uri: NULL_TRACK_URI,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: NULL_TRACK_KIND,
         trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
         name: groupName,
         trackGroup: undefined,
-        config: {},
       });
 
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId,
         name: groupName,
         id: value,
@@ -520,7 +477,6 @@
     }
 
     const addGroup = Actions.addTrackGroup({
-      engineId: this.engineId,
       summaryTrackId,
       name: group,
       id,
@@ -562,17 +518,14 @@
       const summaryTrackId = uuidv4();
 
       this.tracksToAdd.push({
+        uri: NULL_TRACK_URI,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: NULL_TRACK_KIND,
         trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
         name: groupName,
         trackGroup: undefined,
-        config: {},
       });
 
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId,
         name: groupName,
         id: value,
@@ -593,7 +546,7 @@
             track.trackGroup !== SCROLLING_TRACK_GROUP) {
           continue;
         }
-        if (track.kind === NULL_TRACK_KIND) {
+        if (track.uri === NULL_TRACK_URI) {
           continue;
         }
         if (groupUuid === undefined) {
@@ -606,17 +559,14 @@
     if (groupUuid !== undefined) {
       const summaryTrackId = uuidv4();
       this.tracksToAdd.push({
+        uri: NULL_TRACK_URI,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: NULL_TRACK_KIND,
         trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
         name: groupName,
         trackGroup: undefined,
-        config: {},
       });
 
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId,
         name: groupName,
         id: groupUuid,
@@ -641,7 +591,7 @@
           track.trackGroup !== SCROLLING_TRACK_GROUP) {
         continue;
       }
-      if (track.kind === NULL_TRACK_KIND) {
+      if (track.uri === NULL_TRACK_URI) {
         continue;
       }
       let allowlisted = false;
@@ -660,17 +610,14 @@
     if (groupUuid !== undefined) {
       const summaryTrackId = uuidv4();
       this.tracksToAdd.push({
+        uri: NULL_TRACK_URI,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: NULL_TRACK_KIND,
         trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
         name: groupName,
         trackGroup: undefined,
-        config: {},
       });
 
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId,
         name: groupName,
         id: groupUuid,
@@ -689,7 +636,7 @@
             track.trackGroup !== SCROLLING_TRACK_GROUP) {
           continue;
         }
-        if (track.kind === NULL_TRACK_KIND) {
+        if (track.uri === NULL_TRACK_URI) {
           continue;
         }
         if (groupUuid === undefined) {
@@ -702,17 +649,14 @@
     if (groupUuid !== undefined) {
       const summaryTrackId = uuidv4();
       this.tracksToAdd.push({
+        uri: NULL_TRACK_URI,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: NULL_TRACK_KIND,
         trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
         name: groupName,
         trackGroup: undefined,
-        config: {},
       });
 
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId,
         name: groupName,
         id: groupUuid,
@@ -729,13 +673,10 @@
 
     if (count > 0) {
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: 'perfetto.AndroidLog',
         name: 'Android logs',
         trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {},
-        uri: 'perfetto.AndroidLog',
       });
     }
   }
@@ -755,29 +696,23 @@
         groupUuid = 'ftrace-track-group';
         summaryTrackId = uuidv4();
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: NULL_TRACK_KIND,
+          uri: NULL_TRACK_URI,
           trackSortKey: PrimaryTrackSortKey.NULL_TRACK,
           name: `Ftrace Events`,
           trackGroup: undefined,
-          config: {},
           id: summaryTrackId,
         });
       }
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.FtraceRaw#cpu${it.cpu}`,
         trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
         name: `Ftrace Events Cpu ${it.cpu}`,
         trackGroup: groupUuid,
-        config: {},
-        uri: `perfetto.FtraceRaw#cpu${it.cpu}`,
       });
     }
 
     if (groupUuid !== undefined && summaryTrackId !== undefined) {
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         name: 'Ftrace Events',
         id: groupUuid,
         collapsed: true,
@@ -836,23 +771,16 @@
       }
 
       this.tracksToAdd.push({
+        uri: `perfetto.Annotation#${id}`,
         id: summaryTrackId,
-        engineId: this.engineId,
-        kind: SLICE_TRACK_KIND,
         name,
         trackSortKey: PrimaryTrackSortKey.ORDINARY_TRACK,
         trackGroup: trackGroupId,
-        config: {
-          maxDepth: 0,
-          namespace: 'annotation',
-          trackId: id,
-        },
       });
     }
 
     for (const [groupName, groupIds] of groupNameToIds) {
       const addGroup = Actions.addTrackGroup({
-        engineId: this.engineId,
         summaryTrackId: groupIds.summaryTrackId,
         name: groupName,
         id: groupIds.id,
@@ -876,16 +804,11 @@
       const name = counterIt.name;
       const upid = counterIt.upid;
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.Annotation#counter${id}`,
         name,
         trackSortKey: PrimaryTrackSortKey.COUNTER_TRACK,
         trackGroup: upid === 0 ? SCROLLING_TRACK_GROUP :
                                  this.upidToUuid.get(upid),
-        config: {
-          namespace: 'annotation',
-        },
-        uri: `perfetto.Annotation#counter${id}`,
       });
     }
   }
@@ -930,30 +853,26 @@
       if (showV1()) {
         const kind = THREAD_STATE_TRACK_KIND;
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: THREAD_STATE_TRACK_KIND,
+          uri: `perfetto.ThreadState#${upid}.${utid}`,
           name: getTrackName({utid, tid, threadName, kind}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
             priority,
           },
-          config: {utid, tid},
         });
       }
 
       if (showV2()) {
-        const kind = THREAD_STATE_TRACK_V2_KIND;
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind,
-          name: getTrackName({utid, tid, threadName, kind}),
+          uri: `perfetto.ThreadState#${utid}.v2`,
+          name:
+              getTrackName({utid, tid, threadName, kind: 'ThreadStateTrackV2'}),
           trackGroup: uuid,
           trackSortKey: {
             utid,
             priority,
           },
-          config: {utid, tid},
         });
       }
     }
@@ -987,15 +906,13 @@
       const threadName = it.threadName;
       const uuid = this.getUuid(utid, upid);
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: CPU_PROFILE_TRACK_KIND,
+        uri: `perfetto.CpuProfile#${utid}`,
         trackSortKey: {
           utid,
           priority: InThreadTrackSortKey.CPU_STACK_SAMPLES_TRACK,
         },
         name: `${threadName} (CPU Stack Samples)`,
         trackGroup: uuid,
-        config: {utid},
       });
     }
   }
@@ -1040,16 +957,13 @@
         threadTrack: true,
       });
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.Counter#thread${trackId}`,
         name,
         trackSortKey: {
           utid,
           priority: InThreadTrackSortKey.ORDINARY,
         },
         trackGroup: uuid,
-        config: {},
-        uri: `perfetto.Counter#thread${trackId}`,
       });
     }
   }
@@ -1091,7 +1005,6 @@
       const upid = it.upid;
       const trackName = it.trackName;
       const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const processName = it.processName;
       const pid = it.pid;
       const maxDepth = it.maxDepth;
@@ -1102,20 +1015,18 @@
       }
 
       const uuid = this.getUuid(0, upid);
-
-      const kind = ASYNC_SLICE_TRACK_KIND;
-      const name =
-          getTrackName({name: trackName, upid, pid, processName, kind});
+      const name = getTrackName({
+        name: trackName,
+        upid,
+        pid,
+        processName,
+        kind: ASYNC_SLICE_TRACK_KIND,
+      });
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind,
+        uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}`,
         name,
         trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
         trackGroup: uuid,
-        config: {
-          trackIds,
-          maxDepth,
-        },
       });
     }
   }
@@ -1146,7 +1057,6 @@
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
-      trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
       maxDepth: NUM_NULL,
@@ -1154,8 +1064,6 @@
     for (; it.valid(); it.next()) {
       const upid = it.upid;
       const trackName = it.trackName;
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const processName = it.processName;
       const pid = it.pid;
       const maxDepth = it.maxDepth;
@@ -1171,15 +1079,10 @@
       const name =
           getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind,
+        uri: `perfetto.ActualFrames#${upid}`,
         name,
         trackSortKey: PrimaryTrackSortKey.ACTUAL_FRAMES_SLICE_TRACK,
         trackGroup: uuid,
-        config: {
-          trackIds,
-          maxDepth,
-        },
       });
     }
   }
@@ -1210,7 +1113,6 @@
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
-      trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
       maxDepth: NUM_NULL,
@@ -1219,8 +1121,6 @@
     for (; it.valid(); it.next()) {
       const upid = it.upid;
       const trackName = it.trackName;
-      const rawTrackIds = it.trackIds;
-      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
       const processName = it.processName;
       const pid = it.pid;
       const maxDepth = it.maxDepth;
@@ -1236,15 +1136,10 @@
       const name =
           getTrackName({name: trackName, upid, pid, processName, kind});
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind,
+        uri: `perfetto.ExpectedFrames#${upid}`,
         name,
         trackSortKey: PrimaryTrackSortKey.EXPECTED_FRAMES_SLICE_TRACK,
         trackGroup: uuid,
-        config: {
-          trackIds,
-          maxDepth,
-        },
       });
     }
   }
@@ -1259,7 +1154,6 @@
                       'is_root_in_scope') as isDefaultTrackForScope,
           tid,
           thread.name as threadName,
-          max(slice.depth) as maxDepth,
           process.upid as upid
         from slice
         join thread_track on slice.track_id = thread_track.id
@@ -1275,7 +1169,6 @@
       isDefaultTrackForScope: NUM_NULL,
       tid: NUM_NULL,
       threadName: STR_NULL,
-      maxDepth: NUM,
       upid: NUM_NULL,
     });
     for (; it.valid(); it.next()) {
@@ -1287,7 +1180,6 @@
       const tid = it.tid;
       const threadName = it.threadName;
       const upid = it.upid;
-      const maxDepth = it.maxDepth;
 
       const uuid = this.getUuid(utid, upid);
 
@@ -1295,8 +1187,7 @@
       const name = getTrackName({name: trackName, utid, tid, threadName, kind});
       if (showV1()) {
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind,
+          uri: `perfetto.ChromeSlices#${trackId}`,
           name,
           trackGroup: uuid,
           trackSortKey: {
@@ -1305,18 +1196,12 @@
                 InThreadTrackSortKey.DEFAULT_TRACK :
                 InThreadTrackSortKey.ORDINARY,
           },
-          config: {
-            trackId,
-            maxDepth,
-            tid,
-          },
         });
       }
 
       if (showV2()) {
         this.tracksToAdd.push({
-          engineId: this.engineId,
-          kind: 'GenericSliceTrack',
+          uri: `perfetto.ChromeSlices#${trackId}.v2`,
           name,
           trackGroup: uuid,
           trackSortKey: {
@@ -1325,7 +1210,6 @@
                 InThreadTrackSortKey.DEFAULT_TRACK :
                 InThreadTrackSortKey.ORDINARY,
           },
-          config: {sqlTrackId: trackId},
         });
       }
     }
@@ -1359,14 +1243,11 @@
       const name = getTrackName(
           {name: trackName, upid, pid, kind: COUNTER_TRACK_KIND, processName});
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
+        uri: `perfetto.Counter#process${trackId}`,
         name,
         trackSortKey: await this.resolveTrackSortKeyForProcessCounterTrack(
             upid, trackName || undefined),
         trackGroup: uuid,
-        config: {},
-        uri: `perfetto.Counter#process${trackId}`,
       });
     }
   }
@@ -1381,12 +1262,10 @@
       const upid = it.upid;
       const uuid = this.getUuid(0, upid);
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: HEAP_PROFILE_TRACK_KIND,
+        uri: `perfetto.HeapProfile#${upid}`,
         trackSortKey: PrimaryTrackSortKey.HEAP_PROFILE_TRACK,
         name: `Heap Profile`,
         trackGroup: uuid,
-        config: {upid},
       });
     }
   }
@@ -1402,12 +1281,10 @@
       const pid = it.pid;
       const uuid = this.getUuid(0, upid);
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
+        uri: `perfetto.PerfSamplesProfile#${upid}`,
         trackSortKey: PrimaryTrackSortKey.PERF_SAMPLES_PROFILE_TRACK,
         name: `Callstacks ${pid}`,
         trackGroup: uuid,
-        config: {upid},
       });
     }
   }
@@ -1482,16 +1359,12 @@
     const kthreadGroupUuid = uuidv4();
     const summaryTrackId = uuidv4();
     this.tracksToAdd.push({
+      uri: 'perfetto.ProcessSummary#kernel',
       id: summaryTrackId,
-      engineId: this.engineId,
-      kind: PLUGIN_TRACK_KIND,
       trackSortKey: PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
       name: `Kernel thread summary`,
-      config: {},
-      uri: 'perfetto.ProcessSummary#kernel',
     });
     const addTrackGroup = Actions.addTrackGroup({
-      engineId: this.engineId,
       summaryTrackId,
       name: `Kernel threads`,
       id: kthreadGroupUuid,
@@ -1659,22 +1532,18 @@
         const uri = `perfetto.ProcessScheduling#${upid}.${utid}.${type}`;
 
         this.tracksToAdd.push({
+          uri,
           id: summaryTrackId,
-          engineId: this.engineId,
-          kind: PLUGIN_TRACK_KIND,
           trackSortKey: hasSched ?
               PrimaryTrackSortKey.PROCESS_SCHEDULING_TRACK :
               PrimaryTrackSortKey.PROCESS_SUMMARY_TRACK,
           name: `${upid === null ? tid : pid} summary`,
-          config: {},
           labels: it.chromeProcessLabels.split(','),
-          uri,
         });
 
         const name =
             getTrackName({utid, processName, pid, threadName, tid, upid});
         const addTrackGroup = Actions.addTrackGroup({
-          engineId: this.engineId,
           summaryTrackId,
           name,
           id: pUuid,
@@ -1716,64 +1585,29 @@
     return threadOrderingMetadata;
   }
 
-  private async defineMaxLayoutDepthSqlFunction(): Promise<void> {
-    await this.engine.query(`
-      select create_function(
-        'max_layout_depth(track_count INT, track_ids STRING)',
-        'INT',
-        '
-          select iif(
-            $track_count = 1,
-            (
-              select max(depth)
-              from slice
-              where track_id = cast($track_ids AS int)
-            ),
-            (
-              select max(layout_depth)
-              from experimental_slice_layout($track_ids)
-            )
-          );
-        '
-      );
-    `);
-  }
-
   addPluginTracks(): void {
     const tracks = pluginManager.findPotentialTracks();
     for (const info of tracks) {
       this.tracksToAdd.push({
-        engineId: this.engineId,
-        kind: PLUGIN_TRACK_KIND,
-        name: info.name,
         uri: info.uri,
+        name: info.name,
         // TODO(hjd): Fix how sorting works. Plugins should expose
         // 'sort keys' which the user can use to choose a sort order.
         trackSortKey: info.sortKey,
         trackGroup: SCROLLING_TRACK_GROUP,
-        config: {},
       });
     }
   }
 
   async addScrollJankPluginTracks(): Promise<void> {
     if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
-      const scrollJankTracksResult = await getScrollJankTracks(this.engine);
-      const tracks = scrollJankTracksResult.tracks;
-      const originalLength = this.tracksToAdd.length;
-      this.tracksToAdd.length += tracks.tracksToAdd.length;
-
-      for (let i = 0; i < tracks.tracksToAdd.length; ++i) {
-        this.tracksToAdd[i + originalLength] = tracks.tracksToAdd[i];
-      }
-
-      this.addTrackGroupActions.push(scrollJankTracksResult.addTrackGroup);
+      const result = await getScrollJankTracks(this.engine);
+      this.tracksToAdd = this.tracksToAdd.concat(result.tracks.tracksToAdd);
+      this.addTrackGroupActions.push(result.addTrackGroup);
     }
   }
 
   async decideTracks(): Promise<DeferredAction[]> {
-    await this.defineMaxLayoutDepthSqlFunction();
-
     {
       const result = screenshotDecideTracks(this.engine);
       if (result !== null) {
diff --git a/ui/src/controller/visualised_args_controller.ts b/ui/src/controller/visualised_args_controller.ts
deleted file mode 100644
index 8e709f5..0000000
--- a/ui/src/controller/visualised_args_controller.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {v4 as uuidv4} from 'uuid';
-
-import {Actions, AddTrackArgs} from '../common/actions';
-import {Engine} from '../common/engine';
-import {NUM} from '../common/query_result';
-import {InThreadTrackSortKey} from '../common/state';
-import {globals} from '../frontend/globals';
-import {
-  VISUALISED_ARGS_SLICE_TRACK_KIND,
-} from '../tracks/visualised_args/index';
-
-import {Controller} from './controller';
-
-export interface VisualisedArgControllerArgs {
-  argName: string;
-  engine: Engine;
-}
-
-export class VisualisedArgController extends Controller<'init'|'running'> {
-  private engine: Engine;
-  private argName: string;
-  private escapedArgName: string;
-  private tableName: string;
-  private addedTrackIds: string[];
-
-  constructor(args: VisualisedArgControllerArgs) {
-    super('init');
-    this.argName = args.argName;
-    this.engine = args.engine;
-    this.escapedArgName = this.argName.replace(/[^a-zA-Z]/g, '_');
-    this.tableName = `__arg_visualisation_helper_${this.escapedArgName}_slice`;
-    this.addedTrackIds = [];
-  }
-
-  onDestroy() {
-    this.engine.query(`drop table if exists ${this.tableName}`);
-    globals.dispatch(
-        Actions.removeVisualisedArgTracks({trackIds: this.addedTrackIds}));
-  }
-
-  async createTracks() {
-    const result = await this.engine.query(`
-        drop table if exists ${this.tableName};
-
-        create table ${this.tableName} as
-        with slice_with_arg as (
-          select
-            slice.id,
-            slice.track_id,
-            slice.ts,
-            slice.dur,
-            slice.thread_dur,
-            NULL as cat,
-            args.display_value as name
-          from slice
-          join args using (arg_set_id)
-          where args.key='${this.argName}'
-        )
-        select
-          *,
-          (select count()
-           from ancestor_slice(s1.id) s2
-           join slice_with_arg s3 on s2.id=s3.id
-          ) as depth
-        from slice_with_arg s1
-        order by id;
-
-        select
-          track_id as trackId,
-          max(depth) as maxDepth
-        from ${this.tableName}
-        group by track_id;
-    `);
-
-    const tracksToAdd: AddTrackArgs[] = [];
-    const it = result.iter({'trackId': NUM, 'maxDepth': NUM});
-    for (; it.valid(); it.next()) {
-      const track =
-          globals.state
-              .tracks[globals.state.uiTrackIdByTraceTrackId[it.trackId]];
-      const utid = (track.trackSortKey as {utid?: number}).utid;
-      const id = uuidv4();
-      this.addedTrackIds.push(id);
-      tracksToAdd.push({
-        id,
-        trackGroup: track.trackGroup,
-        engineId: this.engine.id,
-        kind: VISUALISED_ARGS_SLICE_TRACK_KIND,
-        name: this.argName,
-        trackSortKey: utid === undefined ?
-            track.trackSortKey :
-            {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
-        config: {
-          maxDepth: it.maxDepth,
-          namespace: `__arg_visualisation_helper_${this.escapedArgName}`,
-          trackId: it.trackId,
-          argName: this.argName,
-          tid: (track.config as {tid?: number}).tid,
-        },
-      });
-    }
-    globals.dispatch(Actions.addTracks({tracks: tracksToAdd}));
-    globals.dispatch(Actions.sortThreadTracks({}));
-  }
-
-  run() {
-    switch (this.state) {
-      case 'init':
-        this.createTracks();
-        this.setState('running');
-        break;
-      case 'running':
-        // Nothing to do here.
-        break;
-      default:
-        throw new Error(`Unexpected state ${this.state}`);
-    }
-  }
-}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 66e221c..4cefe00 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -93,7 +93,6 @@
 
   constructor(args: NewTrackArgs) {
     super(args);
-    this.frontendOnly = true;  // Disable auto checkerboarding.
     this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
   }
 
@@ -121,7 +120,7 @@
     });
   }
 
-  getContextMenu(): m.Vnode<any> {
+  getCounterContextMenu(): m.Child {
     return m(
         PopupMenu2,
         {
@@ -131,6 +130,10 @@
     );
   }
 
+  getTrackShellButtons(): m.Children {
+    return this.getCounterContextMenu();
+  }
+
   renderCanvas(ctx: CanvasRenderingContext2D) {
     const {
       visibleTimeScale: timeScale,
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index e95fb20..f932269 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -256,7 +256,6 @@
 
   constructor(args: NewTrackArgs) {
     super(args);
-    this.frontendOnly = true;  // Disable auto checkerboarding.
     // TODO(hjd): Handle pinned tracks, which current cause a crash
     // since the tableName we generate is the same for both.
     this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index c414454..48cc3b1 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -12,10 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TrackState} from 'src/common/state';
-
 import {time} from '../base/time';
 import {pluginManager} from '../common/plugins';
+import {TrackState} from '../common/state';
 
 import {TRACK_SHELL_WIDTH} from './css_constants';
 import {ALL_CATEGORIES, getFlowCategories} from './flow_events_panel';
@@ -54,14 +53,6 @@
   height: number;
 }
 
-function hasTrackId(obj: {}): obj is {trackId: number} {
-  return (obj as {trackId?: number}).trackId !== undefined;
-}
-
-function hasManyTrackIds(obj: {}): obj is {trackIds: number[]} {
-  return (obj as {trackIds?: number}).trackIds !== undefined;
-}
-
 function hasId(obj: {}): obj is {id: number} {
   return (obj as {id?: number}).id !== undefined;
 }
@@ -71,19 +62,8 @@
 }
 
 function getTrackIds(track: TrackState): number[] {
-  if (track.uri) {
-    const trackInfo = pluginManager.resolveTrackInfo(track.uri);
-    if (trackInfo?.trackIds) return trackInfo?.trackIds;
-  } else {
-    const config = track.config;
-    if (hasTrackId(config)) {
-      return [config.trackId];
-    }
-    if (hasManyTrackIds(config)) {
-      return config.trackIds;
-    }
-  }
-  return [];
+  const trackDesc = pluginManager.resolveTrackInfo(track.uri);
+  return trackDesc?.trackIds ?? [];
 }
 
 export class FlowEventsRendererArgs {
@@ -101,6 +81,17 @@
       for (const trackId of getTrackIds(track)) {
         this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
       }
+
+      // Register new "plugin track" ids
+      const trackState = globals.state.tracks[panel.attrs.id];
+      if (trackState.uri) {
+        const trackInfo = pluginManager.resolveTrackInfo(trackState.uri);
+        if (trackInfo?.trackIds) {
+          for (const trackId of trackInfo.trackIds) {
+            this.trackIdToTrackPanel.set(trackId, {panel: panel.state, yStart});
+          }
+        }
+      }
     } else if (
         panel.state instanceof TrackGroupPanel &&
         hasTrackGroupId(panel.attrs)) {
diff --git a/ui/src/frontend/post_message_handler.ts b/ui/src/frontend/post_message_handler.ts
index ed9d078..c6b5546 100644
--- a/ui/src/frontend/post_message_handler.ts
+++ b/ui/src/frontend/post_message_handler.ts
@@ -22,6 +22,8 @@
 import {showModal} from './modal';
 import {focusHorizontalRange} from './scroll_helper';
 
+const TRUSTED_ORIGINS_KEY = 'trustedOrigins';
+
 interface PostedTraceWrapped {
   perfetto: PostedTrace;
 }
@@ -40,6 +42,7 @@
   ];
   if (origin === window.origin) return true;
   if (TRUSTED_ORIGINS.includes(origin)) return true;
+  if (isUserTrustedOrigin(origin)) return true;
 
   const hostname = new URL(origin).hostname;
   if (hostname.endsWith('corp.google.com')) return true;
@@ -47,6 +50,33 @@
   return false;
 }
 
+// Returns whether the user saved this as an always-trusted origin.
+function isUserTrustedOrigin(hostname: string): boolean {
+  const trustedOrigins = window.localStorage.getItem(TRUSTED_ORIGINS_KEY);
+  if (trustedOrigins === null) return false;
+  try {
+    return JSON.parse(trustedOrigins).includes(hostname);
+  } catch {
+    return false;
+  }
+}
+
+// Saves the given hostname as a trusted origin.
+// This is used for user convenience: if it fails for any reason, it's not a
+// big deal.
+function saveUserTrustedOrigin(hostname: string) {
+  const s = window.localStorage.getItem(TRUSTED_ORIGINS_KEY);
+  let origins: string[];
+  try {
+    origins = JSON.parse(s || '[]');
+    if (origins.includes(hostname)) return;
+    origins.push(hostname);
+    window.localStorage.setItem(TRUSTED_ORIGINS_KEY, JSON.stringify(origins));
+  } catch (e) {
+    console.warn('unable to save trusted origins to localStorage', e);
+  }
+}
+
 // Returns whether we should ignore a given message based on the value of
 // the 'perfettoIgnore' field in the event data.
 function shouldGracefullyIgnoreMessage(messageEvent: MessageEvent) {
@@ -162,6 +192,11 @@
     globals.dispatch(Actions.openTraceFromBuffer(postedTrace));
   };
 
+  const trustAndOpenTrace = () => {
+    saveUserTrustedOrigin(messageEvent.origin);
+    openTrace();
+  };
+
   // If the origin is trusted open the trace directly.
   if (isTrustedOrigin(messageEvent.origin)) {
     openTrace();
@@ -176,8 +211,9 @@
           m('div', `${messageEvent.origin} is trying to open a trace file.`),
           m('div', 'Do you trust the origin and want to proceed?')),
     buttons: [
-      {text: 'NO', primary: true},
-      {text: 'YES', primary: false, action: openTrace},
+      {text: 'No', primary: true},
+      {text: 'Yes', primary: false, action: openTrace},
+      {text: 'Always trust', primary: false, action: trustAndOpenTrace},
     ],
   });
 }
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 5bb8a8c..f68b639 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -13,13 +13,20 @@
 // limitations under the License.
 
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {Icons} from '../base/semantic_icons';
 import {sqliteString} from '../base/string_utils';
 import {exists} from '../base/utils';
-import {Actions} from '../common/actions';
+import {Actions, AddTrackArgs} from '../common/actions';
 import {EngineProxy} from '../common/engine';
+import {NUM} from '../common/query_result';
+import {InThreadTrackSortKey} from '../common/state';
 import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
+import {
+  VISUALISED_ARGS_SLICE_TRACK_URI,
+  VisualisedArgsState,
+} from '../tracks/visualised_args';
 import {Anchor} from '../widgets/anchor';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
 import {Section} from '../widgets/section';
@@ -62,7 +69,7 @@
       return m(
           TreeNode,
           {
-            left: renderArgKey(stringifyKey(key), value),
+            left: renderArgKey(engine, stringifyKey(key), value),
             right: exists(value) && renderArgValue(value),
             summary: children && renderSummary(children),
           },
@@ -72,7 +79,8 @@
   });
 }
 
-function renderArgKey(key: string, value?: Arg): m.Children {
+function renderArgKey(
+    engine: EngineProxy, key: string, value?: Arg): m.Children {
   if (value === undefined) {
     return key;
   } else {
@@ -107,13 +115,84 @@
           label: 'Visualise argument values',
           icon: 'query_stats',
           onclick: () => {
-            globals.dispatch(Actions.addVisualisedArg({argName: fullKey}));
+            addVisualisedArg(engine, fullKey);
           },
         }),
     );
   }
 }
 
+async function addVisualisedArg(engine: EngineProxy, argName: string) {
+  const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
+  const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
+
+  const result = await engine.query(`
+        drop table if exists ${tableName};
+
+        create table ${tableName} as
+        with slice_with_arg as (
+          select
+            slice.id,
+            slice.track_id,
+            slice.ts,
+            slice.dur,
+            slice.thread_dur,
+            NULL as cat,
+            args.display_value as name
+          from slice
+          join args using (arg_set_id)
+          where args.key='${argName}'
+        )
+        select
+          *,
+          (select count()
+           from ancestor_slice(s1.id) s2
+           join slice_with_arg s3 on s2.id=s3.id
+          ) as depth
+        from slice_with_arg s1
+        order by id;
+
+        select
+          track_id as trackId,
+          max(depth) as maxDepth
+        from ${tableName}
+        group by track_id;
+    `);
+
+  const tracksToAdd: AddTrackArgs[] = [];
+  const it = result.iter({'trackId': NUM, 'maxDepth': NUM});
+  const addedTrackIds: string[] = [];
+  for (; it.valid(); it.next()) {
+    const track =
+        globals.state.tracks[globals.state.uiTrackIdByTraceTrackId[it.trackId]];
+    const utid = (track.trackSortKey as {utid?: number}).utid;
+    const id = uuidv4();
+    addedTrackIds.push(id);
+
+    const initialState: VisualisedArgsState = {
+      maxDepth: it.maxDepth,
+      trackId: it.trackId,
+      argName: argName,
+    };
+
+    tracksToAdd.push({
+      id,
+      trackGroup: track.trackGroup,
+      name: argName,
+      trackSortKey: utid === undefined ?
+          track.trackSortKey :
+          {utid, priority: InThreadTrackSortKey.VISUALISED_ARGS_TRACK},
+      initialState,
+      uri: VISUALISED_ARGS_SLICE_TRACK_URI,
+    });
+  }
+
+  globals.dispatchMultiple([
+    Actions.addTracks({tracks: tracksToAdd}),
+    Actions.sortThreadTracks({}),
+  ]);
+}
+
 function renderArgValue({value}: Arg): m.Children {
   if (isWebLink(value)) {
     return renderWebLink(value);
diff --git a/ui/src/frontend/slice_details_panel.ts b/ui/src/frontend/slice_details_panel.ts
index 28bd796..05de7a5 100644
--- a/ui/src/frontend/slice_details_panel.ts
+++ b/ui/src/frontend/slice_details_panel.ts
@@ -15,7 +15,9 @@
 import m from 'mithril';
 
 import {Actions} from '../common/actions';
+import {pluginManager} from '../common/plugins';
 import {translateState} from '../common/thread_state';
+import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
 import {Anchor} from '../widgets/anchor';
 import {DetailsShell} from '../widgets/details_shell';
 import {DurationWidget} from '../widgets/duration';
@@ -206,8 +208,10 @@
 
     let trackId: string|number|undefined;
     for (const track of Object.values(globals.state.tracks)) {
-      if (track.kind === 'ThreadStateTrack' &&
-          (track.config as {utid: number}).utid === threadInfo.utid) {
+      const trackDesc = pluginManager.resolveTrackInfo(track.uri);
+      // TODO(stevegolton): Handle v2.
+      if (trackDesc && trackDesc.kind === THREAD_STATE_TRACK_KIND &&
+          trackDesc.utid === threadInfo.utid) {
         trackId = track.id;
       }
     }
diff --git a/ui/src/frontend/slice_track_base.ts b/ui/src/frontend/slice_track_base.ts
new file mode 100644
index 0000000..e91d1e2
--- /dev/null
+++ b/ui/src/frontend/slice_track_base.ts
@@ -0,0 +1,339 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {duration, Span, Time, time} from '../base/time';
+import {Actions} from '../common/actions';
+import {BasicAsyncTrack} from '../common/basic_async_track';
+import {cropText, drawIncompleteSlice} from '../common/canvas_utils';
+import {
+  colorForThreadIdleSlice,
+  getColorForSlice,
+} from '../common/colorizer';
+import {HighPrecisionTime} from '../common/high_precision_time';
+import {TrackData} from '../common/track_data';
+
+import {checkerboardExcept} from './checkerboard';
+import {globals} from './globals';
+import {cachedHsluvToHex} from './hsluv_cache';
+import {PxSpan, TimeScale} from './time_scale';
+import {SliceRect} from './track';
+
+export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
+const SLICE_HEIGHT = 18;
+const TRACK_PADDING = 2;
+const CHEVRON_WIDTH_PX = 10;
+const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
+
+export interface SliceData extends TrackData {
+  // Slices are stored in a columnar fashion.
+  strings: string[];
+  sliceIds: Float64Array;
+  starts: BigInt64Array;
+  ends: BigInt64Array;
+  depths: Uint16Array;
+  titles: Uint16Array;   // Index into strings.
+  colors?: Uint16Array;  // Index into strings.
+  isInstant: Uint16Array;
+  isIncomplete: Uint16Array;
+  cpuTimeRatio?: Float64Array;
+}
+
+// Track base class which handles rendering slices in a generic way.
+// This is the old way of rendering slices - i.e. "track v1" format  - and
+// exists as a patch to allow old tracks to be converted to controller-less
+// tracks before they are ported to v2.
+// Slice tracks should extend this class and implement the abstract methods,
+// notably onBoundsChange().
+export abstract class SliceTrackBase extends BasicAsyncTrack<SliceData> {
+  constructor(
+      private maxDepth: number, protected trackInstanceId: string,
+      private tableName: string, private namespace?: string) {
+    super();
+  }
+
+  protected namespaceTable(tableName: string = this.tableName): string {
+    if (this.namespace) {
+      return this.namespace + '_' + tableName;
+    } else {
+      return tableName;
+    }
+  }
+
+  private hoveredTitleId = -1;
+
+  // Font used to render the slice name on the current track.
+  protected getFont() {
+    return '12px Roboto Condensed';
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const data = this.data;
+    if (data === undefined) return;  // Can't possibly draw anything.
+
+    const {visibleTimeSpan, visibleWindowTime, visibleTimeScale, windowSpan} =
+        globals.frontendLocalState;
+
+    // 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(),
+        visibleTimeScale.hpTimeToPx(visibleWindowTime.start),
+        visibleTimeScale.hpTimeToPx(visibleWindowTime.end),
+        visibleTimeScale.timeToPx(data.start),
+        visibleTimeScale.timeToPx(data.end),
+    );
+
+    ctx.textAlign = 'center';
+
+    // measuretext is expensive so we only use it once.
+    const charWidth = ctx.measureText('ACBDLqsdfg').width / 10;
+
+    // The draw of the rect on the selected slice must happen after the other
+    // drawings, otherwise it would result under another rect.
+    let drawRectOnSelected = () => {};
+
+
+    for (let i = 0; i < data.starts.length; i++) {
+      const tStart = Time.fromRaw(data.starts[i]);
+      let tEnd = Time.fromRaw(data.ends[i]);
+      const depth = data.depths[i];
+      const titleId = data.titles[i];
+      const sliceId = data.sliceIds[i];
+      const isInstant = data.isInstant[i];
+      const isIncomplete = data.isIncomplete[i];
+      const title = data.strings[titleId];
+      const colorOverride = data.colors && data.strings[data.colors[i]];
+      if (isIncomplete) {  // incomplete slice
+        // TODO(stevegolton): This isn't exactly equivalent, ideally we should
+        // choose tEnd once we've converted to screen space coords.
+        tEnd = visibleWindowTime.end.toTime('ceil');
+      }
+
+      if (!visibleTimeSpan.intersects(tStart, tEnd)) {
+        continue;
+      }
+
+      const rect = this.getSliceRect(
+          visibleTimeScale, visibleTimeSpan, windowSpan, tStart, tEnd, depth);
+      if (!rect || !rect.visible) {
+        continue;
+      }
+
+      const currentSelection = globals.state.currentSelection;
+      const isSelected = currentSelection &&
+          currentSelection.kind === 'CHROME_SLICE' &&
+          currentSelection.id !== undefined && currentSelection.id === sliceId;
+
+      const highlighted = titleId === this.hoveredTitleId ||
+          globals.state.highlightedSliceId === sliceId;
+
+      const hasFocus = highlighted || isSelected;
+      const colorObj = getColorForSlice(title, hasFocus);
+
+      let color: string;
+      if (colorOverride === undefined) {
+        color = colorObj.c;
+      } else {
+        color = colorOverride;
+      }
+      ctx.fillStyle = color;
+
+      // We draw instant events as upward facing chevrons starting at A:
+      //     A
+      //    ###
+      //   ##C##
+      //  ##   ##
+      // D       B
+      // Then B, C, D and back to A:
+      if (isInstant) {
+        if (isSelected) {
+          drawRectOnSelected = () => {
+            ctx.save();
+            ctx.translate(rect.left, rect.top);
+
+            // Draw a rectangle around the selected slice
+            ctx.strokeStyle = cachedHsluvToHex(colorObj.h, 100, 10);
+            ctx.beginPath();
+            ctx.lineWidth = 3;
+            ctx.strokeRect(
+                -HALF_CHEVRON_WIDTH_PX, 0, CHEVRON_WIDTH_PX, SLICE_HEIGHT);
+            ctx.closePath();
+
+            // Draw inner chevron as interior
+            ctx.fillStyle = color;
+            this.drawChevron(ctx);
+
+            ctx.restore();
+          };
+        } else {
+          ctx.save();
+          ctx.translate(rect.left, rect.top);
+          this.drawChevron(ctx);
+          ctx.restore();
+        }
+        continue;
+      }
+
+      if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
+        drawIncompleteSlice(ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT);
+      } else if (
+          data.cpuTimeRatio !== undefined && data.cpuTimeRatio[i] < 1 - 1e-9) {
+        // We draw two rectangles, representing the ratio between wall time and
+        // time spent on cpu.
+        const cpuTimeRatio = data.cpuTimeRatio![i];
+        const firstPartWidth = rect.width * cpuTimeRatio;
+        const secondPartWidth = rect.width * (1 - cpuTimeRatio);
+        ctx.fillRect(rect.left, rect.top, firstPartWidth, SLICE_HEIGHT);
+        ctx.fillStyle = colorForThreadIdleSlice(
+            colorObj.h, colorObj.s, colorObj.l, hasFocus);
+        ctx.fillRect(
+            rect.left + firstPartWidth,
+            rect.top,
+            secondPartWidth,
+            SLICE_HEIGHT);
+      } else {
+        ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
+      }
+
+      // Selected case
+      if (isSelected) {
+        drawRectOnSelected = () => {
+          ctx.strokeStyle = cachedHsluvToHex(colorObj.h, 100, 10);
+          ctx.beginPath();
+          ctx.lineWidth = 3;
+          ctx.strokeRect(
+              rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3);
+          ctx.closePath();
+        };
+      }
+
+      // Don't render text when we have less than 5px to play with.
+      if (rect.width >= 5) {
+        ctx.fillStyle = colorObj.l > 65 ? '#404040' : 'white';
+        const displayText = cropText(title, charWidth, rect.width);
+        const rectXCenter = rect.left + rect.width / 2;
+        ctx.textBaseline = 'middle';
+        ctx.font = this.getFont();
+        ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2);
+      }
+    }
+    drawRectOnSelected();
+  }
+
+  drawChevron(ctx: CanvasRenderingContext2D) {
+    // Draw a chevron at a fixed location and size. Should be used with
+    // ctx.translate and ctx.scale to alter location and size.
+    ctx.beginPath();
+    ctx.moveTo(0, 0);
+    ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
+    ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
+    ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
+    ctx.lineTo(0, 0);
+    ctx.fill();
+  }
+
+  getSliceIndex({x, y}: {x: number, y: number}): number|void {
+    const data = this.data;
+    if (data === undefined) return;
+    const {
+      visibleTimeScale: timeScale,
+      visibleWindowTime: visibleHPTimeSpan,
+    } = globals.frontendLocalState;
+    if (y < TRACK_PADDING) return;
+    const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
+    const t = timeScale.pxToHpTime(x);
+    const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT);
+
+    for (let i = 0; i < data.starts.length; i++) {
+      if (depth !== data.depths[i]) {
+        continue;
+      }
+      const start = Time.fromRaw(data.starts[i]);
+      const tStart = HighPrecisionTime.fromTime(start);
+      if (data.isInstant[i]) {
+        if (tStart.sub(t).abs().lt(instantWidthTime)) {
+          return i;
+        }
+      } else {
+        const end = Time.fromRaw(data.ends[i]);
+        let tEnd = HighPrecisionTime.fromTime(end);
+        if (data.isIncomplete[i]) {
+          tEnd = visibleHPTimeSpan.end;
+        }
+        if (tStart.lte(t) && t.lte(tEnd)) {
+          return i;
+        }
+      }
+    }
+  }
+
+  onMouseMove({x, y}: {x: number, y: number}) {
+    this.hoveredTitleId = -1;
+    globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
+    const sliceIndex = this.getSliceIndex({x, y});
+    if (sliceIndex === undefined) return;
+    const data = this.data;
+    if (data === undefined) return;
+    this.hoveredTitleId = data.titles[sliceIndex];
+    const sliceId = data.sliceIds[sliceIndex];
+    globals.dispatch(Actions.setHighlightedSliceId({sliceId}));
+  }
+
+  onMouseOut() {
+    this.hoveredTitleId = -1;
+    globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
+  }
+
+  onMouseClick({x, y}: {x: number, y: number}): boolean {
+    const sliceIndex = this.getSliceIndex({x, y});
+    if (sliceIndex === undefined) return false;
+    const data = this.data;
+    if (data === undefined) return false;
+    const sliceId = data.sliceIds[sliceIndex];
+    if (sliceId !== undefined && sliceId !== -1) {
+      globals.makeSelection(Actions.selectChromeSlice({
+        id: sliceId,
+        trackId: this.trackInstanceId,
+        table: this.namespace,
+      }));
+      return true;
+    }
+    return false;
+  }
+
+  getHeight() {
+    return SLICE_HEIGHT * (this.maxDepth + 1) + 2 * TRACK_PADDING;
+  }
+
+  getSliceRect(
+      visibleTimeScale: TimeScale, visibleWindow: Span<time, duration>,
+      windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
+      |undefined {
+    const pxEnd = windowSpan.end;
+    const left = Math.max(visibleTimeScale.timeToPx(tStart), 0);
+    const right = Math.min(visibleTimeScale.timeToPx(tEnd), pxEnd);
+
+    const visible = visibleWindow.intersects(tStart, tEnd);
+
+    return {
+      left,
+      width: Math.max(right - left, 1),
+      top: TRACK_PADDING + depth * SLICE_HEIGHT,
+      height: SLICE_HEIGHT,
+      visible,
+    };
+  }
+}
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index 80f690b..50be523 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -27,6 +27,7 @@
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../common/query_result';
 import {translateState} from '../common/thread_state';
 import {CPU_SLICE_TRACK_KIND} from '../tracks/cpu_slices';
+import {THREAD_STATE_TRACK_KIND} from '../tracks/thread_state';
 import {Anchor} from '../widgets/anchor';
 
 import {globals} from './globals';
@@ -179,8 +180,10 @@
           onclick: () => {
             let trackId: string|number|undefined;
             for (const track of Object.values(globals.state.tracks)) {
-              if (track.kind === 'ThreadStateTrack' &&
-                  (track.config as {utid: number}).utid === vnode.attrs.utid) {
+              const trackDesc = pluginManager.resolveTrackInfo(track.uri);
+              // TODO(stevegolton): Handle v2.
+              if (trackDesc && trackDesc.kind === THREAD_STATE_TRACK_KIND &&
+                  trackDesc.utid === vnode.attrs.utid) {
                 trackId = track.id;
               }
             }
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index 6ddbc51..d916e7d 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -17,14 +17,10 @@
 import {assertExists} from '../base/logging';
 import {duration, Span, time} from '../base/time';
 import {EngineProxy} from '../common/engine';
-import {TrackState} from '../common/state';
-import {TrackData} from '../common/track_data';
 import {Track} from '../public';
 
-import {checkerboard} from './checkerboard';
 import {globals} from './globals';
 import {PxSpan, TimeScale} from './time_scale';
-import {TrackButtonAttrs} from './track_panel';
 
 // Args passed to the track constructors when creating a new track.
 export interface NewTrackArgs {
@@ -54,25 +50,23 @@
 }
 
 // The abstract class that needs to be implemented by all tracks.
-export abstract class TrackBase<Config = {}, Data extends TrackData = TrackData>
-    implements Track {
+export abstract class TrackBase<Config = {}> implements Track {
   // The UI-generated track ID (not to be confused with the SQL track.id).
   protected readonly trackId: string;
   protected readonly engine: EngineProxy;
+  private _config?: Config;
 
-  // When true this is a new controller-less track type.
-  // TODO(hjd): eventually all tracks will be controller-less and this
-  // should be removed then.
-  protected frontendOnly = false;
+  get config(): Config {
+    return assertExists(this._config);
+  }
 
-  // Caches the last state.track[this.trackId]. This is to deal with track
-  // deletion, see comments in trackState() below.
-  private lastTrackState: TrackState;
+  set config(x: Config) {
+    this._config = x;
+  }
 
   constructor(args: NewTrackArgs) {
     this.trackId = args.trackId;
     this.engine = args.engine;
-    this.lastTrackState = assertExists(globals.state.tracks[this.trackId]);
   }
 
   onCreate() {}
@@ -83,44 +77,14 @@
 
   protected abstract renderCanvas(ctx: CanvasRenderingContext2D): void;
 
-  protected get trackState(): TrackState {
-    // We can end up in a state where a Track is still in the mithril renderer
-    // tree but its corresponding state has been deleted. This can happen in the
-    // interval of time between a track being removed from the state and the
-    // next animation frame that would remove the Track object. If a mouse event
-    // is dispatched in the meanwhile (or a promise is resolved), we need to be
-    // able to access the state. Hence the caching logic here.
-    const trackState = globals.state.tracks[this.trackId];
-    if (trackState === undefined) {
-      return this.lastTrackState;
-    }
-    this.lastTrackState = trackState;
-    return trackState;
-  }
-
-  get config(): Config {
-    return this.trackState.config as Config;
-  }
-
-  data(): Data|undefined {
-    if (this.frontendOnly) {
-      return undefined;
-    }
-    return globals.trackDataStore.get(this.trackId) as Data;
-  }
-
   getHeight(): number {
     return 40;
   }
 
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
+  getTrackShellButtons(): m.Children {
     return [];
   }
 
-  getContextMenu(): m.Vnode<any>|null {
-    return null;
-  }
-
   onMouseMove(_position: {x: number, y: number}) {}
 
   // Returns whether the mouse click has selected something.
@@ -134,17 +98,8 @@
   onFullRedraw(): void {}
 
   render(ctx: CanvasRenderingContext2D) {
-    globals.frontendLocalState.addVisibleTrack(this.trackState.id);
-    if (this.data() === undefined && !this.frontendOnly) {
-      const {visibleWindowTime, visibleTimeScale} = globals.frontendLocalState;
-      const startPx =
-          Math.floor(visibleTimeScale.hpTimeToPx(visibleWindowTime.start));
-      const endPx =
-          Math.ceil(visibleTimeScale.hpTimeToPx(visibleWindowTime.end));
-      checkerboard(ctx, this.getHeight(), startPx, endPx);
-    } else {
-      this.renderCanvas(ctx);
-    }
+    globals.frontendLocalState.addVisibleTrack(this.trackId);
+    this.renderCanvas(ctx);
   }
 
   // Returns a place where a given slice should be drawn. Should be implemented
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index b378189..c9e1038 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -19,7 +19,6 @@
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
 import {pluginManager} from '../common/plugins';
-import {RegistryError} from '../common/registry';
 import {
   getContainingTrackId,
   TrackGroupState,
@@ -31,7 +30,6 @@
 import {drawGridLines} from './gridline_helper';
 import {Panel, PanelSize} from './panel';
 import {renderChips, TrackContent} from './track_panel';
-import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
 } from './vertical_line_helper';
@@ -70,8 +68,7 @@
       },
     };
 
-    this.summaryTrack =
-        uri ? pluginManager.createTrack(uri, ctx) : loadTrack(trackState, id);
+    this.summaryTrack = pluginManager.createTrack(uri, ctx);
   }
 
   get trackGroupState(): TrackGroupState {
@@ -304,25 +301,3 @@
 function StripPathFromExecutable(path: string) {
   return path.split('/').slice(-1)[0];
 }
-
-function loadTrack(trackState: TrackState, trackId: string): Track|undefined {
-  const engine = globals.engines.get(trackState.engineId);
-  if (engine === undefined) {
-    return undefined;
-  }
-
-  try {
-    const trackCreator = trackRegistry.get(trackState.kind);
-    return trackCreator.create({
-      trackId,
-      engine:
-          engine.getProxy(`Track; kind: ${trackState.kind}; id: ${trackId}`),
-    });
-  } catch (e) {
-    if (e instanceof RegistryError) {
-      return undefined;
-    } else {
-      throw e;
-    }
-  }
-}
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index c280ced..5cd384b 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -18,10 +18,8 @@
 import {currentTargetOffset} from '../base/dom_utils';
 import {Icons} from '../base/semantic_icons';
 import {duration, Span, time} from '../base/time';
-import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {pluginManager} from '../common/plugins';
-import {RegistryError} from '../common/registry';
 import {TrackState} from '../common/state';
 import {raf} from '../core/raf_scheduler';
 import {Migrate, Track, TrackContext} from '../public';
@@ -33,7 +31,6 @@
 import {verticalScrollToTrack} from './scroll_helper';
 import {PxSpan, TimeScale} from './time_scale';
 import {SliceRect} from './track';
-import {trackRegistry} from './track_registry';
 import {
   drawVerticalLineAtTime,
 } from './vertical_line_helper';
@@ -79,24 +76,12 @@
   }
 }
 
-export function renderChips({uri, config}: TrackState) {
+export function renderChips({uri}: TrackState) {
   const tagElements: m.Children = [];
-  if (exists(uri)) {
-    const trackInfo = pluginManager.resolveTrackInfo(uri);
-    const tags = trackInfo?.tags;
-    tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'}));
-    tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'}));
-  } else {
-    if (config && typeof config === 'object') {
-      if ('namespace' in config) {
-        tagElements.push(m(TrackChip, {text: 'metric'}));
-      }
-      if ('isDebuggable' in config && config.isDebuggable) {
-        tagElements.push(m(TrackChip, {text: 'debuggable'}));
-      }
-    }
-  }
-
+  const trackInfo = pluginManager.resolveTrackInfo(uri);
+  const tags = trackInfo?.tags;
+  tags?.metric && tagElements.push(m(TrackChip, {text: 'metric'}));
+  tags?.debuggable && tagElements.push(m(TrackChip, {text: 'debuggable'}));
   return tagElements;
 }
 
@@ -152,7 +137,6 @@
             ),
         m('.track-buttons',
           attrs.track.getTrackShellButtons(),
-          attrs.track.getContextMenu(),
           m(TrackButton, {
             action: () => {
               globals.dispatch(
@@ -379,8 +363,7 @@
       },
     };
 
-    this.track = uri ? pluginManager.createTrack(uri, trackCtx) :
-                       loadTrack(trackState, id);
+    this.track = pluginManager.createTrack(uri, trackCtx);
 
     this.track?.onCreate();
     this.trackState = trackState;
@@ -519,25 +502,3 @@
         visibleTimeScale, visibleWindow, windowSpan, tStart, tDur, depth);
   }
 }
-
-function loadTrack(trackState: TrackState, trackId: string): Track|undefined {
-  const engine = globals.engines.get(trackState.engineId);
-  if (engine === undefined) {
-    return undefined;
-  }
-
-  try {
-    const trackCreator = trackRegistry.get(trackState.kind);
-    return trackCreator.create({
-      trackId,
-      engine:
-          engine.getProxy(`Track; kind: ${trackState.kind}; id: ${trackId}`),
-    });
-  } catch (e) {
-    if (e instanceof RegistryError) {
-      return undefined;
-    } else {
-      throw e;
-    }
-  }
-}
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 283449d..7a474a7 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -17,11 +17,9 @@
 import {Hotkey} from '../base/hotkeys';
 import {duration, Span, time} from '../base/time';
 import {EngineProxy} from '../common/engine';
-import {TrackControllerFactory} from '../controller/track_controller';
 import {Store} from '../frontend/store';
 import {PxSpan, TimeScale} from '../frontend/time_scale';
-import {SliceRect, TrackCreator} from '../frontend/track';
-import {TrackButtonAttrs} from '../frontend/track_panel';
+import {SliceRect} from '../frontend/track';
 
 export {EngineProxy} from '../common/engine';
 export {
@@ -150,22 +148,6 @@
 export interface PluginContext {
   readonly viewer: Viewer;
 
-  // DEPRECATED. In prior versions of the UI tracks were split into a
-  // 'TrackController' and a 'Track'. In more recent versions of the UI
-  // the functionality of |TrackController| has been merged into Track so
-  // |TrackController|s are not necessary in new code.
-  LEGACY_registerTrackController(track: TrackControllerFactory): void;
-
-  // Register a track factory. The core UI invokes |TrackCreator| to
-  // construct tracks discovered by invoking |TrackProvider|s.
-  // The split between 'construction' and 'discovery' allows
-  // plugins to reuse common tracks for new data. For example: the
-  // dev.perfetto.AndroidGpu plugin could register a TrackProvider
-  // which returns GPU counter tracks. The counter track factory itself
-  // could be registered in dev.perfetto.CounterTrack - a whole
-  // different plugin.
-  LEGACY_registerTrack(track: TrackCreator): void;
-
   // Add a command.
   addCommand(command: Command): void;
 }
@@ -196,8 +178,7 @@
       windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
       |undefined;
   getHeight(): number;
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>>;
-  getContextMenu(): m.Vnode<any>|null;
+  getTrackShellButtons(): m.Children;
   onMouseMove(position: {x: number, y: number}): void;
   onMouseClick(position: {x: number, y: number}): boolean;
   onMouseOut(): void;
@@ -230,6 +211,12 @@
   // Optional: The CPU number associated with this track.
   cpu?: number;
 
+  // Optional: The UTID associated with this track.
+  utid?: number;
+
+  // Optional: The UPID associated with this track.
+  upid?: number;
+
   // Optional: A list of tags used for sorting, grouping and "chips".
   tags?: TrackTags;
 }
diff --git a/ui/src/tracks/actual_frames/index.ts b/ui/src/tracks/actual_frames/index.ts
index 90de29a..e43f2b1 100644
--- a/ui/src/tracks/actual_frames/index.ts
+++ b/ui/src/tracks/actual_frames/index.ts
@@ -14,33 +14,26 @@
 
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {duration, time} from '../../base/time';
-import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
-import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
-import {ChromeSliceTrack} from '../chrome_slices';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
+import {
+  EngineProxy,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {getTrackName} from '../../public/utils';
 
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
 
-export interface Config {
-  maxDepth: number;
-  trackIds: number[];
-}
-
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  strings: string[];
-  sliceIds: Float64Array;
-  starts: BigInt64Array;
-  ends: BigInt64Array;
-  depths: Uint16Array;
-  titles: Uint16Array;   // Index in |strings|.
-  colors?: Uint16Array;  // Index in |strings|.
-  isInstant: Uint16Array;
-  isIncomplete: Uint16Array;
-}
-
 const BLUE_COLOR = '#03A9F4';         // Blue 500
 const GREEN_COLOR = '#4CAF50';        // Green 500
 const YELLOW_COLOR = '#FFEB3B';       // Yellow 500
@@ -48,55 +41,60 @@
 const LIGHT_GREEN_COLOR = '#C0D588';  // Light Green 500
 const PINK_COLOR = '#F515E0';         // Pink 500
 
-class ActualFramesSliceTrackController extends TrackController<Config, Data> {
-  static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
+class SliceTrack extends SliceTrackBase {
   private maxDur = 0n;
 
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackInstanceId: string,
+      private trackIds: number[], namespace?: string) {
+    super(maxDepth, trackInstanceId, 'actual_frame_timeline_slice', namespace);
+  }
+
   async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
+      Promise<SliceData> {
     if (this.maxDur === 0n) {
-      const maxDurResult = await this.query(`
-        select
-          max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-            as maxDur
-        from experimental_slice_layout
-        where filter_track_ids = '${this.config.trackIds.join(',')}'
-      `);
+      const maxDurResult = await this.engine.query(`
+    select
+      max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
+        as maxDur
+    from experimental_slice_layout
+    where filter_track_ids = '${this.trackIds.join(',')}'
+  `);
       this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
     }
 
-    const rawResult = await this.query(`
-      SELECT
-        (s.ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
-        s.ts as ts,
-        max(iif(s.dur = -1, (SELECT end_ts FROM trace_bounds) - s.ts, s.dur))
-            as dur,
-        s.layout_depth as layoutDepth,
-        s.name as name,
-        s.id as id,
-        s.dur = 0 as isInstant,
-        s.dur = -1 as isIncomplete,
-        CASE afs.jank_tag
-          WHEN 'Self Jank' THEN '${RED_COLOR}'
-          WHEN 'Other Jank' THEN '${YELLOW_COLOR}'
-          WHEN 'Dropped Frame' THEN '${BLUE_COLOR}'
-          WHEN 'Buffer Stuffing' THEN '${LIGHT_GREEN_COLOR}'
-          WHEN 'SurfaceFlinger Stuffing' THEN '${LIGHT_GREEN_COLOR}'
-          WHEN 'No Jank' THEN '${GREEN_COLOR}'
-          ELSE '${PINK_COLOR}'
-        END as color
-      from experimental_slice_layout s
-      join actual_frame_timeline_slice afs using(id)
-      where
-        filter_track_ids = '${this.config.trackIds.join(',')}' and
-        s.ts >= ${start - this.maxDur} and
-        s.ts <= ${end}
-      group by tsq, s.layout_depth
-      order by tsq, s.layout_depth
-    `);
+    const rawResult = await this.engine.query(`
+  SELECT
+    (s.ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
+    s.ts as ts,
+    max(iif(s.dur = -1, (SELECT end_ts FROM trace_bounds) - s.ts, s.dur))
+        as dur,
+    s.layout_depth as layoutDepth,
+    s.name as name,
+    s.id as id,
+    s.dur = 0 as isInstant,
+    s.dur = -1 as isIncomplete,
+    CASE afs.jank_tag
+      WHEN 'Self Jank' THEN '${RED_COLOR}'
+      WHEN 'Other Jank' THEN '${YELLOW_COLOR}'
+      WHEN 'Dropped Frame' THEN '${BLUE_COLOR}'
+      WHEN 'Buffer Stuffing' THEN '${LIGHT_GREEN_COLOR}'
+      WHEN 'SurfaceFlinger Stuffing' THEN '${LIGHT_GREEN_COLOR}'
+      WHEN 'No Jank' THEN '${GREEN_COLOR}'
+      ELSE '${PINK_COLOR}'
+    END as color
+  from experimental_slice_layout s
+  join actual_frame_timeline_slice afs using(id)
+  where
+    filter_track_ids = '${this.trackIds.join(',')}' and
+    s.ts >= ${start - this.maxDur} and
+    s.ts <= ${end}
+  group by tsq, s.layout_depth
+  order by tsq, s.layout_depth
+`);
 
     const numRows = rawResult.numRows();
-    const slices: Data = {
+    const slices: SliceData = {
       start,
       end,
       resolution,
@@ -154,17 +152,74 @@
   }
 }
 
-export class ActualFramesSliceTrack extends ChromeSliceTrack {
-  static readonly kind = ACTUAL_FRAMES_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): TrackBase {
-    return new ActualFramesSliceTrack(args);
-  }
-}
-
 class ActualFrames implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(ActualFramesSliceTrackController);
-    ctx.LEGACY_registerTrack(ActualFramesSliceTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace<undefined>): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      with process_async_tracks as materialized (
+        select
+          process_track.upid as upid,
+          process_track.name as trackName,
+          process.name as processName,
+          process.pid as pid,
+          group_concat(process_track.id) as trackIds,
+          count(1) as trackCount
+        from process_track
+        left join process using(upid)
+        where process_track.name = "Actual Timeline"
+        group by
+          process_track.upid,
+          process_track.name
+      )
+      select
+        t.*,
+        max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+      from process_async_tracks t;
+  `);
+
+    const it = result.iter({
+      upid: NUM,
+      trackName: STR_NULL,
+      trackIds: STR,
+      processName: STR_NULL,
+      pid: NUM_NULL,
+      maxDepth: NUM_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const upid = it.upid;
+      const trackName = it.trackName;
+      const rawTrackIds = it.trackIds;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const processName = it.processName;
+      const pid = it.pid;
+      const maxDepth = it.maxDepth;
+
+      if (maxDepth === null) {
+        // If there are no slices in this track, skip it.
+        continue;
+      }
+
+      const kind = 'ActualFrames';
+      const displayName =
+          getTrackName({name: trackName, upid, pid, processName, kind});
+
+      ctx.addTrack({
+        uri: `perfetto.ActualFrames#${upid}`,
+        displayName,
+        trackIds,
+        kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          return new SliceTrack(
+              engine,
+              maxDepth,
+              trackInstanceId,
+              trackIds,
+          );
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/annotation/index.ts b/ui/src/tracks/annotation/index.ts
index 2c8649a..c1fdfce 100644
--- a/ui/src/tracks/annotation/index.ts
+++ b/ui/src/tracks/annotation/index.ts
@@ -23,6 +23,7 @@
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
+import {ChromeSliceTrack, SLICE_TRACK_KIND} from '../chrome_slices/';
 import {
   Config as CounterTrackConfig,
   COUNTER_TRACK_KIND,
@@ -33,9 +34,48 @@
   onActivate(_ctx: PluginContext): void {}
 
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addAnnotationTracks(ctx);
     await this.addAnnotationCounterTracks(ctx);
   }
 
+  private async addAnnotationTracks(ctx: PluginContextTrace<undefined>) {
+    const {engine} = ctx;
+
+    const result = await engine.query(`
+      select id, name
+      from annotation_slice_track
+      order by name
+    `);
+
+    const it = result.iter({
+      id: NUM,
+      name: STR,
+    });
+
+    for (; it.valid(); it.next()) {
+      const id = it.id;
+      const name = it.name;
+
+      ctx.addTrack({
+        uri: `perfetto.Annotation#${id}`,
+        displayName: name,
+        kind: SLICE_TRACK_KIND,
+        tags: {
+          metric: true,
+        },
+        track: (({trackInstanceId}) => {
+          return new ChromeSliceTrack(
+              engine,
+              0,
+              trackInstanceId,
+              id,
+              'annotation',
+          );
+        }),
+      });
+    }
+  }
+
   private async addAnnotationCounterTracks(ctx: PluginContextTrace) {
     const {engine} = ctx;
     const counterResult = await engine.query(`
diff --git a/ui/src/tracks/async_slices/index.ts b/ui/src/tracks/async_slices/index.ts
index 09d0dda..01df88f 100644
--- a/ui/src/tracks/async_slices/index.ts
+++ b/ui/src/tracks/async_slices/index.ts
@@ -14,62 +14,57 @@
 
 import {BigintMath as BIMath} from '../../base/bigint_math';
 import {duration, time} from '../../base/time';
-import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
-import {TrackData} from '../../common/track_data';
 import {
-  TrackController,
-} from '../../controller/track_controller';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
-import {ChromeSliceTrack} from '../chrome_slices';
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {SliceData, SliceTrackBase} from '../../frontend/slice_track_base';
+import {
+  EngineProxy,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {getTrackName} from '../../public/utils';
 
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
 
-export interface Config {
-  maxDepth: number;
-  trackIds: number[];
-}
-
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  strings: string[];
-  sliceIds: Float64Array;
-  starts: BigInt64Array;
-  ends: BigInt64Array;
-  depths: Uint16Array;
-  titles: Uint16Array;  // Index in |strings|.
-  isInstant: Uint16Array;
-  isIncomplete: Uint16Array;
-}
-
-class AsyncSliceTrackController extends TrackController<Config, Data> {
-  static readonly kind = ASYNC_SLICE_TRACK_KIND;
+class AsyncSliceTrack extends SliceTrackBase {
   private maxDurNs: duration = 0n;
 
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackInstanceId: string,
+      private trackIds: number[], namespace?: string) {
+    // TODO is 'slice' right here?
+    super(maxDepth, trackInstanceId, 'slice', namespace);
+  }
+
   async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
+      Promise<SliceData> {
     if (this.maxDurNs === 0n) {
-      const maxDurResult = await this.query(`
-        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-        as maxDur from experimental_slice_layout
-        where filter_track_ids = '${this.config.trackIds.join(',')}'
+      const maxDurResult = await this.engine.query(`
+        select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts,
+        dur)) as maxDur from experimental_slice_layout where filter_track_ids
+        = '${this.trackIds.join(',')}'
       `);
       this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
     }
 
-    const queryRes = await this.query(`
+    const queryRes = await this.engine.query(`
       SELECT
       (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
         ts,
-        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as dur,
-        layout_depth as depth,
-        ifnull(name, '[null]') as name,
-        id,
-        dur = 0 as isInstant,
-        dur = -1 as isIncomplete
+        max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur)) as
+        dur, layout_depth as depth, ifnull(name, '[null]') as name, id, dur =
+        0 as isInstant, dur = -1 as isIncomplete
       from experimental_slice_layout
       where
-        filter_track_ids = '${this.config.trackIds.join(',')}' and
+        filter_track_ids = '${this.trackIds.join(',')}' and
         ts >= ${start - this.maxDurNs} and
         ts <= ${end}
       group by tsq, layout_depth
@@ -77,7 +72,7 @@
     `);
 
     const numRows = queryRes.numRows();
-    const slices: Data = {
+    const slices: SliceData = {
       start,
       end,
       resolution,
@@ -132,17 +127,168 @@
   }
 }
 
-export class AsyncSliceTrack extends ChromeSliceTrack {
-  static readonly kind = ASYNC_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): TrackBase {
-    return new AsyncSliceTrack(args);
-  }
-}
-
 class AsyncSlicePlugin implements Plugin {
-  onActivate(ctx: PluginContext) {
-    ctx.LEGACY_registerTrackController(AsyncSliceTrackController);
-    ctx.LEGACY_registerTrack(AsyncSliceTrack);
+  onActivate(_ctx: PluginContext) {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addGlobalAsyncTracks(ctx);
+    await this.addProcessAsyncSliceTracks(ctx);
+  }
+
+  async addGlobalAsyncTracks(ctx: PluginContextTrace): Promise<void> {
+    const {engine} = ctx;
+    const rawGlobalAsyncTracks = await engine.query(`
+      with tracks_with_slices as materialized (
+        select distinct track_id
+        from slice
+      ),
+      global_tracks as (
+        select
+          track.parent_id as parent_id,
+          track.id as track_id,
+          track.name as name
+        from track
+        join tracks_with_slices on tracks_with_slices.track_id = track.id
+        where
+          track.type = "track"
+          or track.type = "gpu_track"
+          or track.type = "cpu_track"
+      ),
+      global_tracks_grouped as (
+        select
+          parent_id,
+          name,
+          group_concat(track_id) as trackIds,
+          count(track_id) as trackCount
+        from global_tracks track
+        group by parent_id, name
+      )
+      select
+        t.parent_id as parentId,
+        p.name as parentName,
+        t.name as name,
+        t.trackIds as trackIds,
+        max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+      from global_tracks_grouped AS t
+      left join track p on (t.parent_id = p.id)
+      order by p.name, t.name;
+    `);
+    const it = rawGlobalAsyncTracks.iter({
+      name: STR_NULL,
+      parentName: STR_NULL,
+      parentId: NUM_NULL,
+      trackIds: STR,
+      maxDepth: NUM_NULL,
+    });
+
+    // let scrollJankRendered = false;
+
+    for (; it.valid(); it.next()) {
+      const rawName = it.name === null ? undefined : it.name;
+      // const rawParentName = it.parentName === null ? undefined :
+      // it.parentName;
+      const displayName = getTrackName({name: rawName, kind: 'AsyncSlice'});
+      const rawTrackIds = it.trackIds;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      // const parentTrackId = it.parentId;
+      const maxDepth = it.maxDepth;
+
+      // If there are no slices in this track, skip it.
+      if (maxDepth === null) {
+        continue;
+      }
+
+      // if (ENABLE_SCROLL_JANK_PLUGIN_V2.get() && !scrollJankRendered &&
+      //     name.includes(INPUT_LATENCY_TRACK)) {
+      //   // This ensures that the scroll jank tracks render above the tracks
+      //   // for GestureScrollUpdate.
+      //   await this.addScrollJankTracks(this.engine);
+      //   scrollJankRendered = true;
+      // }
+
+      ctx.addTrack({
+        uri: `perfetto.AsyncSlices#${rawName}`,
+        displayName,
+        trackIds,
+        kind: ASYNC_SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          return new AsyncSliceTrack(
+              engine,
+              maxDepth,
+              trackInstanceId,
+              trackIds,
+          );
+        },
+      });
+    }
+  }
+
+  async addProcessAsyncSliceTracks(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+      with process_async_tracks as materialized (
+        select
+          process_track.upid as upid,
+          process_track.name as trackName,
+          process.name as processName,
+          process.pid as pid,
+          group_concat(process_track.id) as trackIds,
+          count(1) as trackCount
+        from process_track
+        left join process using(upid)
+        where
+            process_track.name is null or
+            process_track.name not like "% Timeline"
+        group by
+          process_track.upid,
+          process_track.name
+      )
+      select
+        t.*,
+        max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+      from process_async_tracks t;
+    `);
+
+    const it = result.iter({
+      upid: NUM,
+      trackName: STR_NULL,
+      trackIds: STR,
+      processName: STR_NULL,
+      pid: NUM_NULL,
+      maxDepth: NUM_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const upid = it.upid;
+      const trackName = it.trackName;
+      const rawTrackIds = it.trackIds;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const processName = it.processName;
+      const pid = it.pid;
+      const maxDepth = it.maxDepth;
+
+      if (maxDepth === null) {
+        // If there are no slices in this track, skip it.
+        continue;
+      }
+
+      const kind = ASYNC_SLICE_TRACK_KIND;
+      const displayName =
+          getTrackName({name: trackName, upid, pid, processName, kind});
+
+      ctx.addTrack({
+        uri: `perfetto.AsyncSlices#process.${pid}${rawTrackIds}`,
+        displayName,
+        trackIds,
+        kind: ASYNC_SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          return new AsyncSliceTrack(
+              ctx.engine,
+              maxDepth,
+              trackInstanceId,
+              trackIds,
+          );
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
index 1026328..301a207 100644
--- a/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/chrome_tasks_scroll_jank_track.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 {Engine} from '../../common/engine';
 import {NUM} from '../../common/query_result';
 import {InThreadTrackSortKey} from '../../common/state';
@@ -79,20 +77,18 @@
   }
 
   result.tracksToAdd.push({
-    id: uuidv4(),
-    engineId: engine.id,
-    kind: ChromeTasksScrollJankTrack.kind,
+    uri: 'perfetto.ChromeScrollJank',
     trackSortKey: {
       utid: it.utid,
       priority: InThreadTrackSortKey.ORDINARY,
     },
     name: 'Scroll Jank causes - long tasks',
-    config: {},
     trackGroup: getTrackGroupUuid(it.utid, it.upid),
   });
 
   // Initialise the chrome_tasks_delaying_input_processing table. It will be
   // used in the sql table above.
+  // TODO(stevegolton): Use viewer.tabs.openQuery().
   await engine.query(`
 select RUN_METRIC(
    'chrome/chrome_tasks_delaying_input_processing.sql',
diff --git a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
index 42ecf77..539d782 100644
--- a/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/event_latency_track.ts
@@ -12,15 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {
   getColorForSlice,
 } from '../../common/colorizer';
-import {Engine} from '../../common/engine';
-import {
-  generateSqlWithInternalLayout,
-} from '../../common/internal_layout_utils';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
@@ -41,15 +35,18 @@
 } from './index';
 import {DEEP_RED_COLOR, RED_COLOR} from './jank_colors';
 
-const JANKY_LATENCY_NAME = 'Janky EventLatency';
+export const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
 export interface EventLatencyTrackTypes extends NamedSliceTrackTypes {
   config: {baseTable: string;}
 }
 
+const CHROME_EVENT_LATENCY_TRACK_KIND =
+    'org.chromium.ScrollJank.event_latencies';
+
 export class EventLatencyTrack extends
     CustomSqlTableSliceTrack<EventLatencyTrackTypes> {
-  static readonly kind = 'org.chromium.ScrollJank.event_latencies';
+  static readonly kind = CHROME_EVENT_LATENCY_TRACK_KIND;
 
   static create(args: NewTrackArgs): TrackBase {
     return new EventLatencyTrack(args);
@@ -117,83 +114,15 @@
   // this behavior should be customized to show jank-related data.
 }
 
-export async function addLatencyTracks(engine: Engine):
-    Promise<DecideTracksResult> {
+export async function addLatencyTracks(): Promise<DecideTracksResult> {
   const result: DecideTracksResult = {
     tracksToAdd: [],
   };
 
-  const subTableSql = generateSqlWithInternalLayout({
-    columns: ['id', 'ts', 'dur', 'track_id', 'name'],
-    sourceTable: 'slice',
-    ts: 'ts',
-    dur: 'dur',
-    whereClause: `
-      EXTRACT_ARG(arg_set_id, 'event_latency.event_type') IN (
-        'FIRST_GESTURE_SCROLL_UPDATE',
-        'GESTURE_SCROLL_UPDATE',
-        'INERTIAL_GESTURE_SCROLL_UPDATE')
-      AND HAS_DESCENDANT_SLICE_WITH_NAME(
-        id,
-        'SubmitCompositorFrameToPresentationCompositorFrame')`,
-  });
-
-  // Table name must be unique - it cannot include '-' characters or begin with
-  // a numeric value.
-  const baseTable =
-      `table_${uuidv4().split('-').join('_')}_janky_event_latencies_v3`;
-  const tableDefSql = `CREATE TABLE ${baseTable} AS
-      WITH event_latencies AS (
-        ${subTableSql}
-      ), latency_stages AS (
-      SELECT
-        d.id,
-        d.ts,
-        d.dur,
-        d.track_id,
-        d.name,
-        d.depth,
-        min(a.id) AS parent_id
-      FROM slice s
-        JOIN descendant_slice(s.id) d
-        JOIN ancestor_slice(d.id) a
-      WHERE s.id IN (SELECT id FROM event_latencies)
-      GROUP BY d.id, d.ts, d.dur, d.track_id, d.name, d.parent_id, d.depth)
-    SELECT
-      id,
-      ts,
-      dur,
-      CASE
-        WHEN id IN (
-          SELECT id FROM chrome_janky_event_latencies_v3)
-        THEN '${JANKY_LATENCY_NAME}'
-        ELSE name
-      END
-      AS name,
-      depth * 3 AS depth
-    FROM event_latencies
-    UNION ALL
-    SELECT
-      ls.id,
-      ls.ts,
-      ls.dur,
-      ls.name,
-      depth + (
-        (SELECT depth FROM event_latencies
-        WHERE id = ls.parent_id LIMIT 1) * 3) AS depth
-    FROM latency_stages ls;`;
-
-  await engine.query(
-      `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
-  await engine.query(tableDefSql);
-
   result.tracksToAdd.push({
-    id: uuidv4(),
-    engineId: engine.id,
-    kind: EventLatencyTrack.kind,
+    uri: 'perfetto.ChromeScrollJank#eventLatency',
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scroll Input Latencies',
-    config: {baseTable: baseTable},
     trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 5494921..b8c028f 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -17,25 +17,35 @@
 import {Actions, AddTrackArgs, DeferredAction} from '../../common/actions';
 import {Engine} from '../../common/engine';
 import {featureFlags} from '../../common/feature_flags';
+import {
+  generateSqlWithInternalLayout,
+} from '../../common/internal_layout_utils';
 import {ObjectById} from '../../common/state';
 import {
   Plugin,
   PluginContext,
+  PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
 } from '../../public';
 import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
-import {NULL_TRACK_KIND} from '../null_track';
+import {NULL_TRACK_URI} from '../null_track';
 
 import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
-import {addLatencyTracks, EventLatencyTrack} from './event_latency_track';
+import {
+  addLatencyTracks,
+  EventLatencyTrack,
+  JANKY_LATENCY_NAME,
+} from './event_latency_track';
 import {
   addScrollJankV3ScrollTrack,
   ScrollJankV3Track,
 } from './scroll_jank_v3_track';
-import {addTopLevelScrollTrack, TopLevelScrollTrack} from './scroll_track';
-
-export {Data} from '../chrome_slices';
+import {
+  addTopLevelScrollTrack,
+  CHROME_TOPLEVEL_SCROLLS_KIND,
+  TopLevelScrollTrack,
+} from './scroll_track';
 
 export const ENABLE_CHROME_SCROLL_JANK_PLUGIN = featureFlags.register({
   id: 'enableChromeScrollJankPlugin',
@@ -106,41 +116,31 @@
   }
 }
 
-export async function getScrollJankTracks(engine: Engine):
+export async function getScrollJankTracks(_engine: Engine):
     Promise<ScrollJankTrackGroup> {
   const result: ScrollJankTracks = {
     tracksToAdd: [],
   };
 
-  const summaryTrackId = uuidv4();
+  const scrolls = await addTopLevelScrollTrack();
+  result.tracksToAdd = result.tracksToAdd.concat(scrolls.tracksToAdd);
 
+  const janks = await addScrollJankV3ScrollTrack();
+  result.tracksToAdd = result.tracksToAdd.concat(janks.tracksToAdd);
+
+  const eventLatencies = await addLatencyTracks();
+  result.tracksToAdd = result.tracksToAdd.concat(eventLatencies.tracksToAdd);
+
+  const summaryTrackId = uuidv4();
   result.tracksToAdd.push({
-    engineId: engine.id,
-    kind: NULL_TRACK_KIND,
+    uri: NULL_TRACK_URI,
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-    name: ``,
+    name: '',  // TODO(stevegolton): We should probably put some name here.
     trackGroup: undefined,
-    config: {},
     id: summaryTrackId,
   });
 
-  const scrolls = addTopLevelScrollTrack(engine);
-  for (const scroll of (await scrolls).tracksToAdd) {
-    result.tracksToAdd.push(scroll);
-  }
-
-  const janks = addScrollJankV3ScrollTrack(engine);
-  for (const jank of (await janks).tracksToAdd) {
-    result.tracksToAdd.push(jank);
-  }
-
-  const eventLatencies = addLatencyTracks(engine);
-  for (const eventLatency of (await eventLatencies).tracksToAdd) {
-    result.tracksToAdd.push(eventLatency);
-  }
-
   const addTrackGroup = Actions.addTrackGroup({
-    engineId: engine.id,
     name: 'Chrome Scroll Jank',
     id: SCROLL_JANK_GROUP_ID,
     collapsed: false,
@@ -152,11 +152,149 @@
 }
 
 class ChromeScrollJankPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(ChromeTasksScrollJankTrack);
-    ctx.LEGACY_registerTrack(EventLatencyTrack);
-    ctx.LEGACY_registerTrack(ScrollJankV3Track);
-    ctx.LEGACY_registerTrack(TopLevelScrollTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    await this.addChromeScrollJankTrack(ctx);
+    await this.addTopLevelScrollTrack(ctx);
+    await this.addEventLatencyTrack(ctx);
+    await this.addScrollJankV3ScrollTrack(ctx);
+  }
+
+  private async addChromeScrollJankTrack(ctx: PluginContextTrace):
+      Promise<void> {
+    ctx.addTrack({
+      uri: 'perfetto.ChromeScrollJank',
+      displayName: 'Scroll Jank causes - long tasks',
+      kind: ChromeTasksScrollJankTrack.kind,
+      track: ({trackInstanceId}) => {
+        return new ChromeTasksScrollJankTrack({
+          engine: ctx.engine,
+          trackId: trackInstanceId,
+        });
+      },
+    });
+  }
+
+  private async addTopLevelScrollTrack(ctx: PluginContextTrace): Promise<void> {
+    await ctx.engine.query(`
+      INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
+      INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
+    `);
+
+    ctx.addTrack({
+      uri: 'perfetto.ChromeScrollJank#toplevelScrolls',
+      displayName: 'Chrome Scrolls',
+      kind: CHROME_TOPLEVEL_SCROLLS_KIND,
+      track: ({trackInstanceId}) => {
+        return new TopLevelScrollTrack({
+          engine: ctx.engine,
+          trackId: trackInstanceId,
+        });
+      },
+    });
+  }
+
+  private async addEventLatencyTrack(ctx: PluginContextTrace): Promise<void> {
+    const subTableSql = generateSqlWithInternalLayout({
+      columns: ['id', 'ts', 'dur', 'track_id', 'name'],
+      sourceTable: 'slice',
+      ts: 'ts',
+      dur: 'dur',
+      whereClause: `
+        EXTRACT_ARG(arg_set_id, 'event_latency.event_type') IN (
+          'FIRST_GESTURE_SCROLL_UPDATE',
+          'GESTURE_SCROLL_UPDATE',
+          'INERTIAL_GESTURE_SCROLL_UPDATE')
+        AND HAS_DESCENDANT_SLICE_WITH_NAME(
+          id,
+          'SubmitCompositorFrameToPresentationCompositorFrame')`,
+    });
+
+    // Table name must be unique - it cannot include '-' characters or begin
+    // with a numeric value.
+    const baseTable =
+        `table_${uuidv4().split('-').join('_')}_janky_event_latencies_v3`;
+    const tableDefSql = `CREATE TABLE ${baseTable} AS
+        WITH event_latencies AS (
+          ${subTableSql}
+        ), latency_stages AS (
+        SELECT
+          d.id,
+          d.ts,
+          d.dur,
+          d.track_id,
+          d.name,
+          d.depth,
+          min(a.id) AS parent_id
+        FROM slice s
+          JOIN descendant_slice(s.id) d
+          JOIN ancestor_slice(d.id) a
+        WHERE s.id IN (SELECT id FROM event_latencies)
+        GROUP BY d.id, d.ts, d.dur, d.track_id, d.name, d.parent_id, d.depth)
+      SELECT
+        id,
+        ts,
+        dur,
+        CASE
+          WHEN id IN (
+            SELECT id FROM chrome_janky_event_latencies_v3)
+          THEN '${JANKY_LATENCY_NAME}'
+          ELSE name
+        END
+        AS name,
+        depth * 3 AS depth
+      FROM event_latencies
+      UNION ALL
+      SELECT
+        ls.id,
+        ls.ts,
+        ls.dur,
+        ls.name,
+        depth + (
+          (SELECT depth FROM event_latencies
+          WHERE id = ls.parent_id LIMIT 1) * 3) AS depth
+      FROM latency_stages ls;`;
+
+    await ctx.engine.query(
+        `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
+    await ctx.engine.query(tableDefSql);
+
+    ctx.addTrack({
+      uri: 'perfetto.ChromeScrollJank#eventLatency',
+      displayName: 'Chrome Scroll Input Latencies',
+      kind: EventLatencyTrack.kind,
+      track: ({trackInstanceId}) => {
+        const track = new EventLatencyTrack({
+          engine: ctx.engine,
+          trackId: trackInstanceId,
+        });
+
+        track.config = {
+          baseTable,
+        };
+
+        return track;
+      },
+    });
+  }
+
+  private async addScrollJankV3ScrollTrack(ctx: PluginContextTrace):
+      Promise<void> {
+    await ctx.engine.query(
+        `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
+
+    ctx.addTrack({
+      uri: 'perfetto.ChromeScrollJank#scrollJankV3',
+      displayName: 'Chrome Scroll Janks',
+      kind: ScrollJankV3Track.kind,
+      track: ({trackInstanceId}) => {
+        return new ScrollJankV3Track({
+          engine: ctx.engine,
+          trackId: trackInstanceId,
+        });
+      },
+    });
   }
 }
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
index df2556c..aa32889 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -12,12 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
 import {
   getColorForSlice,
 } from '../../common/colorizer';
-import {Engine} from '../../common/engine';
 import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
@@ -37,8 +34,6 @@
 import {DEEP_RED_COLOR, RED_COLOR} from './jank_colors';
 import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
 
-export {Data} from '../chrome_slices';
-
 const UNKNOWN_SLICE_NAME = 'Unknown';
 const JANK_SLICE_NAME = ' Jank';
 
@@ -125,22 +120,16 @@
   }
 }
 
-export async function addScrollJankV3ScrollTrack(engine: Engine):
+export async function addScrollJankV3ScrollTrack():
     Promise<DecideTracksResult> {
   const result: DecideTracksResult = {
     tracksToAdd: [],
   };
 
-  await engine.query(
-      `INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_jank_intervals`);
-
   result.tracksToAdd.push({
-    id: uuidv4(),
-    engineId: engine.id,
-    kind: ScrollJankV3Track.kind,
+    uri: 'perfetto.ChromeScrollJank#scrollJankV3',
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scroll Janks',
-    config: {},
     trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index fcfa639..70c273f 100644
--- a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
@@ -12,10 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-
-import {Engine} from '../../common/engine';
-
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
 import {PrimaryTrackSortKey} from '../../public';
@@ -31,12 +27,12 @@
 } from './index';
 import {ScrollDetailsPanel} from './scroll_details_panel';
 
-export {Data} from '../chrome_slices';
+export const CHROME_TOPLEVEL_SCROLLS_KIND =
+    'org.chromium.TopLevelScrolls.scrolls';
 
 export class TopLevelScrollTrack extends
     CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
-  static readonly kind = 'org.chromium.TopLevelScrolls.scrolls';
-
+  public static kind = CHROME_TOPLEVEL_SCROLLS_KIND;
   static create(args: NewTrackArgs): TrackBase {
     return new TopLevelScrollTrack(args);
   }
@@ -76,24 +72,15 @@
   }
 }
 
-export async function addTopLevelScrollTrack(engine: Engine):
-    Promise<DecideTracksResult> {
+export async function addTopLevelScrollTrack(): Promise<DecideTracksResult> {
   const result: DecideTracksResult = {
     tracksToAdd: [],
   };
 
-  await engine.query(`
-    INCLUDE PERFETTO MODULE chrome.chrome_scrolls;
-    INCLUDE PERFETTO MODULE chrome.scroll_jank.scroll_offsets;
-  `);
-
   result.tracksToAdd.push({
-    id: uuidv4(),
-    engineId: engine.id,
-    kind: TopLevelScrollTrack.kind,
+    uri: 'perfetto.ChromeScrollJank#toplevelScrolls',
     trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
     name: 'Chrome Scrolls',
-    config: {},
     trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
diff --git a/ui/src/tracks/generic_slice_track/index.ts b/ui/src/tracks/chrome_slices/generic_slice_track.ts
similarity index 80%
rename from ui/src/tracks/generic_slice_track/index.ts
rename to ui/src/tracks/chrome_slices/generic_slice_track.ts
index 6053621..802236b 100644
--- a/ui/src/tracks/generic_slice_track/index.ts
+++ b/ui/src/tracks/chrome_slices/generic_slice_track.ts
@@ -17,7 +17,6 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export interface GenericSliceTrackConfig {
   sqlTrackId: number;
@@ -44,14 +43,3 @@
     await this.engine.query(sql);
   }
 }
-
-class GenericSliceTrackPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(GenericSliceTrack);
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.GenericSliceTrack',
-  plugin: GenericSliceTrackPlugin,
-};
diff --git a/ui/src/tracks/chrome_slices/index.ts b/ui/src/tracks/chrome_slices/index.ts
index 28efb24..1392820 100644
--- a/ui/src/tracks/chrome_slices/index.ts
+++ b/ui/src/tracks/chrome_slices/index.ts
@@ -13,63 +13,50 @@
 // limitations under the License.
 
 import {BigintMath as BIMath} from '../../base/bigint_math';
-import {duration, Span, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {cropText, drawIncompleteSlice} from '../../common/canvas_utils';
+import {Duration, duration, time} from '../../base/time';
 import {
-  colorForThreadIdleSlice,
-  getColorForSlice,
-} from '../../common/colorizer';
-import {HighPrecisionTime} from '../../common/high_precision_time';
-import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
-import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {cachedHsluvToHex} from '../../frontend/hsluv_cache';
-import {PxSpan, TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, SliceRect, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {
+  SliceData,
+  SliceTrackBase,
+} from '../../frontend/slice_track_base';
+import {
+  EngineProxy,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {getTrackName} from '../../public/utils';
+
+import {GenericSliceTrack} from './generic_slice_track';
 
 export const SLICE_TRACK_KIND = 'ChromeSliceTrack';
-const SLICE_HEIGHT = 18;
-const TRACK_PADDING = 2;
-const CHEVRON_WIDTH_PX = 10;
-const HALF_CHEVRON_WIDTH_PX = CHEVRON_WIDTH_PX / 2;
 
-export interface Config {
-  maxDepth: number;
-  namespace: string;
-  trackId: number;
-}
-
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion.
-  strings: string[];
-  sliceIds: Float64Array;
-  starts: BigInt64Array;
-  ends: BigInt64Array;
-  depths: Uint16Array;
-  titles: Uint16Array;   // Index into strings.
-  colors?: Uint16Array;  // Index into strings.
-  isInstant: Uint16Array;
-  isIncomplete: Uint16Array;
-  cpuTimeRatio?: Float64Array;
-}
-
-export class ChromeSliceTrackController extends TrackController<Config, Data> {
-  static kind = SLICE_TRACK_KIND;
+export class ChromeSliceTrack extends SliceTrackBase {
   private maxDurNs: duration = 0n;
 
+  constructor(
+      protected engine: EngineProxy, maxDepth: number, trackInstanceId: string,
+      private tpTrackId: number, namespace?: string) {
+    super(maxDepth, trackInstanceId, 'slice', namespace);
+  }
+
   async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
+      Promise<SliceData> {
     const tableName = this.namespaceTable('slice');
 
-    if (this.maxDurNs === 0n) {
+    if (this.maxDurNs === Duration.ZERO) {
       const query = `
           SELECT max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
-          AS maxDur FROM ${tableName} WHERE track_id = ${this.config.trackId}`;
-      const queryRes = await this.query(query);
+          AS maxDur FROM ${tableName} WHERE track_id = ${this.tpTrackId}`;
+      const queryRes = await this.engine.query(query);
       this.maxDurNs = queryRes.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
     }
 
@@ -85,14 +72,14 @@
         dur = -1 as isIncomplete,
         thread_dur as threadDur
       FROM ${tableName}
-      WHERE track_id = ${this.config.trackId} AND
+      WHERE track_id = ${this.tpTrackId} AND
         ts >= (${start - this.maxDurNs}) AND
         ts <= ${end}
       GROUP BY depth, tsq`;
-    const queryRes = await this.query(query);
+    const queryRes = await this.engine.query(query);
 
     const numRows = queryRes.numRows();
-    const slices: Data = {
+    const slices: SliceData = {
       start,
       end,
       resolution,
@@ -159,288 +146,87 @@
   }
 }
 
-export class ChromeSliceTrack extends TrackBase<Config, Data> {
-  static readonly kind: string = SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): TrackBase {
-    return new ChromeSliceTrack(args);
-  }
-
-  private hoveredTitleId = -1;
-
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  // Font used to render the slice name on the current track.
-  protected getFont() {
-    return '12px Roboto Condensed';
-  }
-
-  renderCanvas(ctx: CanvasRenderingContext2D): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const data = this.data();
-    if (data === undefined) return;  // Can't possibly draw anything.
-
-    const {visibleTimeSpan, visibleWindowTime, visibleTimeScale, windowSpan} =
-        globals.frontendLocalState;
-
-    // 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(),
-        visibleTimeScale.hpTimeToPx(visibleWindowTime.start),
-        visibleTimeScale.hpTimeToPx(visibleWindowTime.end),
-        visibleTimeScale.timeToPx(data.start),
-        visibleTimeScale.timeToPx(data.end),
-    );
-
-    ctx.textAlign = 'center';
-
-    // measuretext is expensive so we only use it once.
-    const charWidth = ctx.measureText('ACBDLqsdfg').width / 10;
-
-    // The draw of the rect on the selected slice must happen after the other
-    // drawings, otherwise it would result under another rect.
-    let drawRectOnSelected = () => {};
-
-
-    for (let i = 0; i < data.starts.length; i++) {
-      const tStart = Time.fromRaw(data.starts[i]);
-      let tEnd = Time.fromRaw(data.ends[i]);
-      const depth = data.depths[i];
-      const titleId = data.titles[i];
-      const sliceId = data.sliceIds[i];
-      const isInstant = data.isInstant[i];
-      const isIncomplete = data.isIncomplete[i];
-      const title = data.strings[titleId];
-      const colorOverride = data.colors && data.strings[data.colors[i]];
-      if (isIncomplete) {  // incomplete slice
-        // TODO(stevegolton): This isn't exactly equivalent, ideally we should
-        // choose tEnd once we've conerted to screen space coords.
-        tEnd = visibleWindowTime.end.toTime('ceil');
-      }
-
-      if (!visibleTimeSpan.intersects(tStart, tEnd)) {
-        continue;
-      }
-
-      const rect = this.getSliceRect(
-          visibleTimeScale, visibleTimeSpan, windowSpan, tStart, tEnd, depth);
-      if (!rect || !rect.visible) {
-        continue;
-      }
-
-      const currentSelection = globals.state.currentSelection;
-      const isSelected = currentSelection &&
-          currentSelection.kind === 'CHROME_SLICE' &&
-          currentSelection.id !== undefined && currentSelection.id === sliceId;
-
-      const highlighted = titleId === this.hoveredTitleId ||
-          globals.state.highlightedSliceId === sliceId;
-
-      const hasFocus = highlighted || isSelected;
-      const colorObj = getColorForSlice(title, hasFocus);
-
-      let color: string;
-      if (colorOverride === undefined) {
-        color = colorObj.c;
-      } else {
-        color = colorOverride;
-      }
-      ctx.fillStyle = color;
-
-      // We draw instant events as upward facing chevrons starting at A:
-      //     A
-      //    ###
-      //   ##C##
-      //  ##   ##
-      // D       B
-      // Then B, C, D and back to A:
-      if (isInstant) {
-        if (isSelected) {
-          drawRectOnSelected = () => {
-            ctx.save();
-            ctx.translate(rect.left, rect.top);
-
-            // Draw a rectangle around the selected slice
-            ctx.strokeStyle = cachedHsluvToHex(colorObj.h, 100, 10);
-            ctx.beginPath();
-            ctx.lineWidth = 3;
-            ctx.strokeRect(
-                -HALF_CHEVRON_WIDTH_PX, 0, CHEVRON_WIDTH_PX, SLICE_HEIGHT);
-            ctx.closePath();
-
-            // Draw inner chevron as interior
-            ctx.fillStyle = color;
-            this.drawChevron(ctx);
-
-            ctx.restore();
-          };
-        } else {
-          ctx.save();
-          ctx.translate(rect.left, rect.top);
-          this.drawChevron(ctx);
-          ctx.restore();
-        }
-        continue;
-      }
-
-      if (isIncomplete && rect.width > SLICE_HEIGHT / 4) {
-        drawIncompleteSlice(ctx, rect.left, rect.top, rect.width, SLICE_HEIGHT);
-      } else if (
-          data.cpuTimeRatio !== undefined && data.cpuTimeRatio[i] < 1 - 1e-9) {
-        // We draw two rectangles, representing the ratio between wall time and
-        // time spent on cpu.
-        const cpuTimeRatio = data.cpuTimeRatio![i];
-        const firstPartWidth = rect.width * cpuTimeRatio;
-        const secondPartWidth = rect.width * (1 - cpuTimeRatio);
-        ctx.fillRect(rect.left, rect.top, firstPartWidth, SLICE_HEIGHT);
-        ctx.fillStyle = colorForThreadIdleSlice(
-            colorObj.h, colorObj.s, colorObj.l, hasFocus);
-        ctx.fillRect(
-            rect.left + firstPartWidth,
-            rect.top,
-            secondPartWidth,
-            SLICE_HEIGHT);
-      } else {
-        ctx.fillRect(rect.left, rect.top, rect.width, SLICE_HEIGHT);
-      }
-
-      // Selected case
-      if (isSelected) {
-        drawRectOnSelected = () => {
-          ctx.strokeStyle = cachedHsluvToHex(colorObj.h, 100, 10);
-          ctx.beginPath();
-          ctx.lineWidth = 3;
-          ctx.strokeRect(
-              rect.left, rect.top - 1.5, rect.width, SLICE_HEIGHT + 3);
-          ctx.closePath();
-        };
-      }
-
-      // Don't render text when we have less than 5px to play with.
-      if (rect.width >= 5) {
-        ctx.fillStyle = colorObj.l > 65 ? '#404040' : 'white';
-        const displayText = cropText(title, charWidth, rect.width);
-        const rectXCenter = rect.left + rect.width / 2;
-        ctx.textBaseline = 'middle';
-        ctx.font = this.getFont();
-        ctx.fillText(displayText, rectXCenter, rect.top + SLICE_HEIGHT / 2);
-      }
-    }
-    drawRectOnSelected();
-  }
-
-  drawChevron(ctx: CanvasRenderingContext2D) {
-    // Draw a chevron at a fixed location and size. Should be used with
-    // ctx.translate and ctx.scale to alter location and size.
-    ctx.beginPath();
-    ctx.moveTo(0, 0);
-    ctx.lineTo(HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
-    ctx.lineTo(0, SLICE_HEIGHT - HALF_CHEVRON_WIDTH_PX);
-    ctx.lineTo(-HALF_CHEVRON_WIDTH_PX, SLICE_HEIGHT);
-    ctx.lineTo(0, 0);
-    ctx.fill();
-  }
-
-  getSliceIndex({x, y}: {x: number, y: number}): number|void {
-    const data = this.data();
-    if (data === undefined) return;
-    const {
-      visibleTimeScale: timeScale,
-      visibleWindowTime: visibleHPTimeSpan,
-    } = globals.frontendLocalState;
-    if (y < TRACK_PADDING) return;
-    const instantWidthTime = timeScale.pxDeltaToDuration(HALF_CHEVRON_WIDTH_PX);
-    const t = timeScale.pxToHpTime(x);
-    const depth = Math.floor((y - TRACK_PADDING) / SLICE_HEIGHT);
-
-    for (let i = 0; i < data.starts.length; i++) {
-      if (depth !== data.depths[i]) {
-        continue;
-      }
-      const start = Time.fromRaw(data.starts[i]);
-      const tStart = HighPrecisionTime.fromTime(start);
-      if (data.isInstant[i]) {
-        if (tStart.sub(t).abs().lt(instantWidthTime)) {
-          return i;
-        }
-      } else {
-        const end = Time.fromRaw(data.ends[i]);
-        let tEnd = HighPrecisionTime.fromTime(end);
-        if (data.isIncomplete[i]) {
-          tEnd = visibleHPTimeSpan.end;
-        }
-        if (tStart.lte(t) && t.lte(tEnd)) {
-          return i;
-        }
-      }
-    }
-  }
-
-  onMouseMove({x, y}: {x: number, y: number}) {
-    this.hoveredTitleId = -1;
-    globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
-    const sliceIndex = this.getSliceIndex({x, y});
-    if (sliceIndex === undefined) return;
-    const data = this.data();
-    if (data === undefined) return;
-    this.hoveredTitleId = data.titles[sliceIndex];
-    const sliceId = data.sliceIds[sliceIndex];
-    globals.dispatch(Actions.setHighlightedSliceId({sliceId}));
-  }
-
-  onMouseOut() {
-    this.hoveredTitleId = -1;
-    globals.dispatch(Actions.setHighlightedSliceId({sliceId: -1}));
-  }
-
-  onMouseClick({x, y}: {x: number, y: number}): boolean {
-    const sliceIndex = this.getSliceIndex({x, y});
-    if (sliceIndex === undefined) return false;
-    const data = this.data();
-    if (data === undefined) return false;
-    const sliceId = data.sliceIds[sliceIndex];
-    if (sliceId !== undefined && sliceId !== -1) {
-      globals.makeSelection(Actions.selectChromeSlice({
-        id: sliceId,
-        trackId: this.trackState.id,
-        table: this.config.namespace,
-      }));
-      return true;
-    }
-    return false;
-  }
-
-  getHeight() {
-    return SLICE_HEIGHT * (this.config.maxDepth + 1) + 2 * TRACK_PADDING;
-  }
-
-  getSliceRect(
-      visibleTimeScale: TimeScale, visibleWindow: Span<time, duration>,
-      windowSpan: PxSpan, tStart: time, tEnd: time, depth: number): SliceRect
-      |undefined {
-    const pxEnd = windowSpan.end;
-    const left = Math.max(visibleTimeScale.timeToPx(tStart), 0);
-    const right = Math.min(visibleTimeScale.timeToPx(tEnd), pxEnd);
-
-    const visible = visibleWindow.intersects(tStart, tEnd);
-
-    return {
-      left,
-      width: Math.max(right - left, 1),
-      top: TRACK_PADDING + depth * SLICE_HEIGHT,
-      height: SLICE_HEIGHT,
-      visible,
-    };
-  }
-}
-
 class ChromeSlicesPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(ChromeSliceTrackController);
-    ctx.LEGACY_registerTrack(ChromeSliceTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+        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,
+          thread.name as threadName,
+          max(slice.depth) as maxDepth,
+          process.upid as upid
+        from slice
+        join thread_track on slice.track_id = thread_track.id
+        join thread using(utid)
+        left join process using(upid)
+        group by thread_track.id
+  `);
+
+    const it = result.iter({
+      utid: NUM,
+      trackId: NUM,
+      trackName: STR_NULL,
+      isDefaultTrackForScope: NUM_NULL,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+      maxDepth: NUM,
+      upid: NUM_NULL,
+    });
+
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const trackId = it.trackId;
+      const trackName = it.trackName;
+      const tid = it.tid;
+      const threadName = it.threadName;
+      const maxDepth = it.maxDepth;
+
+      const displayName = getTrackName({
+        name: trackName,
+        utid,
+        tid,
+        threadName,
+        kind: 'Slices',
+      });
+
+      ctx.addTrack({
+        uri: `perfetto.ChromeSlices#${trackId}`,
+        displayName,
+        trackIds: [trackId],
+        kind: SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          return new ChromeSliceTrack(
+              engine,
+              maxDepth,
+              trackInstanceId,
+              trackId,
+          );
+        },
+      });
+
+      // trackIds can only be registered by one track at a time.
+      // TODO(hjd): Move trackIds to only be on V2.
+      ctx.addTrack({
+        uri: `perfetto.ChromeSlices#${trackId}.v2`,
+        displayName,
+        kind: SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          const track = GenericSliceTrack.create({
+            engine: ctx.engine,
+            trackId: trackInstanceId,
+          });
+          track.config = {sqlTrackId: trackId};
+          return track;
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/counter/index.ts b/ui/src/tracks/counter/index.ts
index 1fbc53f..4d22c31 100644
--- a/ui/src/tracks/counter/index.ts
+++ b/ui/src/tracks/counter/index.ts
@@ -318,7 +318,7 @@
     return MARGIN_TOP + RECT_HEIGHT;
   }
 
-  getContextMenu(): m.Vnode<any> {
+  getTrackShellButtons(): m.Children {
     const currentScale = this.store.state.scale;
     const scales: {name: CounterScaleOptions, humanName: string}[] = [
       {name: 'ZERO_BASED', humanName: 'Zero based'},
diff --git a/ui/src/tracks/cpu_profile/index.ts b/ui/src/tracks/cpu_profile/index.ts
index 616e9a2..0afbd57 100644
--- a/ui/src/tracks/cpu_profile/index.ts
+++ b/ui/src/tracks/cpu_profile/index.ts
@@ -17,16 +17,23 @@
 import {duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
 import {hslForSlice} from '../../common/colorizer';
-import {LONG, NUM} from '../../common/query_result';
-import {TrackData} from '../../common/track_data';
+import {LONG, NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
 import {
-  TrackController,
-} from '../../controller/track_controller';
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
+import {TrackData} from '../../common/track_data';
 import {globals} from '../../frontend/globals';
 import {cachedHsluvToHex} from '../../frontend/hsluv_cache';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
 const BAR_HEIGHT = 3;
 const MARGIN_TOP = 4.5;
@@ -44,8 +51,7 @@
   utid: number;
 }
 
-class CpuProfileTrackController extends TrackController<Config, Data> {
-  static readonly kind = CPU_PROFILE_TRACK_KIND;
+class CpuProfileTrackController extends TrackControllerAdapter<Config, Data> {
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
     const query = `select
@@ -85,8 +91,7 @@
   return cachedHsluvToHex(hue, saturation, lightness);
 }
 
-class CpuProfileTrack extends TrackBase<Config, Data> {
-  static readonly kind = CPU_PROFILE_TRACK_KIND;
+class CpuProfileTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): CpuProfileTrack {
     return new CpuProfileTrack(args);
   }
@@ -245,9 +250,48 @@
 }
 
 class CpuProfile implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(CpuProfileTrackController);
-    ctx.LEGACY_registerTrack(CpuProfileTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+      select
+        utid,
+        tid,
+        upid,
+        thread.name as threadName
+      from
+        thread
+        join (select utid
+            from cpu_profile_stack_sample group by utid
+        ) using(utid)
+        left join process using(upid)
+      where utid != 0
+      group by utid`);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      threadName: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const threadName = it.threadName;
+      ctx.addTrack({
+        uri: `perfetto.CpuProfile#${utid}`,
+        displayName: `${threadName} (CPU Stack Samples)`,
+        kind: CPU_PROFILE_TRACK_KIND,
+        utid,
+        track: ({trackInstanceId}) => {
+          return new TrackWithControllerAdapter(
+              ctx.engine,
+              trackInstanceId,
+              {utid},
+              CpuProfileTrack,
+              CpuProfileTrackController);
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/cpu_slices/index.ts b/ui/src/tracks/cpu_slices/index.ts
index c2de3f6..b251188 100644
--- a/ui/src/tracks/cpu_slices/index.ts
+++ b/ui/src/tracks/cpu_slices/index.ts
@@ -454,7 +454,6 @@
   }
 
   onMouseClick({x}: {x: number}) {
-    console.log(this.mousePos);
     const data = this.data();
     if (data === undefined) return false;
     const {visibleTimeScale} = globals.frontendLocalState;
diff --git a/ui/src/tracks/debug/counter_track.ts b/ui/src/tracks/debug/counter_track.ts
index 4879f27..adcc92d 100644
--- a/ui/src/tracks/debug/counter_track.ts
+++ b/ui/src/tracks/debug/counter_track.ts
@@ -16,10 +16,14 @@
 
 import {Actions, DEBUG_COUNTER_TRACK_KIND} from '../../common/actions';
 import {EngineProxy} from '../../common/engine';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {BaseCounterTrack} from '../../frontend/base_counter_track';
 import {globals} from '../../frontend/globals';
 import {NewTrackArgs} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
+import {TrackButton} from '../../frontend/track_panel';
+import {PrimaryTrackSortKey} from '../../public';
+
+import {DEBUG_COUNTER_TRACK_URI} from '.';
 
 // Names of the columns of the underlying view to be used as ts / dur / name.
 export interface CounterColumns {
@@ -44,15 +48,19 @@
     super(args);
   }
 
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
-    return [m(TrackButton, {
-      action: () => {
-        globals.dispatch(Actions.removeDebugTrack({trackId: this.trackId}));
-      },
-      i: 'close',
-      tooltip: 'Close',
-      showButton: true,
-    })];
+  getTrackShellButtons(): m.Children {
+    return [
+      this.getCounterContextMenu(),
+      m(TrackButton, {
+        action: () => {
+          globals.dispatch(
+              Actions.removeTracks({trackInstanceIds: [this.trackId]}));
+        },
+        i: 'close',
+        tooltip: 'Close',
+        showButton: true,
+      }),
+    ];
   }
 
   async initSqlTable(tableName: string): Promise<void> {
@@ -95,10 +103,12 @@
       from data
       order by ts;`);
 
-  globals.dispatch(Actions.addDebugCounterTrack({
-    engineId: engine.engineId,
+  globals.dispatch(Actions.addTrack({
+    uri: DEBUG_COUNTER_TRACK_URI,
     name: trackName.trim() || `Debug Track ${debugTrackId}`,
-    config: {
+    trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+    trackGroup: SCROLLING_TRACK_GROUP,
+    initialState: {
       sqlTableName,
       columns,
     },
diff --git a/ui/src/tracks/debug/index.ts b/ui/src/tracks/debug/index.ts
index e419c5d..1dcf0b1 100644
--- a/ui/src/tracks/debug/index.ts
+++ b/ui/src/tracks/debug/index.ts
@@ -12,15 +12,56 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {SLICE_TRACK_KIND} from '../chrome_slices';
 
-import {DebugCounterTrack} from './counter_track';
-import {DebugTrackV2} from './slice_track';
+import {CounterDebugTrackConfig, DebugCounterTrack} from './counter_track';
+import {DebugTrackV2, DebugTrackV2Config} from './slice_track';
+
+export const DEBUG_SLICE_TRACK_URI = 'perfetto.DebugSlices';
+export const DEBUG_COUNTER_TRACK_URI = 'perfetto.DebugCounter';
 
 class DebugTrackPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(DebugTrackV2);
-    ctx.LEGACY_registerTrack(DebugCounterTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    // Add debug slice track
+    ctx.addTrack({
+      displayName: '',
+      kind: SLICE_TRACK_KIND,
+      uri: DEBUG_SLICE_TRACK_URI,
+      track: (trackCtx) => {
+        const store = trackCtx.mountStore((init) => init as DebugTrackV2Config);
+        const track = new DebugTrackV2({
+          engine: ctx.engine,
+          trackId: trackCtx.trackInstanceId,
+        });
+        track.config = store.state;
+        return track;
+      },
+    });
+
+    // Add debug counter track
+    ctx.addTrack({
+      displayName: '',
+      kind: SLICE_TRACK_KIND,
+      uri: DEBUG_COUNTER_TRACK_URI,
+      track: (trackCtx) => {
+        const store =
+            trackCtx.mountStore((init) => init as CounterDebugTrackConfig);
+        const track = new DebugCounterTrack({
+          engine: ctx.engine,
+          trackId: trackCtx.trackInstanceId,
+        });
+        track.config = store.state;
+        return track;
+      },
+    });
   }
 }
 
diff --git a/ui/src/tracks/debug/slice_track.ts b/ui/src/tracks/debug/slice_track.ts
index 3b18788..9640312 100644
--- a/ui/src/tracks/debug/slice_track.ts
+++ b/ui/src/tracks/debug/slice_track.ts
@@ -16,18 +16,21 @@
 
 import {Actions, DEBUG_SLICE_TRACK_KIND} from '../../common/actions';
 import {EngineProxy} from '../../common/engine';
+import {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
+import {TrackButton} from '../../frontend/track_panel';
+import {PrimaryTrackSortKey} from '../../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
 
+import {DEBUG_SLICE_TRACK_URI} from '.';
 import {ARG_PREFIX} from './add_debug_track_menu';
 import {DebugSliceDetailsTab} from './details_tab';
 
@@ -78,15 +81,16 @@
     super.initSqlTable(tableName);
   }
 
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
-    return [m(TrackButton, {
+  getTrackShellButtons(): m.Children {
+    return m(TrackButton, {
       action: () => {
-        globals.dispatch(Actions.removeDebugTrack({trackId: this.trackId}));
+        globals.dispatch(
+            Actions.removeTracks({trackInstanceIds: [this.trackId]}));
       },
       i: 'close',
       tooltip: 'Close',
       showButton: true,
-    })];
+    });
   }
 }
 
@@ -140,10 +144,12 @@
       from prepared_data
       order by ts;`);
 
-  globals.dispatch(Actions.addDebugSliceTrack({
-    engineId: engine.engineId,
+  globals.dispatch(Actions.addTrack({
+    uri: DEBUG_SLICE_TRACK_URI,
     name: trackName.trim() || `Debug Track ${debugTrackId}`,
-    config: {
+    trackSortKey: PrimaryTrackSortKey.DEBUG_TRACK,
+    trackGroup: SCROLLING_TRACK_GROUP,
+    initialState: {
       sqlTableName,
       columns: sliceColumns,
     },
diff --git a/ui/src/tracks/expected_frames/index.ts b/ui/src/tracks/expected_frames/index.ts
index b30d31e..b25abed 100644
--- a/ui/src/tracks/expected_frames/index.ts
+++ b/ui/src/tracks/expected_frames/index.ts
@@ -12,56 +12,53 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {TrackData} from '../../common/track_data';
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {Duration, duration, time} from '../../base/time';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+  STR_NULL,
+} from '../../common/query_result';
+import {
+  SliceData,
+  SliceTrackBase,
+} from '../../frontend/slice_track_base';
+import {
+  EngineProxy,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {getTrackName} from '../../public/utils';
 
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {ChromeSliceTrack} from '../chrome_slices';
+class SliceTrack extends SliceTrackBase {
+  private maxDur = Duration.ZERO;
 
-import {LONG, LONG_NULL, NUM, STR} from '../../common/query_result';
-import {duration, time} from '../../base/time';
-import {
-  TrackController,
-} from '../../controller/track_controller';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
-import {BigintMath as BIMath} from '../../base/bigint_math';
-
-export interface Config {
-  maxDepth: number;
-  trackIds: number[];
-}
-
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  strings: string[];
-  sliceIds: Float64Array;
-  starts: BigInt64Array;
-  ends: BigInt64Array;
-  depths: Uint16Array;
-  titles: Uint16Array;   // Index in |strings|.
-  colors?: Uint16Array;  // Index in |strings|.
-  isInstant: Uint16Array;
-  isIncomplete: Uint16Array;
-}
-
-class ExpectedFramesSliceTrackController extends TrackController<Config, Data> {
-  static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
-  private maxDurNs: duration = 0n;
+  constructor(
+      private engine: EngineProxy, maxDepth: number, trackInstanceId: string,
+      private trackIds: number[], namespace?: string) {
+    super(maxDepth, trackInstanceId, '', namespace);
+  }
 
   async onBoundsChange(start: time, end: time, resolution: duration):
-      Promise<Data> {
-    if (this.maxDurNs === 0n) {
-      const maxDurResult = await this.query(`
+      Promise<SliceData> {
+    if (this.maxDur === Duration.ZERO) {
+      const maxDurResult = await this.engine.query(`
         select max(iif(dur = -1, (SELECT end_ts FROM trace_bounds) - ts, dur))
           as maxDur
         from experimental_slice_layout
-        where filter_track_ids = '${this.config.trackIds.join(',')}'
+        where filter_track_ids = '${this.trackIds.join(',')}'
       `);
-      this.maxDurNs = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
+      this.maxDur = maxDurResult.firstRow({maxDur: LONG_NULL}).maxDur || 0n;
     }
 
-    const queryRes = await this.query(`
+    const queryRes = await this.engine.query(`
       SELECT
         (ts + ${resolution / 2n}) / ${resolution} * ${resolution} as tsq,
         ts,
@@ -73,15 +70,15 @@
         dur = -1 as isIncomplete
       from experimental_slice_layout
       where
-        filter_track_ids = '${this.config.trackIds.join(',')}' and
-        ts >= ${start - this.maxDurNs} and
+        filter_track_ids = '${this.trackIds.join(',')}' and
+        ts >= ${start - this.maxDur} and
         ts <= ${end}
       group by tsq, layout_depth
       order by tsq, layout_depth
     `);
 
     const numRows = queryRes.numRows();
-    const slices: Data = {
+    const slices: SliceData = {
       start,
       end,
       resolution,
@@ -139,18 +136,74 @@
   }
 }
 
-
-export class ExpectedFramesSliceTrack extends ChromeSliceTrack {
-  static readonly kind = EXPECTED_FRAMES_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): TrackBase {
-    return new ExpectedFramesSliceTrack(args);
-  }
-}
-
 class ExpectedFramesPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(ExpectedFramesSliceTrackController);
-    ctx.LEGACY_registerTrack(ExpectedFramesSliceTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      with process_async_tracks as materialized (
+        select
+          process_track.upid as upid,
+          process_track.name as trackName,
+          process.name as processName,
+          process.pid as pid,
+          group_concat(process_track.id) as trackIds,
+          count(1) as trackCount
+        from process_track
+        left join process using(upid)
+        where process_track.name = "Expected Timeline"
+        group by
+          process_track.upid,
+          process_track.name
+      )
+      select
+        t.*,
+        max_layout_depth(t.trackCount, t.trackIds) as maxDepth
+      from process_async_tracks t;
+  `);
+
+    const it = result.iter({
+      upid: NUM,
+      trackName: STR_NULL,
+      trackIds: STR,
+      processName: STR_NULL,
+      pid: NUM_NULL,
+      maxDepth: NUM_NULL,
+    });
+
+    for (; it.valid(); it.next()) {
+      const upid = it.upid;
+      const trackName = it.trackName;
+      const rawTrackIds = it.trackIds;
+      const trackIds = rawTrackIds.split(',').map((v) => Number(v));
+      const processName = it.processName;
+      const pid = it.pid;
+      const maxDepth = it.maxDepth;
+
+      if (maxDepth === null) {
+        // If there are no slices in this track, skip it.
+        continue;
+      }
+
+      const displayName = getTrackName(
+          {name: trackName, upid, pid, processName, kind: 'ExpectedFrames'});
+
+      ctx.addTrack({
+        uri: `perfetto.ExpectedFrames#${upid}`,
+        displayName,
+        trackIds,
+        kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
+        track: ({trackInstanceId}) => {
+          return new SliceTrack(
+              engine,
+              maxDepth,
+              trackInstanceId,
+              trackIds,
+          );
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/heap_profile/index.ts b/ui/src/tracks/heap_profile/index.ts
index 38b19e7..d7257dd 100644
--- a/ui/src/tracks/heap_profile/index.ts
+++ b/ui/src/tracks/heap_profile/index.ts
@@ -15,16 +15,25 @@
 import {searchSegment} from '../../base/binary_search';
 import {duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {LONG, STR} from '../../common/query_result';
+import {LONG, NUM, STR} from '../../common/query_result';
 import {ProfileType} from '../../common/state';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
 import {profileType} from '../../controller/flamegraph_controller';
-import {TrackController} from '../../controller/track_controller';
 import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {globals} from '../../frontend/globals';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
 export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
 
@@ -37,8 +46,7 @@
   upid: number;
 }
 
-class HeapProfileTrackController extends TrackController<Config, Data> {
-  static readonly kind = HEAP_PROFILE_TRACK_KIND;
+class HeapProfileTrackController extends TrackControllerAdapter<Config, Data> {
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
     if (this.config.upid === undefined) {
@@ -88,8 +96,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 30.5;
 
-class HeapProfileTrack extends TrackBase<Config, Data> {
-  static readonly kind = HEAP_PROFILE_TRACK_KIND;
+class HeapProfileTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): HeapProfileTrack {
     return new HeapProfileTrack(args);
   }
@@ -216,9 +223,30 @@
 }
 
 class HeapProfilePlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(HeapProfileTrackController);
-    ctx.LEGACY_registerTrack(HeapProfileTrack);
+  onActivate(_ctx: PluginContext): void {}
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+    select distinct(upid) from heap_profile_allocation
+    union
+    select distinct(upid) from heap_graph_object
+  `);
+    for (const it = result.iter({upid: NUM}); it.valid(); it.next()) {
+      const upid = it.upid;
+      ctx.addTrack({
+        uri: `perfetto.HeapProfile#${upid}`,
+        displayName: 'Heap Profile',
+        kind: HEAP_PROFILE_TRACK_KIND,
+        upid,
+        track: ({trackInstanceId}) => {
+          return new TrackWithControllerAdapter(
+              ctx.engine,
+              trackInstanceId,
+              {upid},
+              HeapProfileTrack,
+              HeapProfileTrackController);
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/null_track/index.ts b/ui/src/tracks/null_track/index.ts
index a007185..ffd84aa 100644
--- a/ui/src/tracks/null_track/index.ts
+++ b/ui/src/tracks/null_track/index.ts
@@ -13,15 +13,19 @@
 // limitations under the License.
 
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
+export const NULL_TRACK_URI = 'perfetto.NullTrack';
 export const NULL_TRACK_KIND = 'NullTrack';
 
 export class NullTrack extends TrackBase {
-  static readonly kind = NULL_TRACK_KIND;
   constructor(args: NewTrackArgs) {
     super(args);
-    this.frontendOnly = true;
   }
 
   static create(args: NewTrackArgs): NullTrack {
@@ -36,8 +40,21 @@
 }
 
 class NullTrackPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(NullTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace<undefined>): Promise<void> {
+    // TODO(stevegolton): This is not the right way to handle blank tracks,
+    // instead we should probably just render some blank element at render time
+    // if no track uri is supplied.
+    ctx.addTrack({
+      uri: NULL_TRACK_URI,
+      displayName: 'Null Track',
+      kind: NULL_TRACK_KIND,
+      track: ({trackInstanceId}) => NullTrack.create({
+        engine: ctx.engine,
+        trackId: trackInstanceId,
+      }),
+    });
   }
 }
 
diff --git a/ui/src/tracks/perf_samples_profile/index.ts b/ui/src/tracks/perf_samples_profile/index.ts
index b6e624a..1d8b7b1 100644
--- a/ui/src/tracks/perf_samples_profile/index.ts
+++ b/ui/src/tracks/perf_samples_profile/index.ts
@@ -15,15 +15,24 @@
 import {searchSegment} from '../../base/binary_search';
 import {duration, Time, time} from '../../base/time';
 import {Actions} from '../../common/actions';
-import {LONG} from '../../common/query_result';
+import {LONG, NUM} from '../../common/query_result';
 import {ProfileType} from '../../common/state';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {globals} from '../../frontend/globals';
 import {TimeScale} from '../../frontend/time_scale';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
 
 export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
 
@@ -35,8 +44,8 @@
   upid: number;
 }
 
-class PerfSamplesProfileTrackController extends TrackController<Config, Data> {
-  static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND;
+class PerfSamplesProfileTrackController extends
+    TrackControllerAdapter<Config, Data> {
   async onBoundsChange(start: time, end: time, resolution: duration):
       Promise<Data> {
     if (this.config.upid === undefined) {
@@ -77,8 +86,7 @@
 const MARGIN_TOP = 4.5;
 const RECT_HEIGHT = 30.5;
 
-class PerfSamplesProfileTrack extends TrackBase<Config, Data> {
-  static readonly kind = PERF_SAMPLES_PROFILE_TRACK_KIND;
+class PerfSamplesProfileTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): PerfSamplesProfileTrack {
     return new PerfSamplesProfileTrack(args);
   }
@@ -209,9 +217,32 @@
 }
 
 class PerfSamplesProfilePlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(PerfSamplesProfileTrackController);
-    ctx.LEGACY_registerTrack(PerfSamplesProfileTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    const result = await ctx.engine.query(`
+      select distinct upid, pid
+      from perf_sample join thread using (utid) join process using (upid)
+      where callsite_id is not null
+  `);
+    for (const it = result.iter({upid: NUM, pid: NUM}); it.valid(); it.next()) {
+      const upid = it.upid;
+      const pid = it.pid;
+      ctx.addTrack({
+        uri: `perfetto.PerfSamplesProfile#${upid}`,
+        displayName: `Callstacks ${pid}`,
+        kind: PERF_SAMPLES_PROFILE_TRACK_KIND,
+        upid,
+        track: ({trackInstanceId}) => {
+          return new TrackWithControllerAdapter(
+              ctx.engine,
+              trackInstanceId,
+              {upid},
+              PerfSamplesProfileTrack,
+              PerfSamplesProfileTrackController);
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/screenshots/index.ts b/ui/src/tracks/screenshots/index.ts
index cc85c16..3e1d8c2 100644
--- a/ui/src/tracks/screenshots/index.ts
+++ b/ui/src/tracks/screenshots/index.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 {AddTrackArgs} from '../../common/actions';
 import {Engine} from '../../common/engine';
 import {
@@ -23,6 +21,7 @@
 import {
   Plugin,
   PluginContext,
+  PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
 } from '../../public';
@@ -36,8 +35,6 @@
   ScreenshotTab,
 } from './screenshot_panel';
 
-export {Data} from '../chrome_slices';
-
 class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
   static readonly kind = 'dev.perfetto.ScreenshotsTrack';
   static create(args: NewTrackArgs): TrackBase {
@@ -66,6 +63,7 @@
   tracksToAdd: AddTrackArgs[],
 };
 
+// TODO(stevegolton): Use suggestTrack().
 export async function decideTracks(engine: Engine):
     Promise<DecideTracksResult> {
   const result: DecideTracksResult = {
@@ -75,20 +73,28 @@
   await engine.query(`INCLUDE PERFETTO MODULE android.screenshots`);
 
   result.tracksToAdd.push({
-    id: uuidv4(),
-    engineId: engine.id,
-    kind: ScreenshotsTrack.kind,
-    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+    uri: 'perfetto.Screenshots',
     name: 'Screenshots',
-    config: {},
-    trackGroup: undefined,
+    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
   });
   return result;
 }
 
 class ScreenshotsPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(ScreenshotsTrack);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace<undefined>): Promise<void> {
+    ctx.addTrack({
+      uri: 'perfetto.Screenshots',
+      displayName: 'Screenshots',
+      kind: ScreenshotsTrack.kind,
+      track: ({trackInstanceId}) => {
+        return new ScreenshotsTrack({
+          engine: ctx.engine,
+          trackId: trackInstanceId,
+        });
+      },
+    });
   }
 }
 
diff --git a/ui/src/tracks/thread_state/index.ts b/ui/src/tracks/thread_state/index.ts
index 807f8f9..a2fad70 100644
--- a/ui/src/tracks/thread_state/index.ts
+++ b/ui/src/tracks/thread_state/index.ts
@@ -21,17 +21,31 @@
 import {colorForState} from '../../common/colorizer';
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../../common/query_result';
 import {translateState} from '../../common/thread_state';
+import {
+  TrackAdapter,
+  TrackControllerAdapter,
+  TrackWithControllerAdapter,
+} from '../../common/track_adapter';
 import {TrackData} from '../../common/track_data';
-import {TrackController} from '../../controller/track_controller';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {NewTrackArgs} from '../../frontend/track';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {getTrackName} from '../../public/utils';
 
+import {
+  ThreadStateTrack as ThreadStateTrackV2,
+} from './thread_state_v2';
 
 export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
+export const THREAD_STATE_TRACK_V2_KIND = 'ThreadStateTrackV2';
 
-export interface Data extends TrackData {
+interface Data extends TrackData {
   strings: string[];
   ids: Float64Array;
   starts: BigInt64Array;
@@ -40,13 +54,11 @@
   state: Uint16Array;  // Index into |strings|.
 }
 
-export interface Config {
+interface Config {
   utid: number;
 }
 
-class ThreadStateTrackController extends TrackController<Config, Data> {
-  static readonly kind = THREAD_STATE_TRACK_KIND;
-
+class ThreadStateTrackController extends TrackControllerAdapter<Config, Data> {
   private maxDurNs: duration = 0n;
 
   async onSetup() {
@@ -162,8 +174,7 @@
 const RECT_HEIGHT = 12;
 const EXCESS_WIDTH = 10;
 
-class ThreadStateTrack extends TrackBase<Config, Data> {
-  static readonly kind = THREAD_STATE_TRACK_KIND;
+class ThreadStateTrack extends TrackAdapter<Config, Data> {
   static create(args: NewTrackArgs): ThreadStateTrack {
     return new ThreadStateTrack(args);
   }
@@ -277,16 +288,76 @@
     const index = search(data.starts, time.toTime());
     if (index === -1) return false;
     const id = data.ids[index];
-    globals.makeSelection(
-        Actions.selectThreadState({id, trackId: this.trackState.id}));
+    globals.makeSelection(Actions.selectThreadState({id, trackId: this.id}));
     return true;
   }
 }
 
+
 class ThreadState implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(ThreadStateTrack);
-    ctx.LEGACY_registerTrackController(ThreadStateTrackController);
+  onActivate(_ctx: PluginContext): void {}
+
+  async onTraceLoad(ctx: PluginContextTrace<undefined>): Promise<void> {
+    const {engine} = ctx;
+    const result = await engine.query(`
+      select
+        utid,
+        upid,
+        tid,
+        pid,
+        thread.name as threadName
+      from
+        thread_state
+        left join thread using(utid)
+        left join process using(upid)
+      where utid != 0
+      group by utid`);
+
+    const it = result.iter({
+      utid: NUM,
+      upid: NUM_NULL,
+      tid: NUM_NULL,
+      pid: NUM_NULL,
+      threadName: STR_NULL,
+    });
+    for (; it.valid(); it.next()) {
+      const utid = it.utid;
+      const upid = it.upid;
+      const tid = it.tid;
+      const threadName = it.threadName;
+      const displayName =
+          getTrackName({utid, tid, threadName, kind: THREAD_STATE_TRACK_KIND});
+
+      ctx.addTrack({
+        uri: `perfetto.ThreadState#${upid}.${utid}`,
+        displayName,
+        kind: THREAD_STATE_TRACK_KIND,
+        utid: utid,
+        track: ({trackInstanceId}) => {
+          return new TrackWithControllerAdapter<Config, Data>(
+              ctx.engine,
+              trackInstanceId,
+              {utid},
+              ThreadStateTrack,
+              ThreadStateTrackController);
+        },
+      });
+
+      ctx.addTrack({
+        uri: `perfetto.ThreadState#${utid}.v2`,
+        displayName,
+        kind: THREAD_STATE_TRACK_V2_KIND,
+        utid,
+        track: ({trackInstanceId}) => {
+          const track = ThreadStateTrackV2.create({
+            engine: ctx.engine,
+            trackId: trackInstanceId,
+          });
+          track.config = {utid};
+          return track;
+        },
+      });
+    }
   }
 }
 
diff --git a/ui/src/tracks/thread_state_v2/index.ts b/ui/src/tracks/thread_state/thread_state_track_v2.ts
similarity index 86%
copy from ui/src/tracks/thread_state_v2/index.ts
copy to ui/src/tracks/thread_state/thread_state_track_v2.ts
index 9c465b6..5973960 100644
--- a/ui/src/tracks/thread_state_v2/index.ts
+++ b/ui/src/tracks/thread_state/thread_state_track_v2.ts
@@ -1,4 +1,4 @@
-// Copyright (C) 2023 The Android Open Source Project
+// 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.
@@ -29,15 +29,14 @@
   SliceLayout,
 } from '../../frontend/slice_layout';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const THREAD_STATE_ROW = {
   ...BASE_SLICE_ROW,
   state: STR,
   ioWait: NUM_NULL,
 };
-export type ThreadStateRow = typeof THREAD_STATE_ROW;
 
+export type ThreadStateRow = typeof THREAD_STATE_ROW;
 
 export interface ThreadStateTrackConfig {
   utid: number;
@@ -48,10 +47,7 @@
   config: ThreadStateTrackConfig;
 }
 
-export const THREAD_STATE_TRACK_V2_KIND = 'ThreadStateTrackV2';
-
 export class ThreadStateTrack extends BaseSliceTrack<ThreadStateTrackTypes> {
-  static readonly kind = THREAD_STATE_TRACK_V2_KIND;
   static create(args: NewTrackArgs) {
     return new ThreadStateTrack(args);
   }
@@ -123,14 +119,3 @@
     return selection.kind === 'THREAD_STATE';
   }
 }
-
-class ThreadStateTrackV2 implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(ThreadStateTrack);
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ThreadStateTrackV2',
-  plugin: ThreadStateTrackV2,
-};
diff --git a/ui/src/tracks/thread_state_v2/index.ts b/ui/src/tracks/thread_state/thread_state_v2.ts
similarity index 89%
rename from ui/src/tracks/thread_state_v2/index.ts
rename to ui/src/tracks/thread_state/thread_state_v2.ts
index 9c465b6..5f17530 100644
--- a/ui/src/tracks/thread_state_v2/index.ts
+++ b/ui/src/tracks/thread_state/thread_state_v2.ts
@@ -29,7 +29,6 @@
   SliceLayout,
 } from '../../frontend/slice_layout';
 import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
 
 export const THREAD_STATE_ROW = {
   ...BASE_SLICE_ROW,
@@ -51,7 +50,6 @@
 export const THREAD_STATE_TRACK_V2_KIND = 'ThreadStateTrackV2';
 
 export class ThreadStateTrack extends BaseSliceTrack<ThreadStateTrackTypes> {
-  static readonly kind = THREAD_STATE_TRACK_V2_KIND;
   static create(args: NewTrackArgs) {
     return new ThreadStateTrack(args);
   }
@@ -123,14 +121,3 @@
     return selection.kind === 'THREAD_STATE';
   }
 }
-
-class ThreadStateTrackV2 implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrack(ThreadStateTrack);
-  }
-}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.ThreadStateTrackV2',
-  plugin: ThreadStateTrackV2,
-};
diff --git a/ui/src/tracks/visualised_args/index.ts b/ui/src/tracks/visualised_args/index.ts
index 3385bf9..19e6fb6 100644
--- a/ui/src/tracks/visualised_args/index.ts
+++ b/ui/src/tracks/visualised_args/index.ts
@@ -12,63 +12,128 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// import {NewTrackArgs, Track} from '../../frontend/track';
+// import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
 import m from 'mithril';
+import {v4 as uuidv4} from 'uuid';
 
 import {Actions} from '../../common/actions';
 import {globals} from '../../frontend/globals';
-import {NewTrackArgs, TrackBase} from '../../frontend/track';
-import {TrackButton, TrackButtonAttrs} from '../../frontend/track_panel';
-import {Plugin, PluginContext, PluginDescriptor} from '../../public';
+import {TrackButton} from '../../frontend/track_panel';
 import {
-  ChromeSliceTrack,
-  ChromeSliceTrackController,
-  Config as ChromeSliceConfig,
-} from '../chrome_slices';
-
-export {Data} from '../chrome_slices';
+  EngineProxy,
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+} from '../../public';
+import {ChromeSliceTrack} from '../chrome_slices';
 
 export const VISUALISED_ARGS_SLICE_TRACK_KIND = 'VisualisedArgsTrack';
+export const VISUALISED_ARGS_SLICE_TRACK_URI = 'perfetto.VisualisedArgs';
 
-export interface Config extends ChromeSliceConfig {
+export interface VisualisedArgsState {
   argName: string;
-}
-
-// The controller for arg visualisation is exactly the same as the controller
-// for Chrome slices. All customisation is done on the frontend.
-class VisualisedArgsTrackController extends ChromeSliceTrackController {
-  static readonly kind = VISUALISED_ARGS_SLICE_TRACK_KIND;
+  maxDepth: number;
+  trackId: number;
 }
 
 export class VisualisedArgsTrack extends ChromeSliceTrack {
-  static readonly kind = VISUALISED_ARGS_SLICE_TRACK_KIND;
-  static create(args: NewTrackArgs): TrackBase {
-    return new VisualisedArgsTrack(args);
+  private helperViewName: string;
+
+  constructor(
+      engine: EngineProxy, maxDepth: number, trackInstanceId: string,
+      trackId: number, private argName: string) {
+    const uuid = uuidv4();
+    const namespace = `__arg_visualisation_helper_${argName}_${uuid}`;
+    const escapedNamespace = namespace.replace(/[^a-zA-Z]/g, '_');
+    super(engine, maxDepth, trackInstanceId, trackId, escapedNamespace);
+    this.helperViewName = `${escapedNamespace}_slice`;
+  }
+
+  async onCreate(): Promise<void> {
+    // Create the helper view - just one which is relevant to this slice
+    await this.engine.query(`
+          create view ${this.helperViewName} as
+          with slice_with_arg as (
+            select
+              slice.id,
+              slice.track_id,
+              slice.ts,
+              slice.dur,
+              slice.thread_dur,
+              NULL as cat,
+              args.display_value as name
+            from slice
+            join args using (arg_set_id)
+            where args.key='${this.argName}'
+          )
+          select
+            *,
+            (select count()
+            from ancestor_slice(s1.id) s2
+            join slice_with_arg s3 on s2.id=s3.id
+            ) as depth
+          from slice_with_arg s1
+          order by id;
+      `);
+  }
+
+  async onDestroy(): Promise<void> {
+    this.engine.query(`drop view ${this.helperViewName}`);
   }
 
   getFont() {
     return 'italic 11px Roboto';
   }
 
-  getTrackShellButtons(): Array<m.Vnode<TrackButtonAttrs>> {
-    const config = this.config as Config;
-    const buttons: Array<m.Vnode<TrackButtonAttrs>> = [];
-    buttons.push(m(TrackButton, {
+  getTrackShellButtons(): m.Children {
+    return m(TrackButton, {
       action: () => {
+        // 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.removeVisualisedArg({argName: config.argName}));
+            Actions.removeTracks({trackInstanceIds: [this.trackInstanceId]}));
       },
       i: 'close',
       tooltip: 'Close',
       showButton: true,
-    }));
-    return buttons;
+    });
   }
 }
 
 class VisualisedArgsPlugin implements Plugin {
-  onActivate(ctx: PluginContext): void {
-    ctx.LEGACY_registerTrackController(VisualisedArgsTrackController);
-    ctx.LEGACY_registerTrack(VisualisedArgsTrack);
+  onActivate(_ctx: PluginContext): void {}
+  async onTraceLoad(ctx: PluginContextTrace<undefined>): Promise<void> {
+    ctx.addTrack({
+      uri: VISUALISED_ARGS_SLICE_TRACK_URI,
+      displayName: 'Visualised Args',
+      kind: VISUALISED_ARGS_SLICE_TRACK_KIND,
+      tags: {
+        metric: true,  // TODO(stevegolton): Is this track really a metric?
+      },
+      track: (trackCtx) => {
+        // Mount the store and migrate initial state.
+        const store = trackCtx.mountStore((initialState) => {
+          // TODO(stevegolton): Check initialState properly. Note, this is no
+          // worse than the situation we had before with track config.
+          // When we migrate to "proper" dynamic tracks, the problem of
+          // migrating state will be pushed up to the plugin anyway.
+          return initialState as VisualisedArgsState;
+        });
+        return new VisualisedArgsTrack(
+            ctx.engine,
+            store.state.maxDepth,
+            trackCtx.trackInstanceId,
+            store.state.trackId,
+            store.state.argName,
+        );
+      },
+    });
   }
 }