Merge "Add AdServices metric to perfetto." into main
diff --git a/Android.bp b/Android.bp
index 78fb15d..f074854 100644
--- a/Android.bp
+++ b/Android.bp
@@ -65,6 +65,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -151,6 +152,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -331,6 +333,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -426,6 +429,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -534,6 +538,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -639,6 +644,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -776,6 +782,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -860,6 +867,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -945,6 +953,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -1032,6 +1041,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -1084,6 +1094,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -1166,6 +1177,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -1243,6 +1255,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -1334,6 +1347,8 @@
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_cpp_gen",
@@ -1464,6 +1479,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -1533,6 +1550,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -1626,6 +1645,8 @@
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_cpp_gen",
@@ -1742,6 +1763,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -1811,6 +1834,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -2138,6 +2163,9 @@
         ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_lite_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_lite_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_lite_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
@@ -2312,6 +2340,8 @@
         ":perfetto_src_traced_probes_statsd_client_statsd_client",
         ":perfetto_src_traced_probes_sys_stats_sys_stats",
         ":perfetto_src_traced_probes_system_info_system_info",
+        ":perfetto_src_traced_relay_integrationtests",
+        ":perfetto_src_traced_relay_lib",
         ":perfetto_src_tracing_client_api_without_backends",
         ":perfetto_src_tracing_common",
         ":perfetto_src_tracing_core_core",
@@ -2406,6 +2436,9 @@
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_lite_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_lite_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_lite_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
@@ -2842,6 +2875,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.gen.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.gen.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:cpp
@@ -2878,6 +2914,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:lite
@@ -2909,6 +2948,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pb.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pb.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:lite
@@ -2944,6 +2986,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:zero
@@ -2976,6 +3021,9 @@
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_layers_config.pbzero.cc",
         "external/perfetto/protos/perfetto/config/android/surfaceflinger_transactions_config.pbzero.cc",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config/android:zero
@@ -3012,6 +3060,9 @@
         ".",
         "protos",
     ],
+    tool_files: [
+        "protos/perfetto/common/android_log_constants.proto",
+    ],
 }
 
 // GN: //protos/perfetto/config:cpp
@@ -5263,6 +5314,9 @@
         "protos/perfetto/trace/chrome/chrome_metadata.proto",
         "protos/perfetto/trace/chrome/chrome_trace_event.proto",
         "protos/perfetto/trace/clock_snapshot.proto",
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
         "protos/perfetto/trace/extension_descriptor.proto",
         "protos/perfetto/trace/filesystem/inode_file_map.proto",
         "protos/perfetto/trace/ftrace/android_fs.proto",
@@ -5401,6 +5455,136 @@
     ],
 }
 
+// GN: //protos/perfetto/trace/etw:cpp
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_cpp_gen",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.gen.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.gen.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.gen.cc",
+    ],
+}
+
+// GN: //protos/perfetto/trace/etw:cpp
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "perfetto_src_protozero_protoc_plugin_cppgen_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location perfetto_src_protozero_protoc_plugin_cppgen_plugin) --plugin_out=wrapper_namespace=gen:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.gen.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.gen.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.gen.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //protos/perfetto/trace/etw:lite
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_lite_gen",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.pb.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.pb.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.pb.cc",
+    ],
+}
+
+// GN: //protos/perfetto/trace/etw:lite
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_lite_gen_headers",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --cpp_out=lite=true:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.pb.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.pb.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.pb.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
+// GN: //protos/perfetto/trace/etw:zero
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_zero_gen",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.pbzero.cc",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.pbzero.cc",
+    ],
+}
+
+// GN: //protos/perfetto/trace/etw:zero
+genrule {
+    name: "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
+    srcs: [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    tools: [
+        "aprotoc",
+        "protozero_plugin",
+    ],
+    cmd: "mkdir -p $(genDir)/external/perfetto/ && $(location aprotoc) --proto_path=external/perfetto --plugin=protoc-gen-plugin=$(location protozero_plugin) --plugin_out=wrapper_namespace=pbzero:$(genDir)/external/perfetto/ $(in)",
+    out: [
+        "external/perfetto/protos/perfetto/trace/etw/etw.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event.pbzero.h",
+        "external/perfetto/protos/perfetto/trace/etw/etw_event_bundle.pbzero.h",
+    ],
+    export_include_dirs: [
+        ".",
+        "protos",
+    ],
+}
+
 // GN: //protos/perfetto/trace/filesystem:cpp
 genrule {
     name: "perfetto_protos_perfetto_trace_filesystem_cpp_gen",
@@ -11759,6 +11943,32 @@
     name: "perfetto_src_traced_probes_unittests",
 }
 
+// GN: //src/traced_relay:integrationtests
+filegroup {
+    name: "perfetto_src_traced_relay_integrationtests",
+    srcs: [
+        "src/traced_relay/relay_service_integrationtest.cc",
+    ],
+}
+
+// GN: //src/traced_relay:lib
+filegroup {
+    name: "perfetto_src_traced_relay_lib",
+    srcs: [
+        "src/traced_relay/relay_service.cc",
+        "src/traced_relay/socket_relay_handler.cc",
+    ],
+}
+
+// GN: //src/traced_relay:unittests
+filegroup {
+    name: "perfetto_src_traced_relay_unittests",
+    srcs: [
+        "src/traced_relay/relay_service_unittest.cc",
+        "src/traced_relay/socket_relay_handler_unittest.cc",
+    ],
+}
+
 // GN: //src/traced/service:service
 filegroup {
     name: "perfetto_src_traced_service_service",
@@ -12095,6 +12305,9 @@
         "protos/perfetto/trace/chrome/chrome_metadata.proto",
         "protos/perfetto/trace/chrome/chrome_trace_event.proto",
         "protos/perfetto/trace/clock_snapshot.proto",
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
         "protos/perfetto/trace/extension_descriptor.proto",
         "protos/perfetto/trace/filesystem/inode_file_map.proto",
         "protos/perfetto/trace/ftrace/android_fs.proto",
@@ -12250,6 +12463,7 @@
         ":perfetto_protos_perfetto_config_track_event_lite_gen",
         ":perfetto_protos_perfetto_trace_android_lite_gen",
         ":perfetto_protos_perfetto_trace_chrome_lite_gen",
+        ":perfetto_protos_perfetto_trace_etw_lite_gen",
         ":perfetto_protos_perfetto_trace_filesystem_lite_gen",
         ":perfetto_protos_perfetto_trace_ftrace_lite_gen",
         ":perfetto_protos_perfetto_trace_gpu_lite_gen",
@@ -12287,6 +12501,7 @@
         "perfetto_protos_perfetto_config_track_event_lite_gen_headers",
         "perfetto_protos_perfetto_trace_android_lite_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_lite_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_lite_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_lite_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_lite_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_lite_gen_headers",
@@ -12320,6 +12535,7 @@
         "perfetto_protos_perfetto_config_track_event_lite_gen_headers",
         "perfetto_protos_perfetto_trace_android_lite_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_lite_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_lite_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_lite_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_lite_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_lite_gen_headers",
@@ -12438,6 +12654,9 @@
         ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_lite_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_lite_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_lite_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
@@ -12506,6 +12725,7 @@
         ":perfetto_src_ipc_client",
         ":perfetto_src_ipc_common",
         ":perfetto_src_ipc_host",
+        ":perfetto_src_ipc_perfetto_ipc",
         ":perfetto_src_ipc_test_messages_cpp_gen",
         ":perfetto_src_ipc_test_messages_ipc_gen",
         ":perfetto_src_ipc_unittests",
@@ -12693,6 +12913,8 @@
         ":perfetto_src_traced_probes_system_info_system_info",
         ":perfetto_src_traced_probes_system_info_unittests",
         ":perfetto_src_traced_probes_unittests",
+        ":perfetto_src_traced_relay_lib",
+        ":perfetto_src_traced_relay_unittests",
         ":perfetto_src_traced_service_service",
         ":perfetto_src_traced_service_unittests",
         ":perfetto_src_tracing_client_api_without_backends",
@@ -12787,6 +13009,9 @@
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_lite_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_lite_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_lite_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
@@ -12951,6 +13176,8 @@
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_cpp_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_cpp_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_cpp_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_cpp_gen",
@@ -13074,6 +13301,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -13143,6 +13372,8 @@
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_cpp_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_cpp_gen_headers",
@@ -13242,6 +13473,7 @@
         ":perfetto_protos_perfetto_config_zero_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -13358,6 +13590,7 @@
         "perfetto_protos_perfetto_config_zero_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -13475,6 +13708,7 @@
         ":perfetto_protos_perfetto_config_zero_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -13592,6 +13826,7 @@
         "perfetto_protos_perfetto_config_zero_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -13719,6 +13954,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -13817,6 +14053,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
@@ -13924,6 +14161,7 @@
         ":perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen",
         ":perfetto_protos_perfetto_trace_android_zero_gen",
         ":perfetto_protos_perfetto_trace_chrome_zero_gen",
+        ":perfetto_protos_perfetto_trace_etw_zero_gen",
         ":perfetto_protos_perfetto_trace_filesystem_zero_gen",
         ":perfetto_protos_perfetto_trace_ftrace_zero_gen",
         ":perfetto_protos_perfetto_trace_gpu_zero_gen",
@@ -13997,6 +14235,7 @@
         "perfetto_protos_perfetto_ipc_wire_protocol_cpp_gen_headers",
         "perfetto_protos_perfetto_trace_android_zero_gen_headers",
         "perfetto_protos_perfetto_trace_chrome_zero_gen_headers",
+        "perfetto_protos_perfetto_trace_etw_zero_gen_headers",
         "perfetto_protos_perfetto_trace_filesystem_zero_gen_headers",
         "perfetto_protos_perfetto_trace_ftrace_zero_gen_headers",
         "perfetto_protos_perfetto_trace_gpu_zero_gen_headers",
diff --git a/BUILD b/BUILD
index 135ce8e..1ba49a5 100644
--- a/BUILD
+++ b/BUILD
@@ -315,6 +315,7 @@
                ":protos_perfetto_config_zero",
                ":protos_perfetto_trace_android_zero",
                ":protos_perfetto_trace_chrome_zero",
+               ":protos_perfetto_trace_etw_zero",
                ":protos_perfetto_trace_filesystem_zero",
                ":protos_perfetto_trace_ftrace_zero",
                ":protos_perfetto_trace_gpu_zero",
@@ -402,6 +403,7 @@
         ":protos_perfetto_config_zero",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -516,6 +518,7 @@
         ":protos_perfetto_ipc_ipc",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -3202,6 +3205,7 @@
         ":protos_perfetto_config_track_event_protos",
         ":protos_perfetto_trace_android_protos",
         ":protos_perfetto_trace_chrome_protos",
+        ":protos_perfetto_trace_etw_protos",
         ":protos_perfetto_trace_filesystem_protos",
         ":protos_perfetto_trace_ftrace_protos",
         ":protos_perfetto_trace_gpu_protos",
@@ -4285,6 +4289,27 @@
     ],
 )
 
+# GN target: //protos/perfetto/trace/etw:source_set
+perfetto_proto_library(
+    name = "protos_perfetto_trace_etw_protos",
+    srcs = [
+        "protos/perfetto/trace/etw/etw.proto",
+        "protos/perfetto/trace/etw/etw_event.proto",
+        "protos/perfetto/trace/etw/etw_event_bundle.proto",
+    ],
+    visibility = [
+        PERFETTO_CONFIG.proto_library_visibility,
+    ],
+)
+
+# GN target: //protos/perfetto/trace/etw:zero
+perfetto_cc_protozero_library(
+    name = "protos_perfetto_trace_etw_zero",
+    deps = [
+        ":protos_perfetto_trace_etw_protos",
+    ],
+)
+
 # GN target: //protos/perfetto/trace/filesystem:source_set
 perfetto_proto_library(
     name = "protos_perfetto_trace_filesystem_protos",
@@ -4531,6 +4556,7 @@
         ":protos_perfetto_config_track_event_protos",
         ":protos_perfetto_trace_android_protos",
         ":protos_perfetto_trace_chrome_protos",
+        ":protos_perfetto_trace_etw_protos",
         ":protos_perfetto_trace_filesystem_protos",
         ":protos_perfetto_trace_ftrace_protos",
         ":protos_perfetto_trace_gpu_protos",
@@ -4571,6 +4597,7 @@
         ":protos_perfetto_config_zero",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -5048,6 +5075,7 @@
         ":protos_perfetto_ipc_ipc",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -5139,6 +5167,7 @@
         ":protos_perfetto_ipc_ipc",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -5268,6 +5297,7 @@
                ":protos_perfetto_config_zero",
                ":protos_perfetto_trace_android_zero",
                ":protos_perfetto_trace_chrome_zero",
+               ":protos_perfetto_trace_etw_zero",
                ":protos_perfetto_trace_filesystem_zero",
                ":protos_perfetto_trace_ftrace_zero",
                ":protos_perfetto_trace_gpu_zero",
@@ -5420,6 +5450,7 @@
                ":protos_perfetto_config_zero",
                ":protos_perfetto_trace_android_zero",
                ":protos_perfetto_trace_chrome_zero",
+               ":protos_perfetto_trace_etw_zero",
                ":protos_perfetto_trace_filesystem_zero",
                ":protos_perfetto_trace_ftrace_zero",
                ":protos_perfetto_trace_gpu_zero",
@@ -5507,6 +5538,7 @@
         ":protos_perfetto_config_zero",
         ":protos_perfetto_trace_android_zero",
         ":protos_perfetto_trace_chrome_zero",
+        ":protos_perfetto_trace_etw_zero",
         ":protos_perfetto_trace_filesystem_zero",
         ":protos_perfetto_trace_ftrace_zero",
         ":protos_perfetto_trace_gpu_zero",
@@ -5641,6 +5673,7 @@
                ":protos_perfetto_config_zero",
                ":protos_perfetto_trace_android_zero",
                ":protos_perfetto_trace_chrome_zero",
+               ":protos_perfetto_trace_etw_zero",
                ":protos_perfetto_trace_filesystem_zero",
                ":protos_perfetto_trace_ftrace_zero",
                ":protos_perfetto_trace_gpu_zero",
diff --git a/CHANGELOG b/CHANGELOG
index bec63e0..777d115 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,10 +1,10 @@
 Unreleased:
   Tracing service and probes:
-    *
+    * Added reporting of TZ offset under system_info.timezone_off_mins .
   Trace Processor:
     *
   UI:
-    *
+    * Add a new type of debug tracks: counter.
   SDK:
     *
 
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/gn/perfetto_integrationtests.gni b/gn/perfetto_integrationtests.gni
index ff7bb7f..3e65e20 100644
--- a/gn/perfetto_integrationtests.gni
+++ b/gn/perfetto_integrationtests.gni
@@ -49,3 +49,7 @@
   perfetto_integrationtests_targets +=
       [ "src/trace_processor:integrationtests" ]
 }
+
+if (enable_perfetto_traced_relay) {
+  perfetto_integrationtests_targets += [ "src/traced_relay:integrationtests" ]
+}
diff --git a/gn/perfetto_unittests.gni b/gn/perfetto_unittests.gni
index cc51910..91e8d5d 100644
--- a/gn/perfetto_unittests.gni
+++ b/gn/perfetto_unittests.gni
@@ -80,3 +80,7 @@
     perfetto_unittests_targets += [ "src/bigtrace:unittests" ]
   }
 }
+
+if (enable_perfetto_traced_relay) {
+  perfetto_unittests_targets += [ "src/traced_relay:unittests" ]
+}
diff --git a/include/perfetto/base/time.h b/include/perfetto/base/time.h
index f8161ab..ea15dae 100644
--- a/include/perfetto/base/time.h
+++ b/include/perfetto/base/time.h
@@ -20,6 +20,7 @@
 #include <time.h>
 
 #include <chrono>
+#include <optional>
 #include <string>
 
 #include "perfetto/base/build_config.h"
@@ -245,6 +246,8 @@
   return TimeGm(&tms);
 }
 
+std::optional<int32_t> GetTimezoneOffsetMins();
+
 }  // namespace base
 }  // namespace perfetto
 
diff --git a/protos/perfetto/config/chrome/scenario_config.proto b/protos/perfetto/config/chrome/scenario_config.proto
index 5094f82..e90d6db 100644
--- a/protos/perfetto/config/chrome/scenario_config.proto
+++ b/protos/perfetto/config/chrome/scenario_config.proto
@@ -29,9 +29,16 @@
   // triggered.
   optional float trigger_chance = 2;
 
-  // Additional delay on the trigger below.
+  // Additional delay *after* the trigger below. This is mostly useful
+  // to trace beyond a triggered event in upload rules. Other triggers
+  // can still be serviced during this period.
   optional uint64 delay_ms = 3;
 
+  // Delay *before* which the rule is activated. Trigger events during this
+  // period are ignored by this rule. This is mostly useful to trace for a
+  // minimum duration before watching trigger events.
+  optional uint64 activation_delay_ms = 8;
+
   // Triggers when a value within the specified bounds [min_value,
   // max_value] is emitted into a Chrome histogram.
   message HistogramTrigger {
diff --git a/protos/perfetto/trace/BUILD.gn b/protos/perfetto/trace/BUILD.gn
index f81b05c..c808a93 100644
--- a/protos/perfetto/trace/BUILD.gn
+++ b/protos/perfetto/trace/BUILD.gn
@@ -93,6 +93,7 @@
     "../config:@TYPE@",
     "android:@TYPE@",
     "chrome:@TYPE@",
+    "etw:@TYPE@",
     "filesystem:@TYPE@",
     "ftrace:@TYPE@",
     "gpu:@TYPE@",
diff --git a/protos/perfetto/trace/etw/BUILD.gn b/protos/perfetto/trace/etw/BUILD.gn
new file mode 100644
index 0000000..f22514c
--- /dev/null
+++ b/protos/perfetto/trace/etw/BUILD.gn
@@ -0,0 +1,29 @@
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/perfetto.gni")
+import("../../../../gn/proto_library.gni")
+import("all_protos.gni")
+
+perfetto_proto_library("@TYPE@") {
+  sources = etw_proto_names
+}
+
+if (perfetto_build_standalone) {
+  perfetto_proto_library("descriptor") {
+    proto_generators = [ "descriptor" ]
+    generate_descriptor = "etw.descriptor"
+    sources = [ "etw_event_bundle.proto" ]
+  }
+}
diff --git a/protos/perfetto/trace/etw/all_protos.gni b/protos/perfetto/trace/etw/all_protos.gni
new file mode 100644
index 0000000..e6c9e9a
--- /dev/null
+++ b/protos/perfetto/trace/etw/all_protos.gni
@@ -0,0 +1,19 @@
+# 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.
+
+etw_proto_names = [
+  "etw.proto",
+  "etw_event.proto",
+  "etw_event_bundle.proto",
+]
diff --git a/protos/perfetto/trace/etw/etw.proto b/protos/perfetto/trace/etw/etw.proto
new file mode 100644
index 0000000..c008be2
--- /dev/null
+++ b/protos/perfetto/trace/etw/etw.proto
@@ -0,0 +1,153 @@
+/*
+ * 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;
+
+// Proto definition based on the Thread_v2 CSwitch class definition
+// See: https://learn.microsoft.com/en-us/windows/win32/etw/cswitch
+message CSwitchEtwEvent {
+  // New thread ID after the switch.
+  optional uint32 new_thread_id = 1;
+
+  // Previous thread ID.
+  optional uint32 old_thread_id = 2;
+
+  // Thread priority of the new thread.
+  optional sint32 new_thread_priority = 3;
+
+  // Thread priority of the previous thread.
+  optional sint32 old_thread_priority = 4;
+
+  // The index of the C-state that was last used by the processor. A value of 0
+  // represents the lightest idle state with higher values representing deeper
+  // C-states.
+  optional uint32 previous_c_state = 5;
+
+  // Wait reason for the previous thread. The ordering is important as based on
+  // the OldThreadWaitReason definition from the link above. The following are
+  // the possible values:
+  enum OldThreadWaitReason {
+    EXECUTIVE = 0;
+    FREE_PAGE = 1;
+    PAGE_IN = 2;
+    POOL_ALLOCATION = 3;
+    DELAY_EXECUTION = 4;
+    SUSPEND = 5;
+    USER_REQUEST = 6;
+    WR_EXECUTIVE = 7;
+    WR_FREE_PAGE = 8;
+    WR_PAGE_IN = 9;
+    WR_POOL_ALLOCATION = 10;
+    WR_DELAY_EXECUTION = 11;
+    WR_SUSPENDED = 12;
+    WR_USER_REQUEST = 13;
+    WR_EVENT_PAIR = 14;
+    WR_QUEUE = 15;
+    WR_LPC_RECEIVER = 16;
+    WR_LPC_REPLY = 17;
+    WR_VIRTUAL_MEMORY = 18;
+    WR_PAGE_OUT = 19;
+    WR_RENDEZ_VOUS = 20;
+    WR_KEYED_EVENT = 21;
+    WR_TERMINATED = 22;
+    WR_PROCESS_IN_SWAP = 23;
+    WR_CPU_RATE_CONTROL = 24;
+    WR_CALLOUT_STACK = 25;
+    WR_KERNEL = 26;
+    WR_RESOURCE = 27;
+    WR_PUSH_LOCK = 28;
+    WR_MUTEX = 29;
+    WR_QUANTUM_END = 30;
+    WR_DISPATCH_INT = 31;
+    WR_PREEMPTED = 32;
+    WR_YIELD_EXECUTION = 33;
+    WR_FAST_MUTEX = 34;
+    WR_GUARD_MUTEX = 35;
+    WR_RUNDOWN = 36;
+    MAXIMUM_WAIT_REASON = 37;
+  }
+
+  optional OldThreadWaitReason old_thread_wait_reason = 6;
+
+  // Wait mode for the previous thread. The ordering is important as based on
+  // the OldThreadWaitMode definition from the link above. The following are the
+  // possible values:
+  enum OldThreadWaitMode {
+    KERNEL_MODE = 0;
+    USER_MODE = 1;
+  }
+
+  optional OldThreadWaitMode old_thread_wait_mode = 7;
+
+  // State of the previous thread. The ordering is important as based on the
+  // OldThreadState definition from the link above. The following are the
+  // possible state values:
+  enum OldThreadState {
+    INITIALIZED = 0;
+    READY = 1;
+    RUNNING = 2;
+    STANDBY = 3;
+    TERMINATED = 4;
+    WAITING = 5;
+    TRANSITION = 6;
+    DEFERRED_READY = 7;
+  }
+
+  optional OldThreadState old_thread_state = 8;
+
+  // Ideal wait time of the previous thread.
+  optional sint32 old_thread_wait_ideal_processor = 9;
+
+  // Wait time for the new thread.
+  optional uint32 new_thread_wait_time = 10;
+}
+
+// Proto definition based on the Thread_v2 CSwitch class definition
+// See: https://learn.microsoft.com/en-us/windows/win32/etw/readythread
+message ReadyThreadEtwEvent {
+  // The thread identifier of the thread being readied for execution.
+  optional uint32 t_thread_id = 1;
+
+  // The reason for the priority boost. The ordering is important as based on
+  // the AdjustReason definition from the link above.
+  enum AdjustReason {
+    IGNORE_THE_INCREMENT = 0;
+    // Apply the increment, which will decay incrementally at the end of each
+    // quantum.
+    APPLY_INCREMENT = 1;
+    // Apply the increment as a boost that will decay in its entirety at quantum
+    // (typically for priority donation).
+    APPLY_INCREMENT_BOOST = 2;
+  }
+
+  optional AdjustReason adjust_reason = 2;
+
+  //  The value by which the priority is being adjusted.
+  optional sint32 adjust_increment = 3;
+
+  enum TraceFlag {
+    TRACE_FLAG_UNSPECIFIED = 0;
+    // The thread has been readied from DPC (deferred procedure call).
+    THREAD_READIED = 0x1;
+    // The kernel stack is currently swapped out.
+    KERNEL_STACK_SWAPPED_OUT = 0x2;
+    // The process address space is swapped out.
+    PROCESS_ADDRESS_SWAPPED_OUT = 0x4;
+  }
+
+  optional TraceFlag flag = 4;
+}
\ No newline at end of file
diff --git a/protos/perfetto/trace/etw/etw_event.proto b/protos/perfetto/trace/etw/etw_event.proto
new file mode 100644
index 0000000..9ef1b4e
--- /dev/null
+++ b/protos/perfetto/trace/etw/etw_event.proto
@@ -0,0 +1,29 @@
+/*
+ * 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;
+
+import "protos/perfetto/trace/etw/etw.proto";
+
+message EtwTraceEvent {
+  optional uint64 timestamp = 1;
+
+  oneof event {
+    CSwitchEtwEvent c_switch = 2;
+    ReadyThreadEtwEvent ready_thread = 3;
+  }
+}
\ No newline at end of file
diff --git a/protos/perfetto/trace/etw/etw_event_bundle.proto b/protos/perfetto/trace/etw/etw_event_bundle.proto
new file mode 100644
index 0000000..3ea3916
--- /dev/null
+++ b/protos/perfetto/trace/etw/etw_event_bundle.proto
@@ -0,0 +1,29 @@
+/*
+ * 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";
+
+import "protos/perfetto/trace/etw/etw_event.proto";
+
+package perfetto.protos;
+
+// The result of tracing one or more etw event uses per-processor buffers where
+// an in-use buffer is assigned to each processor at all times. Therefore,
+// collecting multiple events they should already be synchronized.
+message EtwTraceEventBundle {
+  optional uint32 cpu = 1;
+  repeated EtwTraceEvent event = 2;
+}
\ No newline at end of file
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 16112ab..41b2651 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -5166,6 +5166,167 @@
 
 // End of protos/perfetto/trace/clock_snapshot.proto
 
+// Begin of protos/perfetto/trace/etw/etw.proto
+
+// Proto definition based on the Thread_v2 CSwitch class definition
+// See: https://learn.microsoft.com/en-us/windows/win32/etw/cswitch
+message CSwitchEtwEvent {
+  // New thread ID after the switch.
+  optional uint32 new_thread_id = 1;
+
+  // Previous thread ID.
+  optional uint32 old_thread_id = 2;
+
+  // Thread priority of the new thread.
+  optional sint32 new_thread_priority = 3;
+
+  // Thread priority of the previous thread.
+  optional sint32 old_thread_priority = 4;
+
+  // The index of the C-state that was last used by the processor. A value of 0
+  // represents the lightest idle state with higher values representing deeper
+  // C-states.
+  optional uint32 previous_c_state = 5;
+
+  // Wait reason for the previous thread. The ordering is important as based on
+  // the OldThreadWaitReason definition from the link above. The following are
+  // the possible values:
+  enum OldThreadWaitReason {
+    EXECUTIVE = 0;
+    FREE_PAGE = 1;
+    PAGE_IN = 2;
+    POOL_ALLOCATION = 3;
+    DELAY_EXECUTION = 4;
+    SUSPEND = 5;
+    USER_REQUEST = 6;
+    WR_EXECUTIVE = 7;
+    WR_FREE_PAGE = 8;
+    WR_PAGE_IN = 9;
+    WR_POOL_ALLOCATION = 10;
+    WR_DELAY_EXECUTION = 11;
+    WR_SUSPENDED = 12;
+    WR_USER_REQUEST = 13;
+    WR_EVENT_PAIR = 14;
+    WR_QUEUE = 15;
+    WR_LPC_RECEIVER = 16;
+    WR_LPC_REPLY = 17;
+    WR_VIRTUAL_MEMORY = 18;
+    WR_PAGE_OUT = 19;
+    WR_RENDEZ_VOUS = 20;
+    WR_KEYED_EVENT = 21;
+    WR_TERMINATED = 22;
+    WR_PROCESS_IN_SWAP = 23;
+    WR_CPU_RATE_CONTROL = 24;
+    WR_CALLOUT_STACK = 25;
+    WR_KERNEL = 26;
+    WR_RESOURCE = 27;
+    WR_PUSH_LOCK = 28;
+    WR_MUTEX = 29;
+    WR_QUANTUM_END = 30;
+    WR_DISPATCH_INT = 31;
+    WR_PREEMPTED = 32;
+    WR_YIELD_EXECUTION = 33;
+    WR_FAST_MUTEX = 34;
+    WR_GUARD_MUTEX = 35;
+    WR_RUNDOWN = 36;
+    MAXIMUM_WAIT_REASON = 37;
+  }
+
+  optional OldThreadWaitReason old_thread_wait_reason = 6;
+
+  // Wait mode for the previous thread. The ordering is important as based on
+  // the OldThreadWaitMode definition from the link above. The following are the
+  // possible values:
+  enum OldThreadWaitMode {
+    KERNEL_MODE = 0;
+    USER_MODE = 1;
+  }
+
+  optional OldThreadWaitMode old_thread_wait_mode = 7;
+
+  // State of the previous thread. The ordering is important as based on the
+  // OldThreadState definition from the link above. The following are the
+  // possible state values:
+  enum OldThreadState {
+    INITIALIZED = 0;
+    READY = 1;
+    RUNNING = 2;
+    STANDBY = 3;
+    TERMINATED = 4;
+    WAITING = 5;
+    TRANSITION = 6;
+    DEFERRED_READY = 7;
+  }
+
+  optional OldThreadState old_thread_state = 8;
+
+  // Ideal wait time of the previous thread.
+  optional sint32 old_thread_wait_ideal_processor = 9;
+
+  // Wait time for the new thread.
+  optional uint32 new_thread_wait_time = 10;
+}
+
+// Proto definition based on the Thread_v2 CSwitch class definition
+// See: https://learn.microsoft.com/en-us/windows/win32/etw/readythread
+message ReadyThreadEtwEvent {
+  // The thread identifier of the thread being readied for execution.
+  optional uint32 t_thread_id = 1;
+
+  // The reason for the priority boost. The ordering is important as based on
+  // the AdjustReason definition from the link above.
+  enum AdjustReason {
+    IGNORE_THE_INCREMENT = 0;
+    // Apply the increment, which will decay incrementally at the end of each
+    // quantum.
+    APPLY_INCREMENT = 1;
+    // Apply the increment as a boost that will decay in its entirety at quantum
+    // (typically for priority donation).
+    APPLY_INCREMENT_BOOST = 2;
+  }
+
+  optional AdjustReason adjust_reason = 2;
+
+  //  The value by which the priority is being adjusted.
+  optional sint32 adjust_increment = 3;
+
+  enum TraceFlag {
+    TRACE_FLAG_UNSPECIFIED = 0;
+    // The thread has been readied from DPC (deferred procedure call).
+    THREAD_READIED = 0x1;
+    // The kernel stack is currently swapped out.
+    KERNEL_STACK_SWAPPED_OUT = 0x2;
+    // The process address space is swapped out.
+    PROCESS_ADDRESS_SWAPPED_OUT = 0x4;
+  }
+
+  optional TraceFlag flag = 4;
+}
+// End of protos/perfetto/trace/etw/etw.proto
+
+// Begin of protos/perfetto/trace/etw/etw_event.proto
+
+message EtwTraceEvent {
+  optional uint64 timestamp = 1;
+
+  oneof event {
+    CSwitchEtwEvent c_switch = 2;
+    ReadyThreadEtwEvent ready_thread = 3;
+  }
+}
+// End of protos/perfetto/trace/etw/etw_event.proto
+
+// Begin of protos/perfetto/trace/etw/etw_event_bundle.proto
+
+// The result of tracing one or more etw event uses per-processor buffers where
+// an in-use buffer is assigned to each processor at all times. Therefore,
+// collecting multiple events they should already be synchronized.
+message EtwTraceEventBundle {
+  optional uint32 cpu = 1;
+  repeated EtwTraceEvent event = 2;
+}
+// End of protos/perfetto/trace/etw/etw_event_bundle.proto
+
 // Begin of protos/perfetto/common/descriptor.proto
 
 // The protocol compiler can output a FileDescriptorSet containing the .proto
@@ -12334,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;
@@ -12452,6 +12613,10 @@
 
   // Kernel page size - sysconf(_SC_PAGESIZE).
   optional uint32 page_size = 6;
+
+  // The timezone offset from UTC, as per strftime("%z"), in minutes.
+  // Introduced in v38 / Android V.
+  optional int32 timezone_off_mins = 7;
 }
 
 // End of protos/perfetto/trace/system_info.proto
@@ -13081,7 +13246,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 95.
+// Next id: 96.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -13201,6 +13366,9 @@
     LayersSnapshotProto surfaceflinger_layers_snapshot = 93;
     TransactionTraceEntry surfaceflinger_transactions = 94;
 
+    // Events from the Windows etw infrastructure.
+    EtwTraceEventBundle etw_events = 95;
+
     // This field is only used for testing.
     // In previous versions of this proto this field had the id 268435455
     // This caused many problems:
diff --git a/protos/perfetto/trace/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/protos/perfetto/trace/system_info.proto b/protos/perfetto/trace/system_info.proto
index f4c0507..9a75773 100644
--- a/protos/perfetto/trace/system_info.proto
+++ b/protos/perfetto/trace/system_info.proto
@@ -44,4 +44,8 @@
 
   // Kernel page size - sysconf(_SC_PAGESIZE).
   optional uint32 page_size = 6;
+
+  // The timezone offset from UTC, as per strftime("%z"), in minutes.
+  // Introduced in v38 / Android V.
+  optional int32 timezone_off_mins = 7;
 }
diff --git a/protos/perfetto/trace/test_extensions.proto b/protos/perfetto/trace/test_extensions.proto
index 0d33c09..2d21e92 100644
--- a/protos/perfetto/trace/test_extensions.proto
+++ b/protos/perfetto/trace/test_extensions.proto
@@ -28,6 +28,7 @@
 message TestExtension {
   extend TrackEvent {
     optional string string_extension_for_testing = 9900;
+    optional string string_extension_for_testing2 = 9905;
     repeated int32 int_extension_for_testing = 9901;
     optional string omitted_extension_for_testing = 9902;
     optional TestExtensionChild nested_message_extension_for_testing = 9903;
diff --git a/protos/perfetto/trace/trace_packet.proto b/protos/perfetto/trace/trace_packet.proto
index b47981b..0c60cf6 100644
--- a/protos/perfetto/trace/trace_packet.proto
+++ b/protos/perfetto/trace/trace_packet.proto
@@ -35,6 +35,7 @@
 import "protos/perfetto/trace/chrome/chrome_metadata.proto";
 import "protos/perfetto/trace/chrome/chrome_trace_event.proto";
 import "protos/perfetto/trace/clock_snapshot.proto";
+import "protos/perfetto/trace/etw/etw_event_bundle.proto";
 import "protos/perfetto/trace/filesystem/inode_file_map.proto";
 import "protos/perfetto/trace/ftrace/ftrace_event_bundle.proto";
 import "protos/perfetto/trace/ftrace/ftrace_stats.proto";
@@ -96,7 +97,7 @@
 // See the [Buffers and Dataflow](/docs/concepts/buffers.md) doc for details.
 //
 // Next reserved id: 14 (up to 15).
-// Next id: 95.
+// Next id: 96.
 message TracePacket {
   // The timestamp of the TracePacket.
   // By default this timestamps refers to the trace clock (CLOCK_BOOTTIME on
@@ -216,6 +217,9 @@
     LayersSnapshotProto surfaceflinger_layers_snapshot = 93;
     TransactionTraceEntry surfaceflinger_transactions = 94;
 
+    // Events from the Windows etw infrastructure.
+    EtwTraceEventBundle etw_events = 95;
+
     // This field is only used for testing.
     // In previous versions of this proto this field had the id 268435455
     // This caused many problems:
diff --git a/python/generators/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/src/base/string_utils_unittest.cc b/src/base/string_utils_unittest.cc
index f65da9f..0138bf1 100644
--- a/src/base/string_utils_unittest.cc
+++ b/src/base/string_utils_unittest.cc
@@ -96,6 +96,8 @@
 TEST(StringUtilsTest, StringToInt32) {
   EXPECT_EQ(StringToInt32("0"), std::make_optional<int32_t>(0));
   EXPECT_EQ(StringToInt32("1"), std::make_optional<int32_t>(1));
+  EXPECT_EQ(StringToInt32("+42"), std::make_optional<int32_t>(42));
+  EXPECT_EQ(StringToInt32("+0042"), std::make_optional<int32_t>(42));
   EXPECT_EQ(StringToInt32("-42"), std::make_optional<int32_t>(-42));
   EXPECT_EQ(StringToInt32("42", 16), std::make_optional<int32_t>(0x42));
   EXPECT_EQ(StringToInt32("7ffffffe", 16),
diff --git a/src/base/time.cc b/src/base/time.cc
index a02c9d6..1507916 100644
--- a/src/base/time.cc
+++ b/src/base/time.cc
@@ -18,6 +18,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
 
 #if PERFETTO_BUILDFLAG(PERFETTO_OS_WIN)
 #include <Windows.h>
@@ -78,5 +79,17 @@
   return buf;
 }
 
+std::optional<int32_t> GetTimezoneOffsetMins() {
+  std::string tz = GetTimeFmt("%z");
+  if (tz.size() != 5 || (tz[0] != '+' && tz[0] != '-'))
+    return std::nullopt;
+  char sign = '\0';
+  int32_t hh = 0;
+  int32_t mm = 0;
+  if (sscanf(tz.c_str(), "%c%2d%2d", &sign, &hh, &mm) != 3)
+    return std::nullopt;
+  return (hh * 60 + mm) * (sign == '-' ? -1 : 1);
+}
+
 }  // namespace base
 }  // namespace perfetto
diff --git a/src/base/time_unittest.cc b/src/base/time_unittest.cc
index 62c8566..a5b9d78 100644
--- a/src/base/time_unittest.cc
+++ b/src/base/time_unittest.cc
@@ -16,6 +16,7 @@
 
 #include "perfetto/base/time.h"
 
+#include "perfetto/ext/base/utils.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
@@ -72,6 +73,32 @@
   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())
+      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".
+
+  base::SetEnv("TZ", "UTC+2");
+  EXPECT_EQ(GetTimezoneOffsetMins(), -2 * 60);
+
+  base::SetEnv("TZ", "UTC-2");
+  EXPECT_EQ(GetTimezoneOffsetMins(), 2 * 60);
+
+  base::SetEnv("TZ", "UTC-07:45");
+  EXPECT_EQ(GetTimezoneOffsetMins(), 7 * 60 + 45);
+}
+#endif
+
 }  // namespace
 }  // namespace base
 }  // namespace perfetto
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/protozero/protoc_plugin/protozero_plugin.cc b/src/protozero/protoc_plugin/protozero_plugin.cc
index 55c3c9d..e4c691f 100644
--- a/src/protozero/protoc_plugin/protozero_plugin.cc
+++ b/src/protozero/protoc_plugin/protozero_plugin.cc
@@ -108,6 +108,8 @@
   void SetOption(const std::string& name, const std::string& value) {
     if (name == "wrapper_namespace") {
       wrapper_namespace_ = value;
+    } else if (name == "sdk") {
+      sdk_mode_ = (value == "true" || value == "1");
     } else {
       Abort(std::string() + "Unknown plugin option '" + name + "'.");
     }
@@ -468,27 +470,37 @@
         "#ifndef $guard$\n"
         "#define $guard$\n\n"
         "#include <stddef.h>\n"
-        "#include <stdint.h>\n\n"
-        "#include \"perfetto/protozero/field_writer.h\"\n"
-        "#include \"perfetto/protozero/message.h\"\n"
-        "#include \"perfetto/protozero/packed_repeated_fields.h\"\n"
-        "#include \"perfetto/protozero/proto_decoder.h\"\n"
-        "#include \"perfetto/protozero/proto_utils.h\"\n",
+        "#include <stdint.h>\n\n",
         "greeting", greeting, "guard", guard);
 
-    // Print includes for public imports.
-    for (const FileDescriptor* dependency : public_imports_) {
-      // Dependency name could contain slashes but importing from upper-level
-      // directories is not possible anyway since build system processes each
-      // proto file individually. Hence proto lookup path is always equal to the
-      // directory where particular proto file is located and protoc does not
-      // allow reference to upper directory (aka ..) in import path.
-      //
-      // Laconically said:
-      // - source_->name() may never have slashes,
-      // - dependency->name() may have slashes but always refers to inner path.
-      stub_h_->Print("#include \"$name$.h\"\n", "name",
-                     ProtoStubName(dependency));
+    if (sdk_mode_) {
+      stub_h_->Print("#include \"perfetto.h\"\n");
+    } else {
+      stub_h_->Print(
+          "#include \"perfetto/protozero/field_writer.h\"\n"
+          "#include \"perfetto/protozero/message.h\"\n"
+          "#include \"perfetto/protozero/packed_repeated_fields.h\"\n"
+          "#include \"perfetto/protozero/proto_decoder.h\"\n"
+          "#include \"perfetto/protozero/proto_utils.h\"\n");
+    }
+
+    // Print includes for public imports. In sdk mode, all imports are assumed
+    // to be part of the sdk.
+    if (!sdk_mode_) {
+      for (const FileDescriptor* dependency : public_imports_) {
+        // Dependency name could contain slashes but importing from upper-level
+        // directories is not possible anyway since build system processes each
+        // proto file individually. Hence proto lookup path is always equal to
+        // the directory where particular proto file is located and protoc does
+        // not allow reference to upper directory (aka ..) in import path.
+        //
+        // Laconically said:
+        // - source_->name() may never have slashes,
+        // - dependency->name() may have slashes but always refers to inner
+        // path.
+        stub_h_->Print("#include \"$name$.h\"\n", "name",
+                       ProtoStubName(dependency));
+      }
     }
     stub_h_->Print("\n");
 
@@ -1003,6 +1015,9 @@
   std::vector<const EnumDescriptor*> enums_;
   std::map<std::string, std::vector<const FieldDescriptor*>> extensions_;
 
+  // Generate headers that can be used with the Perfetto SDK.
+  bool sdk_mode_ = false;
+
   // The custom *Comp comparators are to ensure determinism of the generator.
   std::set<const FileDescriptor*, FileDescriptorComp> public_imports_;
   std::set<const FileDescriptor*, FileDescriptorComp> private_imports_;
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index da6a2d2..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());
@@ -603,6 +603,12 @@
                           Variadic::String(machine_id));
   }
 
+  if (packet.has_timezone_off_mins()) {
+    context_->metadata_tracker->SetMetadata(
+        metadata::timezone_off_mins,
+        Variadic::Integer(packet.timezone_off_mins()));
+  }
+
   if (packet.has_android_build_fingerprint()) {
     context_->metadata_tracker->SetMetadata(
         metadata::android_build_fingerprint,
diff --git a/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql b/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
index 95ea995..3aa7167 100644
--- a/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
+++ b/src/trace_processor/metrics/sql/chrome/chrome_slice_names.sql
@@ -27,6 +27,7 @@
   'slice_name', (
     SELECT RepeatedField(DISTINCT(name))
     FROM slice
+    WHERE name IS NOT NULL
     ORDER BY name
   )
 );
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/trace_processor/rpc/BUILD.gn b/src/trace_processor/rpc/BUILD.gn
index 8604153..26b7528 100644
--- a/src/trace_processor/rpc/BUILD.gn
+++ b/src/trace_processor/rpc/BUILD.gn
@@ -23,9 +23,9 @@
 # interface) and by the :httpd module for the HTTP interface.
 source_set("rpc") {
   sources = [
+    "query_result_serializer.cc",
     "rpc.cc",
     "rpc.h",
-    "query_result_serializer.cc",
   ]
   deps = [
     "..:lib",
@@ -44,8 +44,10 @@
 }
 
 # Static library target for RPC code. Needed for BigTrace in Google3.
-static_library("trace_processor_rpc") {
-  public_deps = [ ":rpc" ]
+if (is_perfetto_build_generator) {
+  static_library("trace_processor_rpc") {
+    public_deps = [ ":rpc" ]
+  }
 }
 
 perfetto_unittest_source_set("unittests") {
diff --git a/src/trace_processor/storage/metadata.h b/src/trace_processor/storage/metadata.h
index 88eac06..ba2f4c0 100644
--- a/src/trace_processor/storage/metadata.h
+++ b/src/trace_processor/storage/metadata.h
@@ -48,6 +48,7 @@
   F(system_name,                       KeyType::kSingle,  Variadic::kString), \
   F(system_release,                    KeyType::kSingle,  Variadic::kString), \
   F(system_version,                    KeyType::kSingle,  Variadic::kString), \
+  F(timezone_off_mins,                 KeyType::kSingle,  Variadic::kInt),    \
   F(trace_config_pbtxt,                KeyType::kSingle,  Variadic::kString), \
   F(trace_size_bytes,                  KeyType::kSingle,  Variadic::kInt),    \
   F(trace_time_clock_id,               KeyType::kSingle,  Variadic::kInt),    \
diff --git a/src/trace_processor/util/descriptors.cc b/src/trace_processor/util/descriptors.cc
index bd365f8..96833ed 100644
--- a/src/trace_processor/util/descriptors.cc
+++ b/src/trace_processor/util/descriptors.cc
@@ -239,7 +239,8 @@
     const std::string file_name = file.name().ToStdString();
     if (base::StartsWithAny(file_name, skip_prefixes))
       continue;
-    if (processed_files_.find(file_name) != processed_files_.end()) {
+    if (!merge_existing_messages &&
+        processed_files_.find(file_name) != processed_files_.end()) {
       // This file has been loaded once already. Skip.
       continue;
     }
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/traced_relay/BUILD.gn b/src/traced_relay/BUILD.gn
index d4e27cc..6b49f94 100644
--- a/src/traced_relay/BUILD.gn
+++ b/src/traced_relay/BUILD.gn
@@ -14,6 +14,7 @@
 
 import("../../gn/perfetto.gni")
 import("../../gn/perfetto_component.gni")
+import("../../gn/test.gni")
 
 executable("traced_relay") {
   deps = [
@@ -45,3 +46,33 @@
     "//src/ipc:perfetto_ipc",
   ]
 }
+
+perfetto_unittest_source_set("unittests") {
+  testonly = true
+  deps = [
+    ":lib",
+    "../../gn:default_deps",
+    "../../gn:gtest_and_gmock",
+    "../base",
+    "../base:test_support",
+    "../base/threading",
+    "//src/ipc:perfetto_ipc",
+  ]
+  sources = [
+    "relay_service_unittest.cc",
+    "socket_relay_handler_unittest.cc",
+  ]
+}
+
+source_set("integrationtests") {
+  testonly = true
+  deps = [
+    ":lib",
+    "../../gn:default_deps",
+    "../../gn:gtest_and_gmock",
+    "../../test:test_helper",
+    "../base",
+    "../base:test_support",
+  ]
+  sources = [ "relay_service_integrationtest.cc" ]
+}
diff --git a/src/traced_relay/relay_service.cc b/src/traced_relay/relay_service.cc
index 4b1dc6b..1ef5855 100644
--- a/src/traced_relay/relay_service.cc
+++ b/src/traced_relay/relay_service.cc
@@ -70,7 +70,10 @@
   IPCFrame ipc_frame;
   ipc_frame.set_request_id(0);
   auto* set_peer_identity = ipc_frame.mutable_set_peer_identity();
+#if PERFETTO_BUILDFLAG(PERFETTO_OS_LINUX) || \
+    PERFETTO_BUILDFLAG(PERFETTO_OS_ANDROID)
   set_peer_identity->set_pid(server_conn->peer_pid_linux());
+#endif
   set_peer_identity->set_uid(
       static_cast<int32_t>(server_conn->peer_uid_posix()));
 
diff --git a/src/traced_relay/relay_service_integrationtest.cc b/src/traced_relay/relay_service_integrationtest.cc
new file mode 100644
index 0000000..7e29041
--- /dev/null
+++ b/src/traced_relay/relay_service_integrationtest.cc
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <memory>
+#include "src/traced_relay/relay_service.h"
+
+#include "src/base/test/test_task_runner.h"
+#include "test/gtest_and_gmock.h"
+#include "test/test_helper.h"
+
+#include "protos/perfetto/config/test_config.gen.h"
+#include "protos/perfetto/config/trace_config.gen.h"
+#include "protos/perfetto/trace/test_event.gen.h"
+
+namespace perfetto {
+namespace {
+
+TEST(TracedRelayIntegrationTest, BasicCase) {
+  base::TestTaskRunner task_runner;
+
+  std::string sock_name;
+  {
+    // Set up a server UnixSocket to find an unused TCP port.
+    base::UnixSocket::EventListener event_listener;
+    auto srv = base::UnixSocket::Listen("127.0.0.1:0", &event_listener,
+                                        &task_runner, base::SockFamily::kInet,
+                                        base::SockType::kStream);
+    ASSERT_TRUE(srv->is_listening());
+    sock_name = srv->GetSockAddr();
+    // Shut down |srv| here to free the port. It's unlikely that the port will
+    // be taken by another process so quickly before we reach the code below.
+  }
+
+  TestHelper helper(&task_runner, TestHelper::Mode::kStartDaemons,
+                    sock_name.c_str());
+  ASSERT_EQ(helper.num_producers(), 1u);
+  helper.StartServiceIfRequired();
+
+  auto relay_service = std::make_unique<RelayService>(&task_runner);
+
+  relay_service->Start("@traced_relay", sock_name.c_str());
+
+  auto producer_connected =
+      task_runner.CreateCheckpoint("perfetto.FakeProducer.connected");
+  auto noop = []() {};
+  auto connected = [&]() { task_runner.PostTask(producer_connected); };
+
+  // We won't use the built-in fake producer and will start our own.
+  auto producer_thread = std::make_unique<FakeProducerThread>(
+      "@traced_relay", connected, noop, noop, "perfetto.FakeProducer");
+  producer_thread->Connect();
+  task_runner.RunUntilCheckpoint("perfetto.FakeProducer.connected");
+
+  helper.ConnectConsumer();
+  helper.WaitForConsumerConnect();
+
+  TraceConfig trace_config;
+  trace_config.add_buffers()->set_size_kb(1024);
+  trace_config.set_duration_ms(200);
+
+  static constexpr uint32_t kMsgSize = 1024;
+  static constexpr uint32_t kRandomSeed = 42;
+  // Enable the producer.
+  auto* ds_config = trace_config.add_data_sources()->mutable_config();
+  ds_config->set_name("perfetto.FakeProducer");
+  ds_config->set_target_buffer(0);
+  ds_config->mutable_for_testing()->set_seed(kRandomSeed);
+  ds_config->mutable_for_testing()->set_message_count(12);
+  ds_config->mutable_for_testing()->set_message_size(kMsgSize);
+  ds_config->mutable_for_testing()->set_send_batch_on_register(true);
+
+  helper.StartTracing(trace_config);
+  helper.WaitForTracingDisabled();
+
+  helper.ReadData();
+  helper.WaitForReadData();
+
+  const auto& packets = helper.trace();
+  ASSERT_EQ(packets.size(), 12u);
+
+  // The producer is connected from this process. The relay service will inject
+  // the SetPeerIdentity message using the pid and euid of the current process.
+  auto pid = static_cast<int32_t>(getpid());
+  auto uid = static_cast<int32_t>(geteuid());
+
+  std::minstd_rand0 rnd_engine(kRandomSeed);
+  for (const auto& packet : packets) {
+    ASSERT_TRUE(packet.has_for_testing());
+    ASSERT_EQ(packet.trusted_pid(), pid);
+    ASSERT_EQ(packet.trusted_uid(), uid);
+    ASSERT_EQ(packet.for_testing().seq_value(), rnd_engine());
+  }
+}
+
+}  // namespace
+}  // namespace perfetto
diff --git a/src/traced_relay/relay_service_unittest.cc b/src/traced_relay/relay_service_unittest.cc
new file mode 100644
index 0000000..649f552
--- /dev/null
+++ b/src/traced_relay/relay_service_unittest.cc
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/traced_relay/relay_service.h"
+
+#include <memory>
+
+#include "perfetto/ext/base/unix_socket.h"
+#include "protos/perfetto/ipc/wire_protocol.gen.h"
+#include "src/base/test/test_task_runner.h"
+#include "src/ipc/buffered_frame_deserializer.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto {
+namespace {
+
+using ::testing::_;
+using ::testing::Invoke;
+
+class TestEventListener : public base::UnixSocket::EventListener {
+ public:
+  MOCK_METHOD(void, OnDataAvailable, (base::UnixSocket*), (override));
+  MOCK_METHOD(void, OnConnect, (base::UnixSocket*, bool), (override));
+  MOCK_METHOD(void, OnNewIncomingConnection, (base::UnixSocket*));
+
+  void OnNewIncomingConnection(
+      base::UnixSocket*,
+      std::unique_ptr<base::UnixSocket> new_connection) override {
+    // Need to keep |new_connection| alive.
+    client_connection_ = std::move(new_connection);
+    OnNewIncomingConnection(client_connection_.get());
+  }
+
+ private:
+  std::unique_ptr<base::UnixSocket> client_connection_;
+};
+
+// Exercises the relay service and also validates that the relay service injects
+// a SetPeerIdentity message:
+//
+// producer (client UnixSocket) <- @producer.sock -> relay service
+// <- 127.0.0.1.* -> tcp_server (listening UnixSocet).
+TEST(RelayServiceTest, SetPeerIdentity) {
+  base::TestTaskRunner task_runner;
+  auto relay_service = std::make_unique<RelayService>(&task_runner);
+
+  // Set up a server UnixSocket to find an unused TCP port.
+  // The TCP connection emulates the socket to the host traced.
+  TestEventListener tcp_listener;
+  auto tcp_server = base::UnixSocket::Listen(
+      "127.0.0.1:0", &tcp_listener, &task_runner, base::SockFamily::kInet,
+      base::SockType::kStream);
+  ASSERT_TRUE(tcp_server->is_listening());
+  auto tcp_sock_name = tcp_server->GetSockAddr();
+  auto* unix_sock_name =
+      "@producer.sock";  // Use abstract unix socket for server socket.
+
+  // Start the relay service.
+  relay_service->Start(unix_sock_name, tcp_sock_name.c_str());
+
+  // Emulates the producer connection.
+  TestEventListener producer_listener;
+  auto producer = base::UnixSocket::Connect(
+      unix_sock_name, &producer_listener, &task_runner, base::SockFamily::kUnix,
+      base::SockType::kStream);
+  auto producer_connected = task_runner.CreateCheckpoint("producer_connected");
+  EXPECT_CALL(producer_listener, OnConnect(_, _))
+      .WillOnce(Invoke([&](base::UnixSocket* s, bool conn) {
+        EXPECT_TRUE(conn);
+        EXPECT_EQ(s, producer.get());
+        producer_connected();
+      }));
+  task_runner.RunUntilCheckpoint("producer_connected");
+
+  // Add some producer data.
+  ipc::Frame test_frame;
+  test_frame.add_data_for_testing("test_data");
+  auto test_data = ipc::BufferedFrameDeserializer::Serialize(test_frame);
+  producer->SendStr(test_data);
+
+  base::UnixSocket* tcp_client_connection = nullptr;
+  auto tcp_client_connected =
+      task_runner.CreateCheckpoint("tcp_client_connected");
+  EXPECT_CALL(tcp_listener, OnNewIncomingConnection(_))
+      .WillOnce(Invoke([&](base::UnixSocket* client) {
+        tcp_client_connection = client;
+        tcp_client_connected();
+      }));
+  task_runner.RunUntilCheckpoint("tcp_client_connected");
+
+  // Asserts that we can receive the SetPeerIdentity message.
+  auto peer_identity_recv = task_runner.CreateCheckpoint("peer_identity_recv");
+  ipc::BufferedFrameDeserializer deserializer;
+  EXPECT_CALL(tcp_listener, OnDataAvailable(_))
+      .WillRepeatedly(Invoke([&](base::UnixSocket* tcp_conn) {
+        auto buf = deserializer.BeginReceive();
+        auto rsize = tcp_conn->Receive(buf.data, buf.size);
+        EXPECT_TRUE(deserializer.EndReceive(rsize));
+
+        auto frame = deserializer.PopNextFrame();
+        EXPECT_TRUE(frame->has_set_peer_identity());
+
+        const auto& set_peer_identity = frame->set_peer_identity();
+        EXPECT_EQ(set_peer_identity.pid(), getpid());
+        EXPECT_EQ(set_peer_identity.uid(), static_cast<int32_t>(geteuid()));
+
+        frame = deserializer.PopNextFrame();
+        EXPECT_EQ(1u, frame->data_for_testing().size());
+        EXPECT_EQ(std::string("test_data"), frame->data_for_testing()[0]);
+
+        peer_identity_recv();
+      }));
+  task_runner.RunUntilCheckpoint("peer_identity_recv");
+}
+
+}  // namespace
+}  // namespace perfetto
diff --git a/src/traced_relay/socket_relay_handler.cc b/src/traced_relay/socket_relay_handler.cc
index 2930c86..e8aa556 100644
--- a/src/traced_relay/socket_relay_handler.cc
+++ b/src/traced_relay/socket_relay_handler.cc
@@ -29,7 +29,6 @@
 #include "perfetto/ext/base/thread_checker.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/ext/base/watchdog.h"
-#include "perfetto/ext/base/watchdog_posix.h"
 
 namespace perfetto {
 namespace {
@@ -52,8 +51,8 @@
 void FdPoller::Poll() {
   PERFETTO_DCHECK_THREAD(thread_checker_);
 
-  int num_fds =
-      PERFETTO_EINTR(poll(&poll_fds_[0], poll_fds_.size(), kPollTimeoutMs));
+  int num_fds = PERFETTO_EINTR(poll(
+      &poll_fds_[0], static_cast<nfds_t>(poll_fds_.size()), kPollTimeoutMs));
   if (num_fds == -1 && base::IsAgain(errno))
     return;  // Poll again.
   PERFETTO_DCHECK(num_fds <= static_cast<int>(poll_fds_.size()));
diff --git a/src/traced_relay/socket_relay_handler_unittest.cc b/src/traced_relay/socket_relay_handler_unittest.cc
new file mode 100644
index 0000000..a96308a
--- /dev/null
+++ b/src/traced_relay/socket_relay_handler_unittest.cc
@@ -0,0 +1,204 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/traced_relay/socket_relay_handler.h"
+
+#include <chrono>
+#include <cstring>
+#include <memory>
+#include <random>
+#include <string>
+#include <thread>
+#include <utility>
+
+#include "perfetto/ext/base/threading/thread_pool.h"
+#include "perfetto/ext/base/unix_socket.h"
+
+#include "test/gtest_and_gmock.h"
+
+using testing::Values;
+
+namespace perfetto {
+namespace {
+
+using RawSocketPair = std::pair<base::UnixSocketRaw, base::UnixSocketRaw>;
+using RngValueType = std::minstd_rand0::result_type;
+
+struct TestClient {
+  RawSocketPair endpoint_sockets;
+  std::minstd_rand0 data_prng;
+  std::thread client_thread;
+};
+
+class SocketRelayHandlerTest : public ::testing::TestWithParam<uint32_t> {
+ protected:
+  void SetUp() override {
+    socket_relay_handler_ = std::make_unique<SocketRelayHandler>();
+
+    for (uint32_t i = 0; i < GetParam(); i++) {
+      TestClient client{SetUpEndToEndSockets(), std::minstd_rand0(i), {}};
+      test_clients_.push_back(std::move(client));
+    }
+  }
+  void TearDown() override { socket_relay_handler_ = nullptr; }
+
+  RawSocketPair SetUpEndToEndSockets() {
+    // Creates 2 SocketPairs:
+    // sock1 <-> sock2 <-> SocketRelayHandler <-> sock3 <-> sock4.
+    // sock2 and sock3 are transferred to the SocketRelayHandler.
+    // We test by reading and writing bidirectionally using sock1 and sock4.
+    auto [sock1, sock2] = base::UnixSocketRaw::CreatePairPosix(
+        base::SockFamily::kUnix, base::SockType::kStream);
+    sock2.SetBlocking(false);
+
+    auto [sock3, sock4] = base::UnixSocketRaw::CreatePairPosix(
+        base::SockFamily::kUnix, base::SockType::kStream);
+    sock3.SetBlocking(false);
+
+    auto socket_pair = std::make_unique<SocketPair>();
+    socket_pair->first.sock = std::move(sock2);
+    socket_pair->second.sock = std::move(sock3);
+
+    socket_relay_handler_->AddSocketPair(std::move(socket_pair));
+
+    RawSocketPair endpoint_sockets;
+    endpoint_sockets.first = std::move(sock1);
+    endpoint_sockets.second = std::move(sock4);
+
+    return endpoint_sockets;
+  }
+
+  std::unique_ptr<SocketRelayHandler> socket_relay_handler_;
+  std::vector<TestClient> test_clients_;
+  // Use fewer receiver threads than sender threads.
+  base::ThreadPool receiver_thread_pool_{1 + GetParam() / 10};
+};
+
+TEST(SocketWithBufferTest, EnqueueDequeue) {
+  SocketWithBuffer socket_with_buffer;
+  // No data initially.
+  EXPECT_EQ(0u, socket_with_buffer.data_size());
+
+  // Has room for writing some bytes into.
+  std::string data = "12345678901234567890";
+  EXPECT_GT(socket_with_buffer.available_bytes(), data.size());
+
+  memcpy(socket_with_buffer.buffer(), data.data(), data.size());
+  socket_with_buffer.EnqueueData(data.size());
+  EXPECT_EQ(data.size(), socket_with_buffer.data_size());
+
+  // Dequeue some bytes.
+  socket_with_buffer.DequeueData(5);
+  EXPECT_EQ(socket_with_buffer.data_size(), data.size() - 5);
+  std::string buffered_data(reinterpret_cast<char*>(socket_with_buffer.data()),
+                            socket_with_buffer.data_size());
+  EXPECT_EQ(buffered_data, "678901234567890");
+}
+
+// Test the SocketRelayHander with randomized request and response data.
+TEST_P(SocketRelayHandlerTest, RandomizedRequestResponse) {
+  // The max message size in the number of RNG calls.
+  constexpr size_t kMaxMsgSizeRng = 1 << 20;
+
+  // Create the threads for sending and receiving data through the
+  // SocketRelayHandler.
+  for (auto& client : test_clients_) {
+    auto* thread_pool = &receiver_thread_pool_;
+
+    auto thread_func = [&client, thread_pool]() {
+      auto& rng = client.data_prng;
+
+      // The max number of requests.
+      const size_t num_requests = rng() % 50;
+
+      for (size_t j = 0; j < num_requests; j++) {
+        auto& send_endpoint = client.endpoint_sockets.first;
+        auto& receive_endpoint = client.endpoint_sockets.second;
+
+        auto req_size = rng() % kMaxMsgSizeRng;
+
+        // Generate the random request.
+        std::vector<RngValueType> request;
+        request.reserve(req_size);
+        for (size_t r = 0; r < req_size; r++) {
+          request.emplace_back(rng());
+        }
+
+        // Create a buffer for receiving the request.
+        std::vector<RngValueType> received_request(request.size());
+
+        std::mutex mutex;
+        std::condition_variable cv;
+        std::unique_lock<std::mutex> lock(mutex);
+        bool done = false;
+
+        // Blocking receive on the thread pool.
+        thread_pool->PostTask([&]() {
+          const size_t bytes_to_receive =
+              received_request.size() * sizeof(RngValueType);
+          uint8_t* receive_buffer =
+              reinterpret_cast<uint8_t*>(received_request.data());
+          size_t bytes_received = 0;
+
+          // Perform a blocking read until we received the expected bytes.
+          while (bytes_received < bytes_to_receive) {
+            ssize_t rsize = PERFETTO_EINTR(
+                receive_endpoint.Receive(receive_buffer + bytes_received,
+                                         bytes_to_receive - bytes_received));
+            if (rsize <= 0)
+              break;
+            bytes_received += static_cast<size_t>(rsize);
+
+            std::this_thread::yield();  // Adds some scheduling randomness.
+          }
+
+          std::lock_guard<std::mutex> inner_lock(mutex);
+          done = true;
+          cv.notify_one();
+        });
+
+        // Perform a blocking send of the request data.
+        PERFETTO_EINTR(send_endpoint.Send(
+            request.data(), request.size() * sizeof(RngValueType)));
+
+        // Wait until the request is fully received.
+        cv.wait(lock, [&done] { return done; });
+
+        // Check data integrity.
+        EXPECT_EQ(request, received_request);
+
+        // Add some randomness to timing.
+        std::this_thread::sleep_for(std::chrono::microseconds(rng() % 1000));
+
+        // Emulate the response by reversing the data flow direction.
+        std::swap(send_endpoint, receive_endpoint);
+      }
+    };
+
+    client.client_thread = std::thread(std::move(thread_func));
+  }
+
+  for (auto& client : test_clients_) {
+    client.client_thread.join();
+  }
+}
+
+INSTANTIATE_TEST_SUITE_P(ByConnections,
+                         SocketRelayHandlerTest,
+                         Values(1, 5, 50));
+
+}  // namespace
+}  // namespace perfetto
diff --git a/src/tracing/core/tracing_service_impl.cc b/src/tracing/core/tracing_service_impl.cc
index 9040ded..45ce1eb 100644
--- a/src/tracing/core/tracing_service_impl.cc
+++ b/src/tracing/core/tracing_service_impl.cc
@@ -3470,6 +3470,11 @@
   protozero::HeapBuffered<protos::pbzero::TracePacket> packet;
   auto* info = packet->set_system_info();
   info->set_tracing_service_version(base::GetVersionString());
+
+  std::optional<int32_t> tzoff = base::GetTimezoneOffsetMins();
+  if (tzoff.has_value())
+    info->set_timezone_off_mins(*tzoff);
+
 #if !PERFETTO_BUILDFLAG(PERFETTO_OS_WIN) && \
     !PERFETTO_BUILDFLAG(PERFETTO_OS_NACL)
   struct utsname uname_info;
diff --git a/src/tracing/internal/track_event_internal.cc b/src/tracing/internal/track_event_internal.cc
index 5d67ad8..c594adb 100644
--- a/src/tracing/internal/track_event_internal.cc
+++ b/src/tracing/internal/track_event_internal.cc
@@ -53,6 +53,7 @@
 static constexpr const char kLegacySlowPrefix[] = "disabled-by-default-";
 static constexpr const char kSlowTag[] = "slow";
 static constexpr const char kDebugTag[] = "debug";
+static constexpr const char kFilteredEventName[] = "FILTERED";
 
 constexpr auto kClockIdIncremental =
     TrackEventIncrementalState::kClockIdIncremental;
@@ -515,7 +516,10 @@
 void TrackEventInternal::WriteEventName(perfetto::DynamicString event_name,
                                         perfetto::EventContext& event_ctx,
                                         const TrackEventTlsState& tls_state) {
-  if (PERFETTO_LIKELY(!tls_state.filter_dynamic_event_names)) {
+  if (PERFETTO_UNLIKELY(tls_state.filter_dynamic_event_names)) {
+    event_ctx.event()->set_name(kFilteredEventName,
+                                sizeof(kFilteredEventName) - 1);
+  } else {
     event_ctx.event()->set_name(event_name.value, event_name.length);
   }
 }
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 0660ada..707e775 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -3560,7 +3560,8 @@
     auto slices = StopSessionAndReadSlicesFromTrace(tracing_session);
     ASSERT_EQ(3u, slices.size());
     EXPECT_EQ("B:test.Event1", slices[0]);
-    EXPECT_EQ(filter_dynamic_names ? "B:test" : "B:test.Event2", slices[1]);
+    EXPECT_EQ(filter_dynamic_names ? "B:test.FILTERED" : "B:test.Event2",
+              slices[1]);
     EXPECT_EQ("B:test.Event3", slices[2]);
   }
 }
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 82%
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
index b2d9208..a85ef2c 100644
--- 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
@@ -72,6 +72,8 @@
     }
     [perfetto.protos.TestExtension.string_extension_for_testing]:
         "an extension string!"
+    [perfetto.protos.TestExtension.string_extension_for_testing2]:
+        "a second extension string!"
     [perfetto.protos.TestExtension.int_extension_for_testing]: 42
     [perfetto.protos.TestExtension.int_extension_for_testing]: 1337
     [perfetto.protos.TestExtension.omitted_extension_for_testing]:
@@ -99,6 +101,7 @@
     extension_set {
       file {
         package: "perfetto.protos"
+        name: "test_track_event_extensions.proto"
         message_type {
           extension {
             name: "string_extension_for_testing"
@@ -143,6 +146,30 @@
     }
   }
 }
+# Test that a field specified in a second descriptor for the same proto file is
+# also detected. This emulates the case of multiple instances of the same app
+# (e.g. Chrome Beta + Chrome Canary) both emitting a descriptor.
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 5001
+  extension_descriptor {
+    extension_set {
+      file {
+        package: "perfetto.protos"
+        name: "test_track_event_extensions.proto"
+        message_type {
+          extension {
+            name: "string_extension_for_testing2"
+            extendee: ".perfetto.protos.TrackEvent"
+            number: 9905
+            type: TYPE_STRING
+            label: LABEL_OPTIONAL
+          }
+        }
+      }
+    }
+  }
+}
 packet {
   trusted_packet_sequence_id: 1
   timestamp: 6000
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 96%
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
index cb6ade4..47a173a 100644
--- 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
@@ -37,3 +37,4 @@
 "source_id","source_id",1,"[NULL]"
 "source_location_iid","source_location_iid",1,"[NULL]"
 "string_extension_for_testing","string_extension_for_testing","[NULL]","an extension string!"
+"string_extension_for_testing2","string_extension_for_testing2","[NULL]","a second extension string!"
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/test/trace_processor/diff_tests/tables/tests.py b/test/trace_processor/diff_tests/tables/tests.py
index 83775a3..a6ff813 100644
--- a/test/trace_processor/diff_tests/tables/tests.py
+++ b/test/trace_processor/diff_tests/tables/tests.py
@@ -266,18 +266,52 @@
 
   def test_thread_state_flattened_aggregated(self):
     return DiffTestBlueprint(
-      trace=DataPath('android_monitor_contention_trace.atr'),
-      query="""
+        trace=DataPath('android_monitor_contention_trace.atr'),
+        query="""
       INCLUDE PERFETTO MODULE experimental.thread_state_flattened;
       select * from experimental_get_flattened_thread_state_aggregated(11155, NULL);
       """,
-      out=Path('thread_state_flattened_aggregated_csv.out'))
+        out=Path('thread_state_flattened_aggregated_csv.out'))
 
   def test_thread_state_flattened(self):
     return DiffTestBlueprint(
-      trace=DataPath('android_monitor_contention_trace.atr'),
-      query="""
+        trace=DataPath('android_monitor_contention_trace.atr'),
+        query="""
       INCLUDE PERFETTO MODULE experimental.thread_state_flattened;
       select * from experimental_get_flattened_thread_state(11155, NULL);
       """,
-      out=Path('thread_state_flattened_csv.out'))
+        out=Path('thread_state_flattened_csv.out'))
+
+  def test_metadata(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          system_info {
+            tracing_service_version: "Perfetto v38.0-0bb49ab54 (0bb49ab54dbe55ce5b9dfea3a2ada68b87aecb65)"
+            timezone_off_mins: 60
+            utsname {
+              sysname: "Darwin"
+              version: "Foobar"
+              machine: "x86_64"
+              release: "22.6.0"
+            }
+          }
+          trusted_uid: 158158
+          trusted_packet_sequence_id: 1
+        }
+        """),
+        query=r"""SELECT name, COALESCE(str_value, int_value) as val
+              FROM metadata
+              WHERE name IN (
+                  "system_name", "system_version", "system_machine",
+                  "system_release", "timezone_off_mins")
+              ORDER BY name
+        """,
+        out=Csv(r"""
+                "name","val"
+                "system_machine","x86_64"
+                "system_name","Darwin"
+                "system_release","22.6.0"
+                "system_version","Foobar"
+                "timezone_off_mins",60
+                """))
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index 552c7d3..176a8d5 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -182,6 +182,15 @@
 # The directory where the generated perfetto_build_flags.h will be copied into.
 buildflags_dir = 'include/perfetto/base/build_configs/android_tree'
 
+# Map of protos to transitive proto deps
+# When generating Android.bp files, these transitive deps will be added to
+# `tool_files` so that aprotoc can run inside a sandbox.
+# TODO(b/304495403): This should not be manually generated
+proto_to_transitive_proto_deps = {
+    'protos/perfetto/config/android/android_log_config.proto': [
+        'protos/perfetto/common/android_log_constants.proto',
+    ],
+}
 
 def enumerate_data_deps():
   with open(os.path.join(ROOT_DIR, 'tools', 'test_data.txt')) as f:
@@ -745,11 +754,20 @@
   blueprint.add_module(source_module)
   source_module.srcs.update(
       gn_utils.label_to_path(src) for src in target.sources)
+  # Add the imported proto file as a tool_file dep so that this action can be
+  # sandboxed.
+  tool_files = []
+  for proto, transitive_proto_deps in proto_to_transitive_proto_deps.items():
+    if proto in source_module.srcs:
+      tool_files.extend(transitive_proto_deps)
+
+  source_module.tool_files = tool_files
 
   header_module = Module('genrule', source_module_name + '_headers',
                          target.name)
   blueprint.add_module(header_module)
   header_module.srcs = set(source_module.srcs)
+  header_module.tool_files = tool_files
 
   # TODO(primiano): at some point we should remove this. This was introduced
   # by aosp/1108421 when adding "protos/" to .proto include paths, in order to
diff --git a/tools/install-build-deps b/tools/install-build-deps
index de0f53c..c3822f6 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -649,12 +649,22 @@
   parser.add_argument('--verify', help='Check all URLs', action='store_true')
   parser.add_argument(
       '--no-toolchain', help='Do not download toolchain', action='store_true')
+  parser.add_argument(
+      '--build-os',
+      default=system().lower(),
+      choices=['windows', 'darwin', 'linux'],
+      help='Override the autodetected build operating system')
+  parser.add_argument(
+      '--build-arch',
+      default=GetArch(),
+      choices=['arm64', 'x64'],
+      help='Override the autodetected build CPU architecture')
   args = parser.parse_args()
   if args.verify:
     CheckHashes()
     return 0
 
-  target_os = system().lower()
+  target_os = args.build_os
   if args.ui and target_os == 'windows':
     print('Building the UI on Windows is unsupported')
     return 1
@@ -680,7 +690,7 @@
     RmtreeIfExists(os.path.join(ROOT_DIR, old_dir))
 
   for dep in deps:
-    target_arch = GetArch()
+    target_arch = args.build_arch
     matches_os = dep.target_os == 'all' or target_os == dep.target_os
     matches_arch = dep.target_arch == 'all' or target_arch == dep.target_arch
     if not matches_os or not matches_arch:
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 0999b31..992bc7b 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -30,8 +30,7 @@
   tableColumnEquals,
   toggleEnabled,
 } from '../frontend/pivot_table_types';
-import {PrimaryTrackSortKey, TrackTags} from '../public/index';
-import {DebugTrackV2Config} from '../tracks/debug/slice_track';
+import {PrimaryTrackSortKey} from '../public/index';
 
 import {randomColor} from './colorizer';
 import {
@@ -77,21 +76,19 @@
   VisibleState,
 } from './state';
 
-export const DEBUG_SLICE_TRACK_KIND = 'DebugSliceTrack';
+export const DEBUG_SLICE_TRACK_KIND = 'dev.perfetto.DebugSliceTrack';
+export const DEBUG_COUNTER_TRACK_KIND = 'dev.perfetto.DebugCounterTrack';
 
 type StateDraft = Draft<State>;
 
 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 {
@@ -213,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) {
@@ -226,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.
@@ -236,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);
-        }
-      }
     }
   },
 
@@ -253,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) {
@@ -278,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;
@@ -292,58 +285,18 @@
       // 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,
       tracks: [args.summaryTrackId],
+      fixedOrdering: args.fixedOrdering,
     };
   },
 
-  addDebugTrack(
-      state: StateDraft,
-      args: {engineId: string, name: string, config: DebugTrackV2Config}):
-      void {
-        if (state.debugTrackId !== undefined) return;
-        const trackId = generateNextId(state);
-        this.addTrack(state, {
-          id: trackId,
-          engineId: args.engineId,
-          kind: DEBUG_SLICE_TRACK_KIND,
-          name: args.name,
-          trackSortKey: PrimaryTrackSortKey.DEBUG_SLICE_TRACK,
-          trackGroup: SCROLLING_TRACK_GROUP,
-          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);
-      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) {
@@ -379,6 +332,8 @@
     // rather than T1, T10, T11, ..., T2, T20, T21 .
     const coll = new Intl.Collator([], {sensitivity: 'base', numeric: true});
     for (const group of Object.values(state.trackGroups)) {
+      if (group.fixedOrdering) continue;
+
       group.tracks.sort((a: string, b: string) => {
         const aRank = getFullKey(a);
         const bRank = getFullKey(b);
@@ -801,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',
@@ -1138,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 1a8c68e..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,29 +219,22 @@
     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.
   state?: unknown;
+  fixedOrdering?: boolean;  // Render tracks without sorting.
 }
 
 export interface EngineConfig {
@@ -349,7 +343,7 @@
 export interface ChromeSliceSelection {
   kind: 'CHROME_SLICE';
   id: number;
-  table: string;
+  table?: string;
 }
 
 export interface ThreadStateSelection {
@@ -553,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 b4a8228..b72c20a 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -43,27 +43,18 @@
 import {
   ENABLE_SCROLL_JANK_PLUGIN_V2,
   getScrollJankTracks,
-  INPUT_LATENCY_TRACK,
 } from '../tracks/chrome_scroll_jank';
 import {
   decideTracks as scrollJankDecideTracks,
 } 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',
@@ -80,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();
 }
@@ -125,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;
   }
 
@@ -175,29 +158,14 @@
       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}`,
       });
     }
   }
 
-  async addScrollJankTracks(engine: Engine): Promise<void> {
-    const scrollJankTracks = getScrollJankTracks(engine);
-    const scrollJankTracksResult = await scrollJankTracks;
-    const originalLength = this.tracksToAdd.length;
-    this.tracksToAdd.length += scrollJankTracksResult.tracksToAdd.length;
-
-    for (let i = 0; i < scrollJankTracksResult.tracksToAdd.length; ++i) {
-      this.tracksToAdd[i + originalLength] =
-          scrollJankTracksResult.tracksToAdd[i];
-    }
-  }
-
   async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
     const cpus = await this.engine.getCpus();
 
@@ -223,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}`,
         });
       }
     }
@@ -276,20 +241,16 @@
       name: STR_NULL,
       parentName: STR_NULL,
       parentId: NUM_NULL,
-      trackIds: STR,
       maxDepth: NUM_NULL,
     });
 
     const parentIdToGroupId = new Map<number, string>();
-    let scrollJankRendered = false;
 
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
       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;
@@ -309,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,
@@ -330,23 +288,11 @@
         }
       }
 
-      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;
-      }
-      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);
@@ -366,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}`,
         });
       }
     }
@@ -416,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}`,
       });
     }
   }
@@ -462,7 +402,6 @@
     }
 
     const addGroup = Actions.addTrackGroup({
-      engineId: this.engineId,
       summaryTrackId,
       name: MEM_DMA_COUNTER_NAME,
       id,
@@ -500,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,
@@ -541,7 +477,6 @@
     }
 
     const addGroup = Actions.addTrackGroup({
-      engineId: this.engineId,
       summaryTrackId,
       name: group,
       id,
@@ -583,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,
@@ -614,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) {
@@ -627,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,
@@ -662,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;
@@ -681,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,
@@ -710,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) {
@@ -723,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,
@@ -750,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',
       });
     }
   }
@@ -776,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,
@@ -857,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,
@@ -897,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}`,
       });
     }
   }
@@ -951,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},
         });
       }
     }
@@ -1008,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},
       });
     }
   }
@@ -1061,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}`,
       });
     }
   }
@@ -1112,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;
@@ -1123,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,
-        },
       });
     }
   }
@@ -1167,7 +1057,6 @@
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
-      trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
       maxDepth: NUM_NULL,
@@ -1175,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;
@@ -1192,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,
-        },
       });
     }
   }
@@ -1231,7 +1113,6 @@
     const it = result.iter({
       upid: NUM,
       trackName: STR_NULL,
-      trackIds: STR,
       processName: STR_NULL,
       pid: NUM_NULL,
       maxDepth: NUM_NULL,
@@ -1240,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;
@@ -1257,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,
-        },
       });
     }
   }
@@ -1280,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
@@ -1296,7 +1169,6 @@
       isDefaultTrackForScope: NUM_NULL,
       tid: NUM_NULL,
       threadName: STR_NULL,
-      maxDepth: NUM,
       upid: NUM_NULL,
     });
     for (; it.valid(); it.next()) {
@@ -1308,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);
 
@@ -1316,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: {
@@ -1326,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: {
@@ -1346,7 +1210,6 @@
                 InThreadTrackSortKey.DEFAULT_TRACK :
                 InThreadTrackSortKey.ORDINARY,
           },
-          config: {sqlTrackId: trackId},
         });
       }
     }
@@ -1380,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}`,
       });
     }
   }
@@ -1402,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},
       });
     }
   }
@@ -1423,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},
       });
     }
   }
@@ -1503,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,
@@ -1680,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,
@@ -1737,49 +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 decideTracks(): Promise<DeferredAction[]> {
-    await this.defineMaxLayoutDepthSqlFunction();
+  async addScrollJankPluginTracks(): Promise<void> {
+    if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
+      const result = await getScrollJankTracks(this.engine);
+      this.tracksToAdd = this.tracksToAdd.concat(result.tracks.tracksToAdd);
+      this.addTrackGroupActions.push(result.addTrackGroup);
+    }
+  }
 
+  async decideTracks(): Promise<DeferredAction[]> {
     {
       const result = screenshotDecideTracks(this.engine);
       if (result !== null) {
@@ -1789,6 +1617,7 @@
     }
 
     // Add first the global tracks that don't require per-process track groups.
+    await this.addScrollJankPluginTracks();
     await this.addCpuSchedulingTracks();
     await this.addFtraceTrack(
         this.engine.getProxy('TrackDecider::addFtraceTrack'));
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
new file mode 100644
index 0000000..4cefe00
--- /dev/null
+++ b/ui/src/frontend/base_counter_track.ts
@@ -0,0 +1,549 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {searchSegment} from '../base/binary_search';
+import {assertTrue} from '../base/logging';
+import {duration, Time, time} from '../base/time';
+import {drawTrackHoverTooltip} from '../common/canvas_utils';
+import {raf} from '../core/raf_scheduler';
+import {LONG, NUM} from '../public';
+import {CounterScaleOptions} from '../tracks/counter';
+import {Button} from '../widgets/button';
+import {MenuItem, PopupMenu2} from '../widgets/menu';
+
+import {checkerboardExcept} from './checkerboard';
+import {globals} from './globals';
+import {constraintsToQuerySuffix} from './sql_utils';
+import {NewTrackArgs, TrackBase} from './track';
+import {CacheKey, TrackCache} from './track_cache';
+
+interface CounterData {
+  timestamps: BigInt64Array;
+  minValues: Float64Array;
+  maxValues: Float64Array;
+  lastValues: Float64Array;
+  totalDeltas: Float64Array;
+  rate: Float64Array;
+  maximumValue: number;
+  minimumValue: number;
+  maximumDelta: number;
+  minimumDelta: number;
+  maximumRate: number;
+  minimumRate: number;
+}
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 3.5;
+
+export abstract class BaseCounterTrack<Config> extends TrackBase<Config> {
+  protected readonly tableName: string;
+
+  // This is the over-skirted cached bounds:
+  private countersKey: CacheKey = CacheKey.zero();
+
+  private counters: CounterData = {
+    timestamps: new BigInt64Array(0),
+    minValues: new Float64Array(0),
+    maxValues: new Float64Array(0),
+    lastValues: new Float64Array(0),
+    totalDeltas: new Float64Array(0),
+    rate: new Float64Array(0),
+    maximumValue: 0,
+    minimumValue: 0,
+    maximumDelta: 0,
+    minimumDelta: 0,
+    maximumRate: 0,
+    minimumRate: 0,
+  };
+
+  private cache: TrackCache<CounterData> = new TrackCache(5);
+
+  private sqlState: 'UNINITIALIZED'|'INITIALIZING'|'QUERY_PENDING'|
+      'QUERY_DONE' = 'UNINITIALIZED';
+  private isDestroyed: boolean = false;
+
+  private maximumValueSeen = 0;
+  private minimumValueSeen = 0;
+  private maximumDeltaSeen = 0;
+  private minimumDeltaSeen = 0;
+  private maxDurNs: duration = 0n;
+
+  private mousePos = {x: 0, y: 0};
+  private hoveredValue: number|undefined = undefined;
+  private hoveredTs: time|undefined = undefined;
+  private hoveredTsEnd: time|undefined = undefined;
+
+  private scale?: CounterScaleOptions;
+
+  // Extension points.
+  abstract initSqlTable(_tableName: string): Promise<void>;
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+    this.tableName = `track_${this.trackId}`.replace(/[^a-zA-Z0-9_]+/g, '_');
+  }
+
+  getHeight() {
+    return 30;
+  }
+
+  getCounterContextMenuItems(): m.Children {
+    const currentScale = this.scale;
+    const scales: {name: CounterScaleOptions, humanName: string}[] = [
+      {name: 'ZERO_BASED', humanName: 'Zero based'},
+      {name: 'MIN_MAX', humanName: 'Min/Max'},
+      {name: 'DELTA_FROM_PREVIOUS', humanName: 'Delta'},
+      {name: 'RATE', humanName: 'Rate'},
+    ];
+    return scales.map((scale) => {
+      return m(MenuItem, {
+        label: scale.humanName,
+        active: currentScale === scale.name,
+        onclick: () => {
+          this.scale = scale.name;
+          raf.scheduleFullRedraw();
+        },
+      });
+    });
+  }
+
+  getCounterContextMenu(): m.Child {
+    return m(
+        PopupMenu2,
+        {
+          trigger: m(Button, {icon: 'show_chart', minimal: true}),
+        },
+        this.getCounterContextMenuItems(),
+    );
+  }
+
+  getTrackShellButtons(): m.Children {
+    return this.getCounterContextMenu();
+  }
+
+  renderCanvas(ctx: CanvasRenderingContext2D) {
+    const {
+      visibleTimeScale: timeScale,
+      visibleWindowTime: vizTime,
+      windowSpan,
+    } = globals.frontendLocalState;
+
+    {
+      const windowSizePx = Math.max(1, timeScale.pxSpan.delta);
+      const rawStartNs = vizTime.start.toTime();
+      const rawEndNs = vizTime.end.toTime();
+      const rawCountersKey =
+          CacheKey.create(rawStartNs, rawEndNs, windowSizePx);
+
+      // If the visible time range is outside the cached area, requests
+      // asynchronously new data from the SQL engine.
+      this.maybeRequestData(rawCountersKey);
+    }
+
+    // In any case, draw whatever we have (which might be stale/incomplete).
+
+    if (this.counters === undefined || this.counters.timestamps.length === 0) {
+      return;
+    }
+
+    const data = this.counters;
+    assertTrue(data.timestamps.length === data.minValues.length);
+    assertTrue(data.timestamps.length === data.maxValues.length);
+    assertTrue(data.timestamps.length === data.lastValues.length);
+    assertTrue(data.timestamps.length === data.totalDeltas.length);
+    assertTrue(data.timestamps.length === data.rate.length);
+
+    const scale: CounterScaleOptions = this.scale ?? 'ZERO_BASED';
+
+    let minValues = data.minValues;
+    let maxValues = data.maxValues;
+    let lastValues = data.lastValues;
+    let maximumValue = data.maximumValue;
+    let minimumValue = data.minimumValue;
+    if (scale === 'DELTA_FROM_PREVIOUS') {
+      lastValues = data.totalDeltas;
+      minValues = data.totalDeltas;
+      maxValues = data.totalDeltas;
+      maximumValue = data.maximumDelta;
+      minimumValue = data.minimumDelta;
+    }
+    if (scale === 'RATE') {
+      lastValues = data.rate;
+      minValues = data.rate;
+      maxValues = data.rate;
+      maximumValue = data.maximumRate;
+      minimumValue = data.minimumRate;
+    }
+
+    const effectiveHeight = this.getHeight() - MARGIN_TOP;
+    const endPx = windowSpan.end;
+    const zeroY = MARGIN_TOP + effectiveHeight / (minimumValue < 0 ? 2 : 1);
+
+    // Quantize the Y axis to quarters of powers of tens (7.5K, 10K, 12.5K).
+    const maxValue = Math.max(maximumValue, 0);
+
+    let yMax = Math.max(Math.abs(minimumValue), maxValue);
+    const kUnits = ['', 'K', 'M', 'G', 'T', 'E'];
+    const exp = Math.ceil(Math.log10(Math.max(yMax, 1)));
+    const pow10 = Math.pow(10, exp);
+    yMax = Math.ceil(yMax / (pow10 / 4)) * (pow10 / 4);
+    let yRange = 0;
+    const unitGroup = Math.floor(exp / 3);
+    let yMin = 0;
+    let yLabel = '';
+    if (scale === 'MIN_MAX') {
+      yRange = maximumValue - minimumValue;
+      yMin = minimumValue;
+      yLabel = 'min - max';
+    } else {
+      yRange = minimumValue < 0 ? yMax * 2 : yMax;
+      yMin = minimumValue < 0 ? -yMax : 0;
+      yLabel = `${yMax / Math.pow(10, unitGroup * 3)} ${kUnits[unitGroup]}`;
+      if (scale === 'DELTA_FROM_PREVIOUS') {
+        yLabel += '\u0394';
+      } else if (scale === 'RATE') {
+        yLabel += '\u0394/t';
+      }
+    }
+
+    // There are 360deg of hue. We want a scale that starts at green with
+    // exp <= 3 (<= 1KB), goes orange around exp = 6 (~1MB) and red/violet
+    // around exp >= 9 (1GB).
+    // The hue scale looks like this:
+    // 0                              180                                 360
+    // Red        orange         green | blue         purple          magenta
+    // So we want to start @ 180deg with pow=0, go down to 0deg and then wrap
+    // back from 360deg back to 180deg.
+    const expCapped = Math.min(Math.max(exp - 3), 9);
+    const hue = (180 - Math.floor(expCapped * (180 / 6)) + 360) % 360;
+
+    ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
+    ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
+
+    const calculateX = (ts: time) => {
+      return Math.floor(timeScale.timeToPx(ts));
+    };
+    const calculateY = (value: number) => {
+      return MARGIN_TOP + effectiveHeight -
+          Math.round(((value - yMin) / yRange) * effectiveHeight);
+    };
+
+    ctx.beginPath();
+    const timestamp = Time.fromRaw(data.timestamps[0]);
+    ctx.moveTo(calculateX(timestamp), zeroY);
+    let lastDrawnY = zeroY;
+    for (let i = 0; i < this.counters.timestamps.length; i++) {
+      const timestamp = Time.fromRaw(data.timestamps[i]);
+      const x = calculateX(timestamp);
+      const minY = calculateY(minValues[i]);
+      const maxY = calculateY(maxValues[i]);
+      const lastY = calculateY(lastValues[i]);
+
+      ctx.lineTo(x, lastDrawnY);
+      if (minY === maxY) {
+        assertTrue(lastY === minY);
+        ctx.lineTo(x, lastY);
+      } else {
+        ctx.lineTo(x, minY);
+        ctx.lineTo(x, maxY);
+        ctx.lineTo(x, lastY);
+      }
+      lastDrawnY = lastY;
+    }
+    ctx.lineTo(endPx, lastDrawnY);
+    ctx.lineTo(endPx, zeroY);
+    ctx.closePath();
+    ctx.fill();
+    ctx.stroke();
+
+    // Draw the Y=0 dashed line.
+    ctx.strokeStyle = `hsl(${hue}, 10%, 71%)`;
+    ctx.beginPath();
+    ctx.setLineDash([2, 4]);
+    ctx.moveTo(0, zeroY);
+    ctx.lineTo(endPx, zeroY);
+    ctx.closePath();
+    ctx.stroke();
+    ctx.setLineDash([]);
+
+    ctx.font = '10px Roboto Condensed';
+
+    if (this.hoveredValue !== undefined && this.hoveredTs !== undefined) {
+      // TODO(hjd): Add units.
+      let text: string;
+      if (scale === 'DELTA_FROM_PREVIOUS') {
+        text = 'delta: ';
+      } else if (scale === 'RATE') {
+        text = 'delta/t: ';
+      } else {
+        text = 'value: ';
+      }
+
+      text += `${this.hoveredValue.toLocaleString()}`;
+
+      ctx.fillStyle = `hsl(${hue}, 45%, 75%)`;
+      ctx.strokeStyle = `hsl(${hue}, 45%, 45%)`;
+
+      const xStart = Math.floor(timeScale.timeToPx(this.hoveredTs));
+      const xEnd = this.hoveredTsEnd === undefined ?
+          endPx :
+          Math.floor(timeScale.timeToPx(this.hoveredTsEnd));
+      const y = MARGIN_TOP + effectiveHeight -
+          Math.round(((this.hoveredValue - yMin) / yRange) * effectiveHeight);
+
+      // Highlight line.
+      ctx.beginPath();
+      ctx.moveTo(xStart, y);
+      ctx.lineTo(xEnd, y);
+      ctx.lineWidth = 3;
+      ctx.stroke();
+      ctx.lineWidth = 1;
+
+      // Draw change marker.
+      ctx.beginPath();
+      ctx.arc(
+          xStart, y, 3 /* r*/, 0 /* start angle*/, 2 * Math.PI /* end angle*/);
+      ctx.fill();
+      ctx.stroke();
+
+      // Draw the tooltip.
+      drawTrackHoverTooltip(ctx, this.mousePos, this.getHeight(), text);
+    }
+
+    // Write the Y scale on the top left corner.
+    ctx.fillStyle = 'rgba(255, 255, 255, 0.6)';
+    ctx.fillRect(0, 0, 42, 16);
+    ctx.fillStyle = '#666';
+    ctx.textAlign = 'left';
+    ctx.textBaseline = 'alphabetic';
+    ctx.fillText(`${yLabel}`, 5, 14);
+
+    // TODO(hjd): Refactor this into checkerboardExcept
+    {
+      const counterEndPx = Infinity;
+      // Grey out RHS.
+      if (counterEndPx < endPx) {
+        ctx.fillStyle = '#0000001f';
+        ctx.fillRect(counterEndPx, 0, endPx - counterEndPx, this.getHeight());
+      }
+    }
+
+    // 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(),
+        windowSpan.start,
+        windowSpan.end,
+        timeScale.timeToPx(this.countersKey.start),
+        timeScale.timeToPx(this.countersKey.end));
+  }
+
+  onMouseMove(pos: {x: number, y: number}) {
+    const data = this.counters;
+    if (data === undefined) return;
+    this.mousePos = pos;
+    const {visibleTimeScale} = globals.frontendLocalState;
+    const time = visibleTimeScale.pxToHpTime(pos.x);
+
+    let values = data.lastValues;
+    if (this.scale === 'DELTA_FROM_PREVIOUS') {
+      values = data.totalDeltas;
+    }
+    if (this.scale === 'RATE') {
+      values = data.rate;
+    }
+
+    const [left, right] = searchSegment(data.timestamps, time.toTime());
+    this.hoveredTs =
+        left === -1 ? undefined : Time.fromRaw(data.timestamps[left]);
+    this.hoveredTsEnd =
+        right === -1 ? undefined : Time.fromRaw(data.timestamps[right]);
+    this.hoveredValue = left === -1 ? undefined : values[left];
+  }
+
+  onMouseOut() {
+    this.hoveredValue = undefined;
+    this.hoveredTs = undefined;
+  }
+
+  // The underlying table has `ts` and `value` columns, but we also want to
+  // query `dur` and `delta` - we create a CTE to help with that.
+  private getSqlPreamble(): string {
+    return `
+      WITH data AS (
+        SELECT
+          ts,
+          value,
+          lead(ts, 1, ts) over (order by ts) - ts as dur,
+          lead(value, 1, value) over (order by ts) - value as delta
+        FROM ${this.tableName}
+      )
+    `;
+  }
+
+  private async maybeRequestData(rawCountersKey: CacheKey) {
+    // Important: this method is async and is invoked on every frame. Care
+    // must be taken to avoid piling up queries on every frame, hence the FSM.
+    // TODO(altimin): Currently this is a copy of the logic in base_slice_track.
+    // Consider merging it.
+    if (this.sqlState === 'UNINITIALIZED') {
+      this.sqlState = 'INITIALIZING';
+
+      if (this.isDestroyed) {
+        return;
+      }
+      await this.initSqlTable(this.tableName);
+
+      if (this.isDestroyed) {
+        return;
+      }
+
+      {
+        const queryRes = (await this.engine.query(`
+          ${this.getSqlPreamble()}
+          SELECT
+            ifnull(max(value), 0) as maxValue,
+            ifnull(min(value), 0) as minValue,
+            ifnull(max(delta), 0) as maxDelta,
+            ifnull(min(delta), 0) as minDelta,
+            max(
+              iif(dur != -1, dur, (select end_ts from trace_bounds) - ts)
+            ) as maxDur
+          FROM data
+        `)).firstRow({
+          maxValue: NUM,
+          minValue: NUM,
+          maxDelta: NUM,
+          minDelta: NUM,
+          maxDur: LONG,
+        });
+
+        this.minimumValueSeen = queryRes.minValue;
+        this.maximumValueSeen = queryRes.maxValue;
+        this.minimumDeltaSeen = queryRes.minDelta;
+        this.maximumDeltaSeen = queryRes.maxDelta;
+        this.maxDurNs = queryRes.maxDur;
+      }
+
+      this.sqlState = 'QUERY_DONE';
+    } else if (
+        this.sqlState === 'INITIALIZING' || this.sqlState === 'QUERY_PENDING') {
+      return;
+    }
+
+    if (rawCountersKey.isCoveredBy(this.countersKey)) {
+      return;  // We have the data already, no need to re-query.
+    }
+
+    const countersKey = rawCountersKey.normalize();
+    if (!rawCountersKey.isCoveredBy(countersKey)) {
+      throw new Error(`Normalization error ${countersKey.toString()} ${
+          rawCountersKey.toString()}`);
+    }
+
+    const maybeCachedCounters = this.cache.lookup(countersKey);
+    if (maybeCachedCounters) {
+      this.countersKey = countersKey;
+      this.counters = maybeCachedCounters;
+    }
+
+    this.sqlState = 'QUERY_PENDING';
+    const bucketNs = countersKey.bucketSize;
+
+    const constraint = constraintsToQuerySuffix({
+      filters: [
+        `ts >= ${countersKey.start} - ${this.maxDurNs}`,
+        `ts <= ${countersKey.end}`,
+      ],
+      groupBy: [
+        'tsq',
+      ],
+      orderBy: [
+        'tsq',
+      ],
+    });
+
+    if (this.isDestroyed) {
+      this.sqlState = 'QUERY_DONE';
+      return;
+    }
+
+    const queryRes = await this.engine.query(`
+      ${this.getSqlPreamble()}
+      SELECT
+        (ts + ${bucketNs / 2n}) / ${bucketNs} * ${bucketNs} as tsq,
+        min(value) as minValue,
+        max(value) as maxValue,
+        sum(delta) as totalDelta,
+        value_at_max_ts(ts, value) as lastValue
+      FROM data
+      ${constraint}
+    `);
+
+    const it = queryRes.iter({
+      tsq: LONG,
+      minValue: NUM,
+      maxValue: NUM,
+      totalDelta: NUM,
+      lastValue: NUM,
+    });
+
+    const numRows = queryRes.numRows();
+    const data: CounterData = {
+      maximumValue: this.maximumValueSeen,
+      minimumValue: this.minimumValueSeen,
+      maximumDelta: this.maximumDeltaSeen,
+      minimumDelta: this.minimumDeltaSeen,
+      maximumRate: 0,
+      minimumRate: 0,
+      timestamps: new BigInt64Array(numRows),
+      minValues: new Float64Array(numRows),
+      maxValues: new Float64Array(numRows),
+      lastValues: new Float64Array(numRows),
+      totalDeltas: new Float64Array(numRows),
+      rate: new Float64Array(numRows),
+    };
+
+    let lastValue = 0;
+    let lastTs = 0n;
+    for (let row = 0; it.valid(); it.next(), row++) {
+      const ts = Time.fromRaw(it.tsq);
+      const value = it.lastValue;
+      const rate = (value - lastValue) / (Time.toSeconds(Time.sub(ts, lastTs)));
+      lastTs = ts;
+      lastValue = value;
+
+      data.timestamps[row] = ts;
+      data.minValues[row] = it.minValue;
+      data.maxValues[row] = it.maxValue;
+      data.lastValues[row] = value;
+      data.totalDeltas[row] = it.totalDelta;
+      data.rate[row] = rate;
+      if (row > 0) {
+        data.rate[row - 1] = rate;
+        data.maximumRate = Math.max(data.maximumRate, rate);
+        data.minimumRate = Math.min(data.minimumRate, rate);
+      }
+    }
+
+    this.cache.insert(countersKey, data);
+    this.counters = data;
+
+    this.sqlState = 'QUERY_DONE';
+    raf.scheduleRedraw();
+  }
+}
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/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 93b5e15..a298b36 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -20,7 +20,7 @@
 import {EngineProxy} from '../common/engine';
 import {runQuery} from '../common/queries';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../common/query_result';
-import {addDebugTrack} from '../tracks/debug/slice_track';
+import {addDebugSliceTrack} from '../tracks/debug/slice_track';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
 import {DurationWidget} from '../widgets/duration';
@@ -99,12 +99,14 @@
     run: (slice: SliceDetails) => {
       const engine = getEngine();
       if (engine === undefined) return;
-      runQuery(`
+      runQuery(
+          `
         INCLUDE PERFETTO MODULE android.binder;
         INCLUDE PERFETTO MODULE android.monitor_contention;
-      `, engine)
+      `,
+          engine)
           .then(
-              () => addDebugTrack(
+              () => addDebugSliceTrack(
                   engine,
                   {
                     sqlSource: `
@@ -138,7 +140,8 @@
                                   JOIN thread ON thread.utid = thread_track.utid
                                   JOIN process ON process.upid = thread.upid
                                   WHERE process.pid = ${getPidFromSlice(slice)}
-                                        AND thread.tid = ${getTidFromSlice(slice)}
+                                        AND thread.tid = ${
+                        getTidFromSlice(slice)}
                                         AND short_blocked_method IS NOT NULL
                                   ORDER BY depth
                                 ) SELECT ts, dur, name FROM merged`,
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/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index 4e25723..35d3b9f 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -17,7 +17,7 @@
 import {Duration, time} from '../base/time';
 import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
-import {addDebugTrack} from '../tracks/debug/slice_track';
+import {addDebugSliceTrack} from '../tracks/debug/slice_track';
 import {Anchor} from '../widgets/anchor';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
@@ -292,7 +292,7 @@
            {
           label: 'Critical path lite',
           onclick: () => runQuery(`INCLUDE PERFETTO MODULE experimental.thread_executing_span;`, this.engine)
-              .then(() => addDebugTrack(
+              .then(() => addDebugSliceTrack(
               this.engine,
                   {
                     sqlSource:
@@ -324,7 +324,7 @@
            {
           label: 'Critical path',
           onclick: () => runQuery(`INCLUDE PERFETTO MODULE experimental.thread_executing_span;`, this.engine)
-              .then(() => addDebugTrack(
+              .then(() => addDebugSliceTrack(
               this.engine,
                   {
                     sqlSource:
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 60ab2b8..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;
 }
@@ -245,7 +232,7 @@
 // (for non-thread tracks) or a tid and secondary sort key (mapping of tid to
 // primary sort key is done independently).
 export enum PrimaryTrackSortKey {
-  DEBUG_SLICE_TRACK,
+  DEBUG_TRACK,
   NULL_TRACK,
   PROCESS_SCHEDULING_TRACK,
   PROCESS_SUMMARY_TRACK,
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 cfcaada..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,16 +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 {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {
   NamedSliceTrackTypes,
@@ -36,20 +29,24 @@
 
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } 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,84 +114,16 @@
   // 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: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;
diff --git a/ui/src/tracks/chrome_scroll_jank/index.ts b/ui/src/tracks/chrome_scroll_jank/index.ts
index 592d9a5..b8c028f 100644
--- a/ui/src/tracks/chrome_scroll_jank/index.ts
+++ b/ui/src/tracks/chrome_scroll_jank/index.ts
@@ -12,22 +12,40 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {AddTrackArgs} from '../../common/actions';
+import {v4 as uuidv4} from 'uuid';
+
+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, PluginDescriptor} from '../../public';
+import {
+  Plugin,
+  PluginContext,
+  PluginContextTrace,
+  PluginDescriptor,
+  PrimaryTrackSortKey,
+} from '../../public';
 import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
+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',
@@ -36,8 +54,6 @@
   defaultValue: false,
 });
 
-export const INPUT_LATENCY_TRACK = 'InputLatency::';
-
 export const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
   id: 'enableScrollJankPluginV2',
   name: 'Enable Scroll Jank plugin V2',
@@ -45,10 +61,16 @@
   defaultValue: false,
 });
 
+export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
+
 export type ScrollJankTracks = {
   tracksToAdd: AddTrackArgs[],
 };
 
+export type ScrollJankTrackGroup = {
+  tracks: ScrollJankTracks; addTrackGroup: DeferredAction
+}
+
 export interface ScrollJankTrackSpec {
   id: string;
   sqlTableName: string;
@@ -94,45 +116,185 @@
   }
 }
 
-export async function getScrollJankTracks(engine: Engine):
-    Promise<ScrollJankTracks> {
+export async function getScrollJankTracks(_engine: Engine):
+    Promise<ScrollJankTrackGroup> {
   const result: ScrollJankTracks = {
     tracksToAdd: [],
   };
 
-  const scrolls = addTopLevelScrollTrack(engine);
-  const scrollsResult = await scrolls;
-  let originalLength = result.tracksToAdd.length;
-  result.tracksToAdd.length += scrollsResult.tracksToAdd.length;
-  for (let i = 0; i < scrollsResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = scrollsResult.tracksToAdd[i];
-  }
+  const scrolls = await addTopLevelScrollTrack();
+  result.tracksToAdd = result.tracksToAdd.concat(scrolls.tracksToAdd);
 
-  const janks = addScrollJankV3ScrollTrack(engine);
-  const janksResult = await janks;
-  originalLength = result.tracksToAdd.length;
-  result.tracksToAdd.length += janksResult.tracksToAdd.length;
-  for (let i = 0; i < janksResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = janksResult.tracksToAdd[i];
-  }
+  const janks = await addScrollJankV3ScrollTrack();
+  result.tracksToAdd = result.tracksToAdd.concat(janks.tracksToAdd);
 
-  originalLength = result.tracksToAdd.length;
-  const eventLatencies = addLatencyTracks(engine);
-  const eventLatencyResult = await eventLatencies;
-  result.tracksToAdd.length += eventLatencyResult.tracksToAdd.length;
-  for (let i = 0; i < eventLatencyResult.tracksToAdd.length; ++i) {
-    result.tracksToAdd[i + originalLength] = eventLatencyResult.tracksToAdd[i];
-  }
+  const eventLatencies = await addLatencyTracks();
+  result.tracksToAdd = result.tracksToAdd.concat(eventLatencies.tracksToAdd);
 
-  return result;
+  const summaryTrackId = uuidv4();
+  result.tracksToAdd.push({
+    uri: NULL_TRACK_URI,
+    trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
+    name: '',  // TODO(stevegolton): We should probably put some name here.
+    trackGroup: undefined,
+    id: summaryTrackId,
+  });
+
+  const addTrackGroup = Actions.addTrackGroup({
+    name: 'Chrome Scroll Jank',
+    id: SCROLL_JANK_GROUP_ID,
+    collapsed: false,
+    summaryTrackId,
+    fixedOrdering: true,
+  });
+
+  return {tracks: result, addTrackGroup};
 }
 
 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 f1db56e..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,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 {
-  SCROLLING_TRACK_GROUP,
-} from '../../common/state';
 import {globals} from '../../frontend/globals';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
@@ -33,14 +27,13 @@
 
 import {EventLatencyTrackTypes} from './event_latency_track';
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } from './index';
 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';
 
@@ -127,23 +120,17 @@
   }
 }
 
-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: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;
diff --git a/ui/src/tracks/chrome_scroll_jank/scroll_track.ts b/ui/src/tracks/chrome_scroll_jank/scroll_track.ts
index 97ccc4a..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 {SCROLLING_TRACK_GROUP} from '../../common/state';
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {NewTrackArgs, TrackBase} from '../../frontend/track';
 import {PrimaryTrackSortKey} from '../../public';
@@ -24,19 +20,19 @@
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../custom_sql_table_slices';
-
 import {
+  SCROLL_JANK_GROUP_ID,
   ScrollJankPluginState,
   ScrollJankTracks as DecideTracksResult,
 } 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,25 +72,16 @@
   }
 }
 
-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: SCROLLING_TRACK_GROUP,
+    trackGroup: SCROLL_JANK_GROUP_ID,
   });
 
   return result;
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/add_debug_track_menu.ts b/ui/src/tracks/debug/add_debug_track_menu.ts
index e7712a0..ccdc71d 100644
--- a/ui/src/tracks/debug/add_debug_track_menu.ts
+++ b/ui/src/tracks/debug/add_debug_track_menu.ts
@@ -16,11 +16,13 @@
 
 import {findRef} from '../../base/dom_utils';
 import {EngineProxy} from '../../common/engine';
+import {raf} from '../../core/raf_scheduler';
 import {Form, FormLabel} from '../../widgets/form';
 import {Select} from '../../widgets/select';
 import {TextInput} from '../../widgets/text_input';
 
-import {addDebugTrack, SliceColumns, SqlDataSource} from './slice_track';
+import {addDebugCounterTrack} from './counter_track';
+import {addDebugSliceTrack, SqlDataSource} from './slice_track';
 
 export const ARG_PREFIX = 'arg_';
 
@@ -40,11 +42,13 @@
   readonly columns: string[];
 
   name: string = '';
-  sliceColumns: SliceColumns;
-  arrangeBy?: {
-    type: 'thread'|'process',
-    column: string,
-  };
+  trackType: 'slice'|'counter' = 'slice';
+  // Names of columns which will be used as data sources for rendering.
+  // We store the config for all possible columns used for rendering (i.e.
+  // 'value' for slice and 'name' for counter) and then just don't the values
+  // which don't match the currently selected track type (so changing track type
+  // from A to B and back to A is a no-op).
+  renderParams: {ts: string; dur: string; name: string; value: string;};
 
   constructor(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
     this.columns = [...vnode.attrs.dataSource.columns];
@@ -64,10 +68,11 @@
       return this.columns[0];
     };
 
-    this.sliceColumns = {
+    this.renderParams = {
       ts: chooseDefaultOption('ts'),
       dur: chooseDefaultOption('dur'),
       name: chooseDefaultOption('name'),
+      value: chooseDefaultOption('value'),
     };
   }
 
@@ -84,21 +89,46 @@
     }
   }
 
+  private renderTrackTypeSelect() {
+    const options = [];
+    for (const type of ['slice', 'counter']) {
+      options.push(
+          m('option',
+            {
+              value: type,
+              selected: this.trackType === type ? true : undefined,
+            },
+            type));
+    }
+    return m(
+        Select,
+        {
+          id: 'track_type',
+          oninput: (e: Event) => {
+            if (!e.target) return;
+            this.trackType =
+                (e.target as HTMLSelectElement).value as 'slice' | 'counter';
+            raf.scheduleFullRedraw();
+          },
+        },
+        options);
+  }
+
   view(vnode: m.Vnode<AddDebugTrackMenuAttrs>) {
-    const renderSelect = (name: 'ts'|'dur'|'name') => {
+    const renderSelect = (name: 'ts'|'dur'|'name'|'value') => {
       const options = [];
       for (const column of this.columns) {
         options.push(
             m('option',
               {
-                selected: this.sliceColumns[name] === column ? true : undefined,
+                selected: this.renderParams[name] === column ? true : undefined,
               },
               column));
       }
       if (name === 'dur') {
         options.push(
             m('option',
-              {selected: this.sliceColumns[name] === '0' ? true : undefined},
+              {selected: this.renderParams[name] === '0' ? true : undefined},
               m('i', '0')));
       }
       return [
@@ -111,7 +141,7 @@
             id: name,
             oninput: (e: Event) => {
               if (!e.target) return;
-              this.sliceColumns[name] = (e.target as HTMLSelectElement).value;
+              this.renderParams[name] = (e.target as HTMLSelectElement).value;
             },
           },
           options),
@@ -121,12 +151,27 @@
         Form,
         {
           onSubmit: () => {
-            addDebugTrack(
-                vnode.attrs.engine,
-                vnode.attrs.dataSource,
-                this.name,
-                this.sliceColumns,
-                this.columns);
+            switch (this.trackType) {
+              case 'slice':
+                addDebugSliceTrack(
+                    vnode.attrs.engine,
+                    vnode.attrs.dataSource,
+                    this.name,
+                    {
+                      ts: this.renderParams.ts,
+                      dur: this.renderParams.dur,
+                      name: this.renderParams.name,
+                    },
+                    this.columns);
+                break;
+              case 'counter':
+                addDebugCounterTrack(
+                    vnode.attrs.engine, vnode.attrs.dataSource, this.name, {
+                      ts: this.renderParams.ts,
+                      value: this.renderParams.value,
+                    });
+                break;
+            }
           },
           submitLabel: 'Show',
         },
@@ -146,9 +191,15 @@
             this.name = (e.target as HTMLInputElement).value;
           },
         }),
+        m(FormLabel,
+          {for: 'track_type',
+          },
+          'Track type'),
+        this.renderTrackTypeSelect(),
         renderSelect('ts'),
-        renderSelect('dur'),
-        renderSelect('name'),
+        this.trackType === 'slice' && renderSelect('dur'),
+        this.trackType === 'slice' && renderSelect('name'),
+        this.trackType === 'counter' && renderSelect('value'),
     );
   }
 }
diff --git a/ui/src/tracks/debug/counter_track.ts b/ui/src/tracks/debug/counter_track.ts
new file mode 100644
index 0000000..adcc92d
--- /dev/null
+++ b/ui/src/tracks/debug/counter_track.ts
@@ -0,0 +1,116 @@
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import m from 'mithril';
+
+import {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} 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 {
+  ts: string;
+  value: string;
+}
+
+export interface CounterDebugTrackConfig {
+  sqlTableName: string;
+  columns: CounterColumns;
+}
+
+export class DebugCounterTrack extends
+    BaseCounterTrack<CounterDebugTrackConfig> {
+  static readonly kind = DEBUG_COUNTER_TRACK_KIND;
+
+  static create(args: NewTrackArgs) {
+    return new DebugCounterTrack(args);
+  }
+
+  constructor(args: NewTrackArgs) {
+    super(args);
+  }
+
+  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> {
+    await this.engine.query(`
+      create view ${tableName} as
+      select * from ${this.config.sqlTableName};
+    `);
+  }
+}
+
+let debugTrackCount = 0;
+
+export interface SqlDataSource {
+  // SQL source selecting the necessary data.
+  sqlSource: string;
+  // The caller is responsible for ensuring that the number of items in this
+  // list matches the number of columns returned by sqlSource.
+  columns: string[];
+}
+
+export async function addDebugCounterTrack(
+    engine: EngineProxy,
+    data: SqlDataSource,
+    trackName: string,
+    columns: CounterColumns) {
+  // To prepare displaying the provided data as a track, materialize it and
+  // compute depths.
+  const debugTrackId = ++debugTrackCount;
+  const sqlTableName = `__debug_counter_${debugTrackId}`;
+
+  // TODO(altimin): Support removing this table when the track is closed.
+  await engine.query(`
+      create table ${sqlTableName} as
+      with data as (
+        ${data.sqlSource}
+      )
+      select
+        ${columns.ts} as ts,
+        ${columns.value} as value
+      from data
+      order by ts;`);
+
+  globals.dispatch(Actions.addTrack({
+    uri: DEBUG_COUNTER_TRACK_URI,
+    name: trackName.trim() || `Debug Track ${debugTrackId}`,
+    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 78c12e7..1dcf0b1 100644
--- a/ui/src/tracks/debug/index.ts
+++ b/ui/src/tracks/debug/index.ts
@@ -12,13 +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 {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);
+  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 739980a..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,
-    })];
+    });
   }
 }
 
@@ -100,7 +104,7 @@
   columns: string[];
 }
 
-export async function addDebugTrack(
+export async function addDebugSliceTrack(
     engine: EngineProxy,
     data: SqlDataSource,
     trackName: string,
@@ -140,10 +144,12 @@
       from prepared_data
       order by ts;`);
 
-  globals.dispatch(Actions.addDebugTrack({
-    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,
+        );
+      },
+    });
   }
 }