Merge "Adjust permissions for perfetto profiling dir" into main
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..a35fb26
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+ui
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
index b638356..c46b914 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1318,8 +1318,8 @@
 }
 
 // GN: [//protos/perfetto/config:source_set]
-java_library {
-    name: "perfetto_config_java_protos",
+filegroup {
+    name: "perfetto_config_filegroup_proto",
     srcs: [
         "protos/perfetto/common/android_energy_consumer_descriptor.proto",
         "protos/perfetto/common/android_log_constants.proto",
@@ -1375,10 +1375,6 @@
         "protos/perfetto/config/trace_config.proto",
         "protos/perfetto/config/track_event/track_event_config.proto",
     ],
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
 }
 
 // GN: //test/cts:perfetto_cts_deps
@@ -2445,6 +2441,7 @@
         ":perfetto_src_trace_processor_util_build_id",
         ":perfetto_src_trace_processor_util_bump_allocator",
         ":perfetto_src_trace_processor_util_descriptors",
+        ":perfetto_src_trace_processor_util_file_buffer",
         ":perfetto_src_trace_processor_util_glob",
         ":perfetto_src_trace_processor_util_gzip",
         ":perfetto_src_trace_processor_util_interned_message_view",
@@ -2457,6 +2454,7 @@
         ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stdlib",
+        ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_traced_probes_android_game_intervention_list_android_game_intervention_list",
@@ -12030,6 +12028,7 @@
         "src/trace_processor/importers/common/machine_tracker.cc",
         "src/trace_processor/importers/common/mapping_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.cc",
+        "src/trace_processor/importers/common/process_track_translation_table.cc",
         "src/trace_processor/importers/common/process_tracker.cc",
         "src/trace_processor/importers/common/sched_event_tracker.cc",
         "src/trace_processor/importers/common/slice_tracker.cc",
@@ -12065,6 +12064,7 @@
         "src/trace_processor/importers/common/deobfuscation_mapping_table_unittest.cc",
         "src/trace_processor/importers/common/event_tracker_unittest.cc",
         "src/trace_processor/importers/common/flow_tracker_unittest.cc",
+        "src/trace_processor/importers/common/process_track_translation_table_unittest.cc",
         "src/trace_processor/importers/common/process_tracker_unittest.cc",
         "src/trace_processor/importers/common/slice_tracker_unittest.cc",
         "src/trace_processor/importers/common/slice_translation_table_unittest.cc",
@@ -12236,10 +12236,12 @@
 filegroup {
     name: "perfetto_src_trace_processor_importers_perf_perf",
     srcs: [
-        "src/trace_processor/importers/perf/perf_data_parser.cc",
-        "src/trace_processor/importers/perf/perf_data_reader.cc",
+        "src/trace_processor/importers/perf/attrs_section_reader.cc",
+        "src/trace_processor/importers/perf/features.cc",
+        "src/trace_processor/importers/perf/mmap_record.cc",
         "src/trace_processor/importers/perf/perf_data_tokenizer.cc",
-        "src/trace_processor/importers/perf/perf_data_tracker.cc",
+        "src/trace_processor/importers/perf/record_parser.cc",
+        "src/trace_processor/importers/perf/sample.cc",
     ],
 }
 
@@ -12247,6 +12249,7 @@
 filegroup {
     name: "perfetto_src_trace_processor_importers_perf_record",
     srcs: [
+        "src/trace_processor/importers/perf/perf_counter.cc",
         "src/trace_processor/importers/perf/perf_event_attr.cc",
         "src/trace_processor/importers/perf/perf_session.cc",
     ],
@@ -12256,8 +12259,6 @@
 filegroup {
     name: "perfetto_src_trace_processor_importers_perf_unittests",
     srcs: [
-        "src/trace_processor/importers/perf/perf_data_reader_unittest.cc",
-        "src/trace_processor/importers/perf/perf_data_tracker_unittest.cc",
         "src/trace_processor/importers/perf/perf_session_unittest.cc",
         "src/trace_processor/importers/perf/reader_unittest.cc",
     ],
@@ -13182,6 +13183,7 @@
         "src/trace_processor/trace_processor_context.cc",
         "src/trace_processor/trace_processor_storage.cc",
         "src/trace_processor/trace_processor_storage_impl.cc",
+        "src/trace_processor/trace_reader_registry.cc",
         "src/trace_processor/virtual_destructors.cc",
     ],
 }
@@ -13456,6 +13458,14 @@
     name: "perfetto_src_trace_processor_util_stdlib",
 }
 
+// GN: //src/trace_processor/util:trace_type
+filegroup {
+    name: "perfetto_src_trace_processor_util_trace_type",
+    srcs: [
+        "src/trace_processor/util/trace_type.cc",
+    ],
+}
+
 // GN: //src/trace_processor/util:unittests
 filegroup {
     name: "perfetto_src_trace_processor_util_unittests",
@@ -15061,6 +15071,7 @@
         ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stdlib",
+        ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_unittests",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
@@ -16030,6 +16041,7 @@
         ":perfetto_src_trace_processor_util_build_id",
         ":perfetto_src_trace_processor_util_bump_allocator",
         ":perfetto_src_trace_processor_util_descriptors",
+        ":perfetto_src_trace_processor_util_file_buffer",
         ":perfetto_src_trace_processor_util_glob",
         ":perfetto_src_trace_processor_util_gzip",
         ":perfetto_src_trace_processor_util_interned_message_view",
@@ -16042,6 +16054,7 @@
         ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stdlib",
+        ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
         "src/trace_processor/trace_processor_shell.cc",
@@ -16216,6 +16229,7 @@
         ":perfetto_src_trace_processor_importers_fuchsia_fuchsia_record",
         ":perfetto_src_trace_processor_importers_json_minimal",
         ":perfetto_src_trace_processor_importers_memory_tracker_graph_processor",
+        ":perfetto_src_trace_processor_importers_perf_record",
         ":perfetto_src_trace_processor_importers_proto_minimal",
         ":perfetto_src_trace_processor_importers_proto_packet_sequence_state_generation_hdr",
         ":perfetto_src_trace_processor_importers_proto_proto_importer_module",
@@ -16236,6 +16250,7 @@
         ":perfetto_src_trace_processor_util_proto_to_args_parser",
         ":perfetto_src_trace_processor_util_protozero_to_text",
         ":perfetto_src_trace_processor_util_regex",
+        ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_redaction_trace_redaction",
         "src/trace_redaction/main.cc",
@@ -16412,6 +16427,7 @@
         ":perfetto_src_trace_processor_util_build_id",
         ":perfetto_src_trace_processor_util_bump_allocator",
         ":perfetto_src_trace_processor_util_descriptors",
+        ":perfetto_src_trace_processor_util_file_buffer",
         ":perfetto_src_trace_processor_util_glob",
         ":perfetto_src_trace_processor_util_gzip",
         ":perfetto_src_trace_processor_util_interned_message_view",
@@ -16424,6 +16440,7 @@
         ":perfetto_src_trace_processor_util_regex",
         ":perfetto_src_trace_processor_util_sql_argument",
         ":perfetto_src_trace_processor_util_stdlib",
+        ":perfetto_src_trace_processor_util_trace_type",
         ":perfetto_src_trace_processor_util_util",
         ":perfetto_src_trace_processor_util_zip_reader",
         ":perfetto_src_traceconv_lib",
@@ -17244,6 +17261,38 @@
     output_extension: "srcjar",
 }
 
+java_library {
+    name: "perfetto_config_java_protos",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+}
+
+java_library {
+    name: "perfetto_config_java_protos_system_server_current",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    sdk_version: "system_server_current",
+    apex_available: [
+        "com.android.profiling",
+    ],
+}
+
 prebuilt_etc {
     name: "perfetto_persistent_cfg.pbtxt",
     filename: "persistent_cfg.pbtxt",
diff --git a/Android.bp.extras b/Android.bp.extras
index 75d3a81..56cfcdd 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -198,6 +198,38 @@
     output_extension: "srcjar",
 }
 
+java_library {
+    name: "perfetto_config_java_protos",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+}
+
+java_library {
+    name: "perfetto_config_java_protos_system_server_current",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    sdk_version: "system_server_current",
+    apex_available: [
+        "com.android.profiling",
+    ],
+}
+
 prebuilt_etc {
     name: "perfetto_persistent_cfg.pbtxt",
     filename: "persistent_cfg.pbtxt",
diff --git a/BUILD b/BUILD
index f4167f8..aa5ed3c 100644
--- a/BUILD
+++ b/BUILD
@@ -272,6 +272,7 @@
         ":src_trace_processor_util_build_id",
         ":src_trace_processor_util_bump_allocator",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_file_buffer",
         ":src_trace_processor_util_glob",
         ":src_trace_processor_util_gzip",
         ":src_trace_processor_util_interned_message_view",
@@ -284,6 +285,7 @@
         ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stdlib",
+        ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
         ":src_trace_processor_util_zip_reader",
     ],
@@ -884,6 +886,7 @@
 perfetto_filegroup(
     name = "include_perfetto_trace_processor_storage",
     srcs = [
+        "include/perfetto/trace_processor/ref_counted.h",
         "include/perfetto/trace_processor/trace_blob.h",
         "include/perfetto/trace_processor/trace_blob_view.h",
         "include/perfetto/trace_processor/trace_processor_storage.h",
@@ -897,7 +900,6 @@
         "include/perfetto/trace_processor/iterator.h",
         "include/perfetto/trace_processor/metatrace_config.h",
         "include/perfetto/trace_processor/read_trace.h",
-        "include/perfetto/trace_processor/ref_counted.h",
         "include/perfetto/trace_processor/trace_processor.h",
     ],
 )
@@ -1494,6 +1496,8 @@
         "src/trace_processor/importers/common/mapping_tracker.h",
         "src/trace_processor/importers/common/metadata_tracker.cc",
         "src/trace_processor/importers/common/metadata_tracker.h",
+        "src/trace_processor/importers/common/process_track_translation_table.cc",
+        "src/trace_processor/importers/common/process_track_translation_table.h",
         "src/trace_processor/importers/common/process_tracker.cc",
         "src/trace_processor/importers/common/process_tracker.h",
         "src/trace_processor/importers/common/sched_event_state.h",
@@ -1702,16 +1706,19 @@
 perfetto_filegroup(
     name = "src_trace_processor_importers_perf_perf",
     srcs = [
-        "src/trace_processor/importers/perf/perf_data_parser.cc",
-        "src/trace_processor/importers/perf/perf_data_parser.h",
-        "src/trace_processor/importers/perf/perf_data_reader.cc",
-        "src/trace_processor/importers/perf/perf_data_reader.h",
+        "src/trace_processor/importers/perf/attrs_section_reader.cc",
+        "src/trace_processor/importers/perf/attrs_section_reader.h",
+        "src/trace_processor/importers/perf/features.cc",
+        "src/trace_processor/importers/perf/features.h",
+        "src/trace_processor/importers/perf/mmap_record.cc",
+        "src/trace_processor/importers/perf/mmap_record.h",
         "src/trace_processor/importers/perf/perf_data_tokenizer.cc",
         "src/trace_processor/importers/perf/perf_data_tokenizer.h",
-        "src/trace_processor/importers/perf/perf_data_tracker.cc",
-        "src/trace_processor/importers/perf/perf_data_tracker.h",
         "src/trace_processor/importers/perf/perf_file.h",
-        "src/trace_processor/importers/perf/reader.h",
+        "src/trace_processor/importers/perf/record_parser.cc",
+        "src/trace_processor/importers/perf/record_parser.h",
+        "src/trace_processor/importers/perf/sample.cc",
+        "src/trace_processor/importers/perf/sample.h",
     ],
 )
 
@@ -1719,6 +1726,8 @@
 perfetto_filegroup(
     name = "src_trace_processor_importers_perf_record",
     srcs = [
+        "src/trace_processor/importers/perf/perf_counter.cc",
+        "src/trace_processor/importers/perf/perf_counter.h",
         "src/trace_processor/importers/perf/perf_event.h",
         "src/trace_processor/importers/perf/perf_event_attr.cc",
         "src/trace_processor/importers/perf/perf_event_attr.h",
@@ -2902,6 +2911,15 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:file_buffer
+perfetto_filegroup(
+    name = "src_trace_processor_util_file_buffer",
+    srcs = [
+        "src/trace_processor/util/file_buffer.cc",
+        "src/trace_processor/util/file_buffer.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:glob
 perfetto_filegroup(
     name = "src_trace_processor_util_glob",
@@ -3011,6 +3029,15 @@
     ],
 )
 
+# GN target: //src/trace_processor/util:trace_type
+perfetto_filegroup(
+    name = "src_trace_processor_util_trace_type",
+    srcs = [
+        "src/trace_processor/util/trace_type.cc",
+        "src/trace_processor/util/trace_type.h",
+    ],
+)
+
 # GN target: //src/trace_processor/util:util
 perfetto_filegroup(
     name = "src_trace_processor_util_util",
@@ -3092,6 +3119,8 @@
         "src/trace_processor/trace_processor_storage.cc",
         "src/trace_processor/trace_processor_storage_impl.cc",
         "src/trace_processor/trace_processor_storage_impl.h",
+        "src/trace_processor/trace_reader_registry.cc",
+        "src/trace_processor/trace_reader_registry.h",
         "src/trace_processor/virtual_destructors.cc",
     ],
 )
@@ -5935,6 +5964,7 @@
         ":src_trace_processor_util_build_id",
         ":src_trace_processor_util_bump_allocator",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_file_buffer",
         ":src_trace_processor_util_glob",
         ":src_trace_processor_util_gzip",
         ":src_trace_processor_util_interned_message_view",
@@ -5947,6 +5977,7 @@
         ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stdlib",
+        ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
         ":src_trace_processor_util_zip_reader",
     ],
@@ -6112,6 +6143,7 @@
         ":src_trace_processor_util_build_id",
         ":src_trace_processor_util_bump_allocator",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_file_buffer",
         ":src_trace_processor_util_glob",
         ":src_trace_processor_util_gzip",
         ":src_trace_processor_util_interned_message_view",
@@ -6124,6 +6156,7 @@
         ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stdlib",
+        ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
         ":src_trace_processor_util_zip_reader",
         "src/trace_processor/trace_processor_shell.cc",
@@ -6343,6 +6376,7 @@
         ":src_trace_processor_util_build_id",
         ":src_trace_processor_util_bump_allocator",
         ":src_trace_processor_util_descriptors",
+        ":src_trace_processor_util_file_buffer",
         ":src_trace_processor_util_glob",
         ":src_trace_processor_util_gzip",
         ":src_trace_processor_util_interned_message_view",
@@ -6355,6 +6389,7 @@
         ":src_trace_processor_util_regex",
         ":src_trace_processor_util_sql_argument",
         ":src_trace_processor_util_stdlib",
+        ":src_trace_processor_util_trace_type",
         ":src_trace_processor_util_util",
         ":src_trace_processor_util_zip_reader",
         ":src_traceconv_lib",
diff --git a/CHANGELOG b/CHANGELOG
index d117e7d..70cb79c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,13 +1,21 @@
 Unreleased:
   Tracing service and probes:
-    *
+    * 
+  SQL Standard library:
+    * Added megacycles support to CPU package. Added tables:
+      `cpu_cycles_per_process`, `cpu_cycles_per_thread` and
+      `cpu_cycles_per_cpu`.
+    * Migrated `sched.utilization` package to `cpu.utilization`.
   Trace Processor:
     * Added "time to initial display" and "time to full display" metrics to
       the Android startup metric.
   UI:
     *
   SDK:
-    *
+    * The TRACE_COUNTER macro and CounterTrack constructor no longer accept
+      `const char *` track names. In case your code fails to compile,
+      https://perfetto.dev/docs/instrumentation/track-events#dynamic-event-names
+      explains how to fix the problem.
 
 
 v45.0 - 2024-05-09:
diff --git a/TEST_MAPPING b/TEST_MAPPING
index 9d84571..0e6a83d 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,4 +1,14 @@
 {
+  "art-mainline-presubmit": [
+    {
+      "name": "CtsPerfettoTestCases",
+      "options": [
+        {
+          "include-filter": "HeapprofdJavaCtsTest*"
+        }
+      ]
+    }
+  ],
   "presubmit": [
     {
       "name": "CtsPerfettoTestCases"
diff --git a/buildtools/BUILD.gn b/buildtools/BUILD.gn
index ae25789..0518359 100644
--- a/buildtools/BUILD.gn
+++ b/buildtools/BUILD.gn
@@ -164,141 +164,147 @@
   }
 }
 
+_protobuf_headers = [
+  "protobuf/src/google/protobuf/any.h",
+  "protobuf/src/google/protobuf/any.pb.h",
+  "protobuf/src/google/protobuf/api.pb.h",
+  "protobuf/src/google/protobuf/arena_impl.h",
+  "protobuf/src/google/protobuf/arena.h",
+  "protobuf/src/google/protobuf/arenastring.h",
+  "protobuf/src/google/protobuf/arenaz_sampler.h",
+  "protobuf/src/google/protobuf/compiler/importer.h",
+  "protobuf/src/google/protobuf/compiler/parser.h",
+  "protobuf/src/google/protobuf/descriptor_database.h",
+  "protobuf/src/google/protobuf/descriptor.h",
+  "protobuf/src/google/protobuf/descriptor.pb.h",
+  "protobuf/src/google/protobuf/duration.pb.h",
+  "protobuf/src/google/protobuf/dynamic_message.h",
+  "protobuf/src/google/protobuf/empty.pb.h",
+  "protobuf/src/google/protobuf/endian.h",
+  "protobuf/src/google/protobuf/explicitly_constructed.h",
+  "protobuf/src/google/protobuf/extension_set_inl.h",
+  "protobuf/src/google/protobuf/extension_set.h",
+  "protobuf/src/google/protobuf/field_access_listener.h",
+  "protobuf/src/google/protobuf/field_mask.pb.h",
+  "protobuf/src/google/protobuf/generated_enum_reflection.h",
+  "protobuf/src/google/protobuf/generated_enum_util.h",
+  "protobuf/src/google/protobuf/generated_message_bases.h",
+  "protobuf/src/google/protobuf/generated_message_reflection.h",
+  "protobuf/src/google/protobuf/generated_message_tctable_decl.h",
+  "protobuf/src/google/protobuf/generated_message_tctable_impl.h",
+  "protobuf/src/google/protobuf/generated_message_util.h",
+  "protobuf/src/google/protobuf/has_bits.h",
+  "protobuf/src/google/protobuf/implicit_weak_message.h",
+  "protobuf/src/google/protobuf/inlined_string_field.h",
+  "protobuf/src/google/protobuf/io/coded_stream.h",
+  "protobuf/src/google/protobuf/io/io_win32.h",
+  "protobuf/src/google/protobuf/io/printer.h",
+  "protobuf/src/google/protobuf/io/strtod.h",
+  "protobuf/src/google/protobuf/io/tokenizer.h",
+  "protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h",
+  "protobuf/src/google/protobuf/io/zero_copy_stream_impl.h",
+  "protobuf/src/google/protobuf/io/zero_copy_stream.h",
+  "protobuf/src/google/protobuf/map_entry_lite.h",
+  "protobuf/src/google/protobuf/map_entry.h",
+  "protobuf/src/google/protobuf/map_field_inl.h",
+  "protobuf/src/google/protobuf/map_field_lite.h",
+  "protobuf/src/google/protobuf/map_field.h",
+  "protobuf/src/google/protobuf/map_type_handler.h",
+  "protobuf/src/google/protobuf/map.h",
+  "protobuf/src/google/protobuf/message_lite.h",
+  "protobuf/src/google/protobuf/message.h",
+  "protobuf/src/google/protobuf/metadata_lite.h",
+  "protobuf/src/google/protobuf/metadata.h",
+  "protobuf/src/google/protobuf/parse_context.h",
+  "protobuf/src/google/protobuf/port_def.inc",
+  "protobuf/src/google/protobuf/port_undef.inc",
+  "protobuf/src/google/protobuf/port.h",
+  "protobuf/src/google/protobuf/reflection_internal.h",
+  "protobuf/src/google/protobuf/reflection_ops.h",
+  "protobuf/src/google/protobuf/reflection.h",
+  "protobuf/src/google/protobuf/repeated_field.h",
+  "protobuf/src/google/protobuf/repeated_ptr_field.h",
+  "protobuf/src/google/protobuf/service.h",
+  "protobuf/src/google/protobuf/source_context.pb.h",
+  "protobuf/src/google/protobuf/struct.pb.h",
+  "protobuf/src/google/protobuf/stubs/bytestream.h",
+  "protobuf/src/google/protobuf/stubs/callback.h",
+  "protobuf/src/google/protobuf/stubs/casts.h",
+  "protobuf/src/google/protobuf/stubs/common.h",
+  "protobuf/src/google/protobuf/stubs/hash.h",
+  "protobuf/src/google/protobuf/stubs/logging.h",
+  "protobuf/src/google/protobuf/stubs/macros.h",
+  "protobuf/src/google/protobuf/stubs/map_util.h",
+  "protobuf/src/google/protobuf/stubs/mutex.h",
+  "protobuf/src/google/protobuf/stubs/once.h",
+  "protobuf/src/google/protobuf/stubs/platform_macros.h",
+  "protobuf/src/google/protobuf/stubs/port.h",
+  "protobuf/src/google/protobuf/stubs/status.h",
+  "protobuf/src/google/protobuf/stubs/stl_util.h",
+  "protobuf/src/google/protobuf/stubs/stringpiece.h",
+  "protobuf/src/google/protobuf/stubs/strutil.h",
+  "protobuf/src/google/protobuf/stubs/template_util.h",
+  "protobuf/src/google/protobuf/text_format.h",
+  "protobuf/src/google/protobuf/timestamp.pb.h",
+  "protobuf/src/google/protobuf/type.pb.h",
+  "protobuf/src/google/protobuf/unknown_field_set.h",
+  "protobuf/src/google/protobuf/util/delimited_message_util.h",
+  "protobuf/src/google/protobuf/util/field_comparator.h",
+  "protobuf/src/google/protobuf/util/field_mask_util.h",
+  "protobuf/src/google/protobuf/util/json_util.h",
+  "protobuf/src/google/protobuf/util/message_differencer.h",
+  "protobuf/src/google/protobuf/util/time_util.h",
+  "protobuf/src/google/protobuf/util/type_resolver_util.h",
+  "protobuf/src/google/protobuf/util/type_resolver.h",
+  "protobuf/src/google/protobuf/wire_format_lite.h",
+  "protobuf/src/google/protobuf/wire_format.h",
+  "protobuf/src/google/protobuf/wrappers.pb.h",
+]
+
 source_set("protobuf_lite") {
   visibility = _buildtools_visibility
   sources = [
-    "protobuf/src/google/protobuf/any.h",
-    "protobuf/src/google/protobuf/any.pb.h",
     "protobuf/src/google/protobuf/any_lite.cc",
-    "protobuf/src/google/protobuf/api.pb.h",
     "protobuf/src/google/protobuf/arena.cc",
-    "protobuf/src/google/protobuf/arena.h",
-    "protobuf/src/google/protobuf/arena_impl.h",
     "protobuf/src/google/protobuf/arenastring.cc",
-    "protobuf/src/google/protobuf/arenastring.h",
     "protobuf/src/google/protobuf/arenaz_sampler.cc",
-    "protobuf/src/google/protobuf/arenaz_sampler.h",
-    "protobuf/src/google/protobuf/compiler/importer.h",
-    "protobuf/src/google/protobuf/compiler/parser.h",
-    "protobuf/src/google/protobuf/descriptor.h",
-    "protobuf/src/google/protobuf/descriptor.pb.h",
-    "protobuf/src/google/protobuf/descriptor_database.h",
-    "protobuf/src/google/protobuf/duration.pb.h",
-    "protobuf/src/google/protobuf/dynamic_message.h",
-    "protobuf/src/google/protobuf/empty.pb.h",
-    "protobuf/src/google/protobuf/explicitly_constructed.h",
     "protobuf/src/google/protobuf/extension_set.cc",
-    "protobuf/src/google/protobuf/extension_set.h",
-    "protobuf/src/google/protobuf/extension_set_inl.h",
-    "protobuf/src/google/protobuf/field_access_listener.h",
-    "protobuf/src/google/protobuf/field_mask.pb.h",
-    "protobuf/src/google/protobuf/generated_enum_reflection.h",
     "protobuf/src/google/protobuf/generated_enum_util.cc",
-    "protobuf/src/google/protobuf/generated_enum_util.h",
-    "protobuf/src/google/protobuf/generated_message_bases.h",
-    "protobuf/src/google/protobuf/generated_message_reflection.h",
-    "protobuf/src/google/protobuf/generated_message_tctable_decl.h",
-    "protobuf/src/google/protobuf/generated_message_tctable_impl.h",
     "protobuf/src/google/protobuf/generated_message_tctable_lite.cc",
     "protobuf/src/google/protobuf/generated_message_util.cc",
-    "protobuf/src/google/protobuf/generated_message_util.h",
-    "protobuf/src/google/protobuf/has_bits.h",
     "protobuf/src/google/protobuf/implicit_weak_message.cc",
-    "protobuf/src/google/protobuf/implicit_weak_message.h",
     "protobuf/src/google/protobuf/inlined_string_field.cc",
-    "protobuf/src/google/protobuf/inlined_string_field.h",
     "protobuf/src/google/protobuf/io/coded_stream.cc",
-    "protobuf/src/google/protobuf/io/coded_stream.h",
     "protobuf/src/google/protobuf/io/io_win32.cc",
-    "protobuf/src/google/protobuf/io/io_win32.h",
-    "protobuf/src/google/protobuf/io/printer.h",
     "protobuf/src/google/protobuf/io/strtod.cc",
-    "protobuf/src/google/protobuf/io/strtod.h",
-    "protobuf/src/google/protobuf/io/tokenizer.h",
     "protobuf/src/google/protobuf/io/zero_copy_stream.cc",
-    "protobuf/src/google/protobuf/io/zero_copy_stream.h",
     "protobuf/src/google/protobuf/io/zero_copy_stream_impl.cc",
-    "protobuf/src/google/protobuf/io/zero_copy_stream_impl.h",
     "protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.cc",
-    "protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h",
     "protobuf/src/google/protobuf/map.cc",
-    "protobuf/src/google/protobuf/map.h",
-    "protobuf/src/google/protobuf/map_entry.h",
-    "protobuf/src/google/protobuf/map_entry_lite.h",
-    "protobuf/src/google/protobuf/map_field.h",
-    "protobuf/src/google/protobuf/map_field_inl.h",
-    "protobuf/src/google/protobuf/map_field_lite.h",
-    "protobuf/src/google/protobuf/map_type_handler.h",
-    "protobuf/src/google/protobuf/message.h",
     "protobuf/src/google/protobuf/message_lite.cc",
-    "protobuf/src/google/protobuf/message_lite.h",
-    "protobuf/src/google/protobuf/metadata.h",
-    "protobuf/src/google/protobuf/metadata_lite.h",
     "protobuf/src/google/protobuf/parse_context.cc",
-    "protobuf/src/google/protobuf/parse_context.h",
-    "protobuf/src/google/protobuf/port.h",
-    "protobuf/src/google/protobuf/port_def.inc",
-    "protobuf/src/google/protobuf/port_undef.inc",
-    "protobuf/src/google/protobuf/reflection.h",
-    "protobuf/src/google/protobuf/reflection_ops.h",
     "protobuf/src/google/protobuf/repeated_field.cc",
-    "protobuf/src/google/protobuf/repeated_field.h",
     "protobuf/src/google/protobuf/repeated_ptr_field.cc",
-    "protobuf/src/google/protobuf/repeated_ptr_field.h",
-    "protobuf/src/google/protobuf/service.h",
-    "protobuf/src/google/protobuf/source_context.pb.h",
     "protobuf/src/google/protobuf/string_member_robber.h",
-    "protobuf/src/google/protobuf/struct.pb.h",
     "protobuf/src/google/protobuf/stubs/bytestream.cc",
-    "protobuf/src/google/protobuf/stubs/bytestream.h",
-    "protobuf/src/google/protobuf/stubs/callback.h",
-    "protobuf/src/google/protobuf/stubs/casts.h",
     "protobuf/src/google/protobuf/stubs/common.cc",
-    "protobuf/src/google/protobuf/stubs/common.h",
-    "protobuf/src/google/protobuf/stubs/hash.h",
     "protobuf/src/google/protobuf/stubs/int128.cc",
     "protobuf/src/google/protobuf/stubs/int128.h",
-    "protobuf/src/google/protobuf/stubs/logging.h",
-    "protobuf/src/google/protobuf/stubs/macros.h",
-    "protobuf/src/google/protobuf/stubs/map_util.h",
     "protobuf/src/google/protobuf/stubs/mathutil.h",
-    "protobuf/src/google/protobuf/stubs/mutex.h",
-    "protobuf/src/google/protobuf/stubs/once.h",
-    "protobuf/src/google/protobuf/stubs/platform_macros.h",
-    "protobuf/src/google/protobuf/stubs/port.h",
     "protobuf/src/google/protobuf/stubs/status.cc",
-    "protobuf/src/google/protobuf/stubs/status.h",
     "protobuf/src/google/protobuf/stubs/status_macros.h",
     "protobuf/src/google/protobuf/stubs/statusor.cc",
     "protobuf/src/google/protobuf/stubs/statusor.h",
-    "protobuf/src/google/protobuf/stubs/stl_util.h",
     "protobuf/src/google/protobuf/stubs/stringpiece.cc",
-    "protobuf/src/google/protobuf/stubs/stringpiece.h",
     "protobuf/src/google/protobuf/stubs/stringprintf.cc",
     "protobuf/src/google/protobuf/stubs/stringprintf.h",
     "protobuf/src/google/protobuf/stubs/structurally_valid.cc",
     "protobuf/src/google/protobuf/stubs/strutil.cc",
-    "protobuf/src/google/protobuf/stubs/strutil.h",
-    "protobuf/src/google/protobuf/stubs/template_util.h",
     "protobuf/src/google/protobuf/stubs/time.cc",
     "protobuf/src/google/protobuf/stubs/time.h",
-    "protobuf/src/google/protobuf/text_format.h",
-    "protobuf/src/google/protobuf/timestamp.pb.h",
-    "protobuf/src/google/protobuf/type.pb.h",
-    "protobuf/src/google/protobuf/unknown_field_set.h",
-    "protobuf/src/google/protobuf/util/delimited_message_util.h",
-    "protobuf/src/google/protobuf/util/field_comparator.h",
-    "protobuf/src/google/protobuf/util/field_mask_util.h",
-    "protobuf/src/google/protobuf/util/json_util.h",
-    "protobuf/src/google/protobuf/util/message_differencer.h",
-    "protobuf/src/google/protobuf/util/time_util.h",
-    "protobuf/src/google/protobuf/util/type_resolver.h",
-    "protobuf/src/google/protobuf/util/type_resolver_util.h",
-    "protobuf/src/google/protobuf/wire_format.h",
     "protobuf/src/google/protobuf/wire_format_lite.cc",
-    "protobuf/src/google/protobuf/wire_format_lite.h",
-    "protobuf/src/google/protobuf/wrappers.pb.h",
   ]
+  sources += _protobuf_headers
   configs -= [ "//gn/standalone:extra_warnings" ]
   if (is_win) {
     # Protobuf has its own #define WIN32_LEAN_AND_MEAN.
@@ -317,124 +323,39 @@
   ]
   sources = [
     "protobuf/src/google/protobuf/any.cc",
-    "protobuf/src/google/protobuf/any.h",
     "protobuf/src/google/protobuf/any.pb.cc",
-    "protobuf/src/google/protobuf/any.pb.h",
     "protobuf/src/google/protobuf/api.pb.cc",
-    "protobuf/src/google/protobuf/api.pb.h",
-    "protobuf/src/google/protobuf/arena.h",
-    "protobuf/src/google/protobuf/arena_impl.h",
-    "protobuf/src/google/protobuf/arenastring.h",
-    "protobuf/src/google/protobuf/arenaz_sampler.h",
     "protobuf/src/google/protobuf/compiler/importer.cc",
-    "protobuf/src/google/protobuf/compiler/importer.h",
     "protobuf/src/google/protobuf/compiler/parser.cc",
-    "protobuf/src/google/protobuf/compiler/parser.h",
     "protobuf/src/google/protobuf/descriptor.cc",
-    "protobuf/src/google/protobuf/descriptor.h",
     "protobuf/src/google/protobuf/descriptor.pb.cc",
-    "protobuf/src/google/protobuf/descriptor.pb.h",
     "protobuf/src/google/protobuf/descriptor_database.cc",
-    "protobuf/src/google/protobuf/descriptor_database.h",
     "protobuf/src/google/protobuf/duration.pb.cc",
-    "protobuf/src/google/protobuf/duration.pb.h",
     "protobuf/src/google/protobuf/dynamic_message.cc",
-    "protobuf/src/google/protobuf/dynamic_message.h",
     "protobuf/src/google/protobuf/empty.pb.cc",
-    "protobuf/src/google/protobuf/empty.pb.h",
-    "protobuf/src/google/protobuf/explicitly_constructed.h",
-    "protobuf/src/google/protobuf/extension_set.h",
     "protobuf/src/google/protobuf/extension_set_heavy.cc",
-    "protobuf/src/google/protobuf/extension_set_inl.h",
-    "protobuf/src/google/protobuf/field_access_listener.h",
     "protobuf/src/google/protobuf/field_mask.pb.cc",
-    "protobuf/src/google/protobuf/field_mask.pb.h",
-    "protobuf/src/google/protobuf/generated_enum_reflection.h",
-    "protobuf/src/google/protobuf/generated_enum_util.h",
     "protobuf/src/google/protobuf/generated_message_bases.cc",
-    "protobuf/src/google/protobuf/generated_message_bases.h",
     "protobuf/src/google/protobuf/generated_message_reflection.cc",
-    "protobuf/src/google/protobuf/generated_message_reflection.h",
-    "protobuf/src/google/protobuf/generated_message_tctable_decl.h",
     "protobuf/src/google/protobuf/generated_message_tctable_full.cc",
-    "protobuf/src/google/protobuf/generated_message_tctable_impl.h",
-    "protobuf/src/google/protobuf/generated_message_util.h",
-    "protobuf/src/google/protobuf/has_bits.h",
-    "protobuf/src/google/protobuf/implicit_weak_message.h",
-    "protobuf/src/google/protobuf/inlined_string_field.h",
-    "protobuf/src/google/protobuf/io/coded_stream.h",
     "protobuf/src/google/protobuf/io/gzip_stream.cc",
-    "protobuf/src/google/protobuf/io/io_win32.h",
     "protobuf/src/google/protobuf/io/printer.cc",
-    "protobuf/src/google/protobuf/io/printer.h",
-    "protobuf/src/google/protobuf/io/strtod.h",
     "protobuf/src/google/protobuf/io/tokenizer.cc",
-    "protobuf/src/google/protobuf/io/tokenizer.h",
-    "protobuf/src/google/protobuf/io/zero_copy_stream.h",
-    "protobuf/src/google/protobuf/io/zero_copy_stream_impl.h",
-    "protobuf/src/google/protobuf/io/zero_copy_stream_impl_lite.h",
-    "protobuf/src/google/protobuf/map.h",
-    "protobuf/src/google/protobuf/map_entry.h",
-    "protobuf/src/google/protobuf/map_entry_lite.h",
     "protobuf/src/google/protobuf/map_field.cc",
-    "protobuf/src/google/protobuf/map_field.h",
-    "protobuf/src/google/protobuf/map_field_inl.h",
-    "protobuf/src/google/protobuf/map_field_lite.h",
-    "protobuf/src/google/protobuf/map_type_handler.h",
     "protobuf/src/google/protobuf/message.cc",
-    "protobuf/src/google/protobuf/message.h",
-    "protobuf/src/google/protobuf/message_lite.h",
-    "protobuf/src/google/protobuf/metadata.h",
-    "protobuf/src/google/protobuf/metadata_lite.h",
-    "protobuf/src/google/protobuf/parse_context.h",
-    "protobuf/src/google/protobuf/port.h",
-    "protobuf/src/google/protobuf/port_def.inc",
-    "protobuf/src/google/protobuf/port_undef.inc",
-    "protobuf/src/google/protobuf/reflection.h",
-    "protobuf/src/google/protobuf/reflection_internal.h",
     "protobuf/src/google/protobuf/reflection_ops.cc",
-    "protobuf/src/google/protobuf/reflection_ops.h",
-    "protobuf/src/google/protobuf/repeated_field.h",
-    "protobuf/src/google/protobuf/repeated_ptr_field.h",
     "protobuf/src/google/protobuf/service.cc",
-    "protobuf/src/google/protobuf/service.h",
     "protobuf/src/google/protobuf/source_context.pb.cc",
-    "protobuf/src/google/protobuf/source_context.pb.h",
     "protobuf/src/google/protobuf/struct.pb.cc",
-    "protobuf/src/google/protobuf/struct.pb.h",
-    "protobuf/src/google/protobuf/stubs/bytestream.h",
-    "protobuf/src/google/protobuf/stubs/callback.h",
-    "protobuf/src/google/protobuf/stubs/casts.h",
-    "protobuf/src/google/protobuf/stubs/common.h",
-    "protobuf/src/google/protobuf/stubs/hash.h",
-    "protobuf/src/google/protobuf/stubs/logging.h",
-    "protobuf/src/google/protobuf/stubs/macros.h",
-    "protobuf/src/google/protobuf/stubs/map_util.h",
-    "protobuf/src/google/protobuf/stubs/mutex.h",
-    "protobuf/src/google/protobuf/stubs/once.h",
-    "protobuf/src/google/protobuf/stubs/platform_macros.h",
-    "protobuf/src/google/protobuf/stubs/port.h",
-    "protobuf/src/google/protobuf/stubs/status.h",
-    "protobuf/src/google/protobuf/stubs/stl_util.h",
-    "protobuf/src/google/protobuf/stubs/stringpiece.h",
-    "protobuf/src/google/protobuf/stubs/strutil.h",
     "protobuf/src/google/protobuf/stubs/substitute.cc",
     "protobuf/src/google/protobuf/stubs/substitute.h",
-    "protobuf/src/google/protobuf/stubs/template_util.h",
     "protobuf/src/google/protobuf/text_format.cc",
-    "protobuf/src/google/protobuf/text_format.h",
     "protobuf/src/google/protobuf/timestamp.pb.cc",
-    "protobuf/src/google/protobuf/timestamp.pb.h",
     "protobuf/src/google/protobuf/type.pb.cc",
-    "protobuf/src/google/protobuf/type.pb.h",
     "protobuf/src/google/protobuf/unknown_field_set.cc",
-    "protobuf/src/google/protobuf/unknown_field_set.h",
     "protobuf/src/google/protobuf/util/delimited_message_util.cc",
-    "protobuf/src/google/protobuf/util/delimited_message_util.h",
     "protobuf/src/google/protobuf/util/field_comparator.cc",
-    "protobuf/src/google/protobuf/util/field_comparator.h",
     "protobuf/src/google/protobuf/util/field_mask_util.cc",
-    "protobuf/src/google/protobuf/util/field_mask_util.h",
     "protobuf/src/google/protobuf/util/internal/constants.h",
     "protobuf/src/google/protobuf/util/internal/datapiece.cc",
     "protobuf/src/google/protobuf/util/internal/datapiece.h",
@@ -470,20 +391,13 @@
     "protobuf/src/google/protobuf/util/internal/utility.cc",
     "protobuf/src/google/protobuf/util/internal/utility.h",
     "protobuf/src/google/protobuf/util/json_util.cc",
-    "protobuf/src/google/protobuf/util/json_util.h",
     "protobuf/src/google/protobuf/util/message_differencer.cc",
-    "protobuf/src/google/protobuf/util/message_differencer.h",
     "protobuf/src/google/protobuf/util/time_util.cc",
-    "protobuf/src/google/protobuf/util/time_util.h",
-    "protobuf/src/google/protobuf/util/type_resolver.h",
     "protobuf/src/google/protobuf/util/type_resolver_util.cc",
-    "protobuf/src/google/protobuf/util/type_resolver_util.h",
     "protobuf/src/google/protobuf/wire_format.cc",
-    "protobuf/src/google/protobuf/wire_format.h",
-    "protobuf/src/google/protobuf/wire_format_lite.h",
     "protobuf/src/google/protobuf/wrappers.pb.cc",
-    "protobuf/src/google/protobuf/wrappers.pb.h",
   ]
+  sources += _protobuf_headers
   configs -= [ "//gn/standalone:extra_warnings" ]
   if (is_win) {
     # Protobuf has its own #define WIN32_LEAN_AND_MEAN.
@@ -507,39 +421,40 @@
     "protobuf/src/google/protobuf/compiler/code_generator.h",
     "protobuf/src/google/protobuf/compiler/command_line_interface.cc",
     "protobuf/src/google/protobuf/compiler/command_line_interface.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_enum.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_enum.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_enum_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_enum_field.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_extension.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_extension.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_field.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_file.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_file.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_generator.cc",
     "protobuf/src/google/protobuf/compiler/cpp/cpp_generator.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_helpers.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_helpers.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_map_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_map_field.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_message.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_message.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_message_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_message_field.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_message_layout_helper.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_names.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_options.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_padding_optimizer.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_padding_optimizer.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_parse_function_generator.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_parse_function_generator.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_primitive_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_primitive_field.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_service.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_service.h",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_string_field.cc",
-    "protobuf/src/google/protobuf/compiler/cpp/cpp_string_field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/enum.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/enum.h",
+    "protobuf/src/google/protobuf/compiler/cpp/enum_field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/enum_field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/extension.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/extension.h",
+    "protobuf/src/google/protobuf/compiler/cpp/field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/file.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/file.h",
+    "protobuf/src/google/protobuf/compiler/cpp/generator.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/generator.h",
+    "protobuf/src/google/protobuf/compiler/cpp/helpers.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/helpers.h",
+    "protobuf/src/google/protobuf/compiler/cpp/map_field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/map_field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/message.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/message.h",
+    "protobuf/src/google/protobuf/compiler/cpp/message_field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/message_field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/message_layout_helper.h",
+    "protobuf/src/google/protobuf/compiler/cpp/names.h",
+    "protobuf/src/google/protobuf/compiler/cpp/options.h",
+    "protobuf/src/google/protobuf/compiler/cpp/padding_optimizer.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/padding_optimizer.h",
+    "protobuf/src/google/protobuf/compiler/cpp/parse_function_generator.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/parse_function_generator.h",
+    "protobuf/src/google/protobuf/compiler/cpp/primitive_field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/primitive_field.h",
+    "protobuf/src/google/protobuf/compiler/cpp/service.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/service.h",
+    "protobuf/src/google/protobuf/compiler/cpp/string_field.cc",
+    "protobuf/src/google/protobuf/compiler/cpp/string_field.h",
     "protobuf/src/google/protobuf/compiler/csharp/csharp_doc_comment.cc",
     "protobuf/src/google/protobuf/compiler/csharp/csharp_doc_comment.h",
     "protobuf/src/google/protobuf/compiler/csharp/csharp_enum.cc",
@@ -574,70 +489,67 @@
     "protobuf/src/google/protobuf/compiler/csharp/csharp_source_generator_base.h",
     "protobuf/src/google/protobuf/compiler/csharp/csharp_wrapper_field.cc",
     "protobuf/src/google/protobuf/compiler/csharp/csharp_wrapper_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_context.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_context.h",
-    "protobuf/src/google/protobuf/compiler/java/java_doc_comment.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_doc_comment.h",
-    "protobuf/src/google/protobuf/compiler/java/java_enum.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_enum.h",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_field_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_field_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_enum_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_extension.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_extension.h",
-    "protobuf/src/google/protobuf/compiler/java/java_extension_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_extension_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_file.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_file.h",
-    "protobuf/src/google/protobuf/compiler/java/java_generator.cc",
+    "protobuf/src/google/protobuf/compiler/java/context.cc",
+    "protobuf/src/google/protobuf/compiler/java/context.h",
+    "protobuf/src/google/protobuf/compiler/java/doc_comment.cc",
+    "protobuf/src/google/protobuf/compiler/java/doc_comment.h",
+    "protobuf/src/google/protobuf/compiler/java/enum.cc",
+    "protobuf/src/google/protobuf/compiler/java/enum.h",
+    "protobuf/src/google/protobuf/compiler/java/enum_field.cc",
+    "protobuf/src/google/protobuf/compiler/java/enum_field.h",
+    "protobuf/src/google/protobuf/compiler/java/enum_field_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/enum_field_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/enum_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/enum_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/extension.cc",
+    "protobuf/src/google/protobuf/compiler/java/extension.h",
+    "protobuf/src/google/protobuf/compiler/java/extension_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/extension_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/field.cc",
+    "protobuf/src/google/protobuf/compiler/java/field.h",
+    "protobuf/src/google/protobuf/compiler/java/file.cc",
+    "protobuf/src/google/protobuf/compiler/java/file.h",
+    "protobuf/src/google/protobuf/compiler/java/generator.cc",
+    "protobuf/src/google/protobuf/compiler/java/generator.h",
+    "protobuf/src/google/protobuf/compiler/java/generator_factory.cc",
+    "protobuf/src/google/protobuf/compiler/java/generator_factory.h",
+    "protobuf/src/google/protobuf/compiler/java/helpers.cc",
+    "protobuf/src/google/protobuf/compiler/java/helpers.h",
     "protobuf/src/google/protobuf/compiler/java/java_generator.h",
-    "protobuf/src/google/protobuf/compiler/java/java_generator_factory.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_generator_factory.h",
-    "protobuf/src/google/protobuf/compiler/java/java_helpers.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_helpers.h",
-    "protobuf/src/google/protobuf/compiler/java/java_kotlin_generator.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_kotlin_generator.h",
-    "protobuf/src/google/protobuf/compiler/java/java_map_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_map_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_map_field_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_map_field_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message_builder.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message_builder.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message_builder_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message_builder_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message_field_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message_field_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_message_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_message_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_name_resolver.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_name_resolver.h",
-    "protobuf/src/google/protobuf/compiler/java/java_names.h",
-    "protobuf/src/google/protobuf/compiler/java/java_options.h",
-    "protobuf/src/google/protobuf/compiler/java/java_primitive_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_primitive_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_primitive_field_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_primitive_field_lite.h",
-    "protobuf/src/google/protobuf/compiler/java/java_service.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_service.h",
-    "protobuf/src/google/protobuf/compiler/java/java_shared_code_generator.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_shared_code_generator.h",
-    "protobuf/src/google/protobuf/compiler/java/java_string_field.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_string_field.h",
-    "protobuf/src/google/protobuf/compiler/java/java_string_field_lite.cc",
-    "protobuf/src/google/protobuf/compiler/java/java_string_field_lite.h",
-    "protobuf/src/google/protobuf/compiler/js/js_generator.cc",
-    "protobuf/src/google/protobuf/compiler/js/js_generator.h",
-    "protobuf/src/google/protobuf/compiler/js/well_known_types_embed.cc",
-    "protobuf/src/google/protobuf/compiler/js/well_known_types_embed.h",
+    "protobuf/src/google/protobuf/compiler/java/kotlin_generator.cc",
+    "protobuf/src/google/protobuf/compiler/java/kotlin_generator.h",
+    "protobuf/src/google/protobuf/compiler/java/map_field.cc",
+    "protobuf/src/google/protobuf/compiler/java/map_field.h",
+    "protobuf/src/google/protobuf/compiler/java/map_field_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/map_field_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/message.cc",
+    "protobuf/src/google/protobuf/compiler/java/message.h",
+    "protobuf/src/google/protobuf/compiler/java/message_builder.cc",
+    "protobuf/src/google/protobuf/compiler/java/message_builder.h",
+    "protobuf/src/google/protobuf/compiler/java/message_builder_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/message_builder_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/message_field.cc",
+    "protobuf/src/google/protobuf/compiler/java/message_field.h",
+    "protobuf/src/google/protobuf/compiler/java/message_field_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/message_field_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/message_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/message_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/name_resolver.cc",
+    "protobuf/src/google/protobuf/compiler/java/name_resolver.h",
+    "protobuf/src/google/protobuf/compiler/java/names.h",
+    "protobuf/src/google/protobuf/compiler/java/options.h",
+    "protobuf/src/google/protobuf/compiler/java/primitive_field.cc",
+    "protobuf/src/google/protobuf/compiler/java/primitive_field.h",
+    "protobuf/src/google/protobuf/compiler/java/primitive_field_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/primitive_field_lite.h",
+    "protobuf/src/google/protobuf/compiler/java/service.cc",
+    "protobuf/src/google/protobuf/compiler/java/service.h",
+    "protobuf/src/google/protobuf/compiler/java/shared_code_generator.cc",
+    "protobuf/src/google/protobuf/compiler/java/shared_code_generator.h",
+    "protobuf/src/google/protobuf/compiler/java/string_field.cc",
+    "protobuf/src/google/protobuf/compiler/java/string_field.h",
+    "protobuf/src/google/protobuf/compiler/java/string_field_lite.cc",
+    "protobuf/src/google/protobuf/compiler/java/string_field_lite.h",
     "protobuf/src/google/protobuf/compiler/objectivec/objectivec_enum.cc",
     "protobuf/src/google/protobuf/compiler/objectivec/objectivec_enum.h",
     "protobuf/src/google/protobuf/compiler/objectivec/objectivec_enum_field.cc",
@@ -669,12 +581,13 @@
     "protobuf/src/google/protobuf/compiler/plugin.h",
     "protobuf/src/google/protobuf/compiler/plugin.pb.cc",
     "protobuf/src/google/protobuf/compiler/plugin.pb.h",
-    "protobuf/src/google/protobuf/compiler/python/python_generator.cc",
+    "protobuf/src/google/protobuf/compiler/python/generator.cc",
+    "protobuf/src/google/protobuf/compiler/python/generator.h",
+    "protobuf/src/google/protobuf/compiler/python/helpers.cc",
+    "protobuf/src/google/protobuf/compiler/python/helpers.h",
+    "protobuf/src/google/protobuf/compiler/python/pyi_generator.cc",
+    "protobuf/src/google/protobuf/compiler/python/pyi_generator.h",
     "protobuf/src/google/protobuf/compiler/python/python_generator.h",
-    "protobuf/src/google/protobuf/compiler/python/python_helpers.cc",
-    "protobuf/src/google/protobuf/compiler/python/python_helpers.h",
-    "protobuf/src/google/protobuf/compiler/python/python_pyi_generator.cc",
-    "protobuf/src/google/protobuf/compiler/python/python_pyi_generator.h",
     "protobuf/src/google/protobuf/compiler/ruby/ruby_generator.cc",
     "protobuf/src/google/protobuf/compiler/ruby/ruby_generator.h",
     "protobuf/src/google/protobuf/compiler/scc.h",
diff --git a/docs/case-studies/memory.md b/docs/case-studies/memory.md
index 93f67a2..a9508be 100644
--- a/docs/case-studies/memory.md
+++ b/docs/case-studies/memory.md
@@ -407,15 +407,17 @@
 
 ![Profile Diamond](/docs/images/profile-diamond.png)
 
-This will present a flamegraph of the memory attributed to the shortest path
-to a garbage-collection root. In general an object is reachable by many paths,
-we only show the shortest as that reduces the complexity of the data displayed
-and is generally the highest-signal. The rightmost `[merged]` stacks is the
-sum of all objects that are too small to be displayed.
+This will present a set of flamegraph views as explained below.
 
-![Java Flamegraph](/docs/images/java-heap-graph.png)
+#### "Size" and "Objects" tabs
 
-The tabs that are available are
+![Java Flamegraph: Size](/docs/images/java-heap-graph.png)
+
+These views show the memory attributed to the shortest path to a
+garbage-collection root. In general an object is reachable by many paths, we
+only show the shortest as that reduces the complexity of the data displayed and
+is generally the highest-signal. The rightmost `[merged]` stacks is the sum of
+all objects that are too small to be displayed.
 
 * **Size**: how many bytes are retained via this path to the GC root.
 * **Objects**: how many objects are retained via this path to the GC root.
@@ -432,4 +434,26 @@
 
 We aggregate the paths per class name, so if there are multiple objects of the
 same type retained by a `java.lang.Object[]`, we will show one element as its
-child, as you can see in the leftmost stack above.
+child, as you can see in the leftmost stack above. This also applies to the
+dominator tree paths as described below.
+
+#### "Dominated Size" and "Dominated Objects" tabs
+
+![Java Flamegraph: Dominated Size](/docs/images/java-heap-graph-dominated-size.png)
+
+Another way to present the heap graph as a flamegraph (a tree) is to show its
+[dominator tree](/docs/analysis/stdlib-docs.autogen#memory-heap_graph_dominator_tree).
+In a heap graph, an object `a` dominates an object `b` if `b` is reachable from
+the root only via paths that go through `a`. The dominators of an object form a
+chain from the root and the object is exclusvely retained by all objects on this
+chain. For all reachable objects in the graph those chains form a tree, i.e. the
+dominator tree.
+
+We aggregate the tree paths per class name, and each element (tree node)
+represents a set of objects that have the same class name and position in the
+dominator tree.
+
+* **Dominated Size**: how many bytes are exclusively retained by the objects in
+a node.
+* **Dominated Objects**: how many objects are exclusively retained by the
+objects in a node.
diff --git a/docs/images/java-heap-graph-dominated-size.png b/docs/images/java-heap-graph-dominated-size.png
new file mode 100644
index 0000000..94b8186
--- /dev/null
+++ b/docs/images/java-heap-graph-dominated-size.png
Binary files differ
diff --git a/docs/images/java-heap-graph-focus.png b/docs/images/java-heap-graph-focus.png
index cd5d232..c619c41 100644
--- a/docs/images/java-heap-graph-focus.png
+++ b/docs/images/java-heap-graph-focus.png
Binary files differ
diff --git a/docs/images/java-heap-graph.png b/docs/images/java-heap-graph.png
index 829c7fe..730437f 100644
--- a/docs/images/java-heap-graph.png
+++ b/docs/images/java-heap-graph.png
Binary files differ
diff --git a/include/perfetto/trace_processor/BUILD.gn b/include/perfetto/trace_processor/BUILD.gn
index d0dae7f..8c26054 100644
--- a/include/perfetto/trace_processor/BUILD.gn
+++ b/include/perfetto/trace_processor/BUILD.gn
@@ -17,7 +17,6 @@
     "iterator.h",
     "metatrace_config.h",
     "read_trace.h",
-    "ref_counted.h",
     "trace_processor.h",
   ]
   public_deps = [
@@ -28,6 +27,7 @@
 
 source_set("storage") {
   sources = [
+    "ref_counted.h",
     "trace_blob.h",
     "trace_blob_view.h",
     "trace_processor_storage.h",
diff --git a/include/perfetto/tracing/string_helpers.h b/include/perfetto/tracing/string_helpers.h
index 0d2819a..03fa9e4 100644
--- a/include/perfetto/tracing/string_helpers.h
+++ b/include/perfetto/tracing/string_helpers.h
@@ -38,6 +38,8 @@
 
   constexpr explicit StaticString(const char* str) : value(str) {}
 
+  operator bool() const { return !!value; }
+
   const char* value;
 };
 
@@ -52,6 +54,9 @@
     length = strlen(str);
   }
   DynamicString(const char* str, size_t len) : value(str), length(len) {}
+  constexpr DynamicString() : value(nullptr), length(0) {}
+
+  operator bool() const { return !!value; }
 
   const char* value;
   size_t length;
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 37f02e5..831c290 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -25,6 +25,7 @@
 #include "perfetto/tracing/internal/fnv1a.h"
 #include "perfetto/tracing/internal/tracing_muxer.h"
 #include "perfetto/tracing/platform.h"
+#include "perfetto/tracing/string_helpers.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 #include "protos/perfetto/trace/track_event/counter_descriptor.gen.h"
 #include "protos/perfetto/trace/track_event/counter_descriptor.pbzero.h"
@@ -201,71 +202,92 @@
       perfetto::protos::gen::CounterDescriptor::BuiltinCounterType;
 
   // |name| must outlive this object.
-  constexpr explicit CounterTrack(const char* name,
+  constexpr explicit CounterTrack(StaticString name,
                                   Track parent = MakeProcessTrack())
-      : Track(internal::Fnv1a(name) ^ kCounterMagic, parent),
-        name_(name),
-        category_(nullptr) {}
+      : CounterTrack(
+            name,
+            perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED,
+            nullptr,
+            parent) {}
+
+  explicit CounterTrack(DynamicString name, Track parent = MakeProcessTrack())
+      : CounterTrack(
+            name,
+            perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED,
+            nullptr,
+            parent) {}
 
   // |unit_name| is a free-form description of the unit used by this counter. It
   // must outlive this object.
-  constexpr CounterTrack(const char* name,
+  template <class TrackEventName>
+  constexpr CounterTrack(TrackEventName&& name,
                          const char* unit_name,
                          Track parent = MakeProcessTrack())
-      : Track(internal::Fnv1a(name) ^ kCounterMagic, parent),
-        name_(name),
-        category_(nullptr),
-        unit_name_(unit_name) {}
+      : CounterTrack(
+            std::forward<TrackEventName>(name),
+            perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED,
+            unit_name,
+            parent) {}
 
-  constexpr CounterTrack(const char* name,
+  template <class TrackEventName>
+  constexpr CounterTrack(TrackEventName&& name,
                          Unit unit,
                          Track parent = MakeProcessTrack())
-      : Track(internal::Fnv1a(name) ^ kCounterMagic, parent),
-        name_(name),
-        category_(nullptr),
-        unit_(unit) {}
+      : CounterTrack(std::forward<TrackEventName>(name),
+                     unit,
+                     nullptr,
+                     parent) {}
 
-  static constexpr CounterTrack Global(const char* name,
+  template <class TrackEventName>
+  static constexpr CounterTrack Global(TrackEventName&& name,
                                        const char* unit_name) {
-    return CounterTrack(name, unit_name, Track());
+    return CounterTrack(std::forward<TrackEventName>(name), unit_name, Track());
   }
 
-  static constexpr CounterTrack Global(const char* name, Unit unit) {
-    return CounterTrack(name, unit, Track());
+  template <class TrackEventName>
+  static constexpr CounterTrack Global(TrackEventName&& name, Unit unit) {
+    return CounterTrack(std::forward<TrackEventName>(name), unit, Track());
   }
 
-  static constexpr CounterTrack Global(const char* name) {
-    return Global(name, nullptr);
+  template <class TrackEventName>
+  static constexpr CounterTrack Global(TrackEventName&& name) {
+    return Global(std::forward<TrackEventName>(name), nullptr);
   }
 
   constexpr CounterTrack set_unit(Unit unit) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit, unit_name_,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit, unit_name_, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_type(CounterType type) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier_, is_incremental_, type);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier_,
+                        is_incremental_, type);
   }
 
   constexpr CounterTrack set_unit_name(const char* unit_name) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_unit_multiplier(int64_t unit_multiplier) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_category(const char* category) const {
-    return CounterTrack(uuid, parent_uuid, name_, category, unit_, unit_name_,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category, unit_, unit_name_, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_is_incremental(bool is_incremental = true) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier_, is_incremental, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier_,
+                        is_incremental, type_);
   }
 
   constexpr bool is_incremental() const { return is_incremental_; }
@@ -274,9 +296,29 @@
   protos::gen::TrackDescriptor Serialize() const;
 
  private:
+  constexpr CounterTrack(StaticString name,
+                         Unit unit,
+                         const char* unit_name,
+                         Track parent)
+      : Track(internal::Fnv1a(name.value) ^ kCounterMagic, parent),
+        static_name_(name),
+        category_(nullptr),
+        unit_(unit),
+        unit_name_(unit_name) {}
+  CounterTrack(DynamicString name,
+               Unit unit,
+               const char* unit_name,
+               Track parent)
+      : Track(internal::Fnv1a(name.value, name.length) ^ kCounterMagic, parent),
+        static_name_(nullptr),
+        dynamic_name_(name),
+        category_(nullptr),
+        unit_(unit),
+        unit_name_(unit_name) {}
   constexpr CounterTrack(uint64_t uuid_,
                          uint64_t parent_uuid_,
-                         const char* name,
+                         StaticString static_name,
+                         DynamicString dynamic_name,
                          const char* category,
                          Unit unit,
                          const char* unit_name,
@@ -284,7 +326,8 @@
                          bool is_incremental,
                          CounterType type)
       : Track(uuid_, parent_uuid_),
-        name_(name),
+        static_name_(static_name),
+        dynamic_name_(dynamic_name),
         category_(category),
         unit_(unit),
         unit_name_(unit_name),
@@ -292,7 +335,8 @@
         is_incremental_(is_incremental),
         type_(type) {}
 
-  const char* const name_;
+  StaticString static_name_;
+  DynamicString dynamic_name_;
   const char* const category_;
   Unit unit_ = perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED;
   const char* const unit_name_ = nullptr;
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index 960f8c3..20612e4 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -13895,6 +13895,7 @@
     optional uint64 smr_pss_anon_kb = 18;
     optional uint64 smr_pss_file_kb = 19;
     optional uint64 smr_pss_shmem_kb = 20;
+    optional uint64 smr_swap_pss_kb = 23;
 
     // Time spent scheduled in user mode in nanoseconds. Parsed from utime in
     // /proc/pid/stat. Recorded if record_process_runtime config option is set.
@@ -14675,7 +14676,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 10.
+// Next id: 11.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -14695,7 +14696,12 @@
   // Name of the track. Optional - if unspecified, it may be derived from the
   // process/thread name (process/thread tracks), the first event's name (async
   // tracks), or counter name (counter tracks).
-  optional string name = 2;
+  oneof static_or_dynamic_name {
+    string name = 2;
+    // This field is only set by the SDK when perfetto::StaticString is
+    // provided.
+    string static_name = 10;
+  }
 
   // Associate the track with a process, making it the process-global track.
   // There should only be one such track per process (usually for instant
@@ -14743,6 +14749,7 @@
     ChromeUserEventTranslationTable chrome_user_event = 2;
     ChromePerformanceMarkTranslationTable chrome_performance_mark = 3;
     SliceNameTranslationTable slice_name = 4;
+    ProcessTrackNameTranslationTable process_track_name = 5;
   }
 }
 
@@ -14767,6 +14774,11 @@
   map<string, string> raw_to_deobfuscated_name = 1;
 };
 
+// Raw -> deobfuscated process track name translation rules.
+message ProcessTrackNameTranslationTable {
+  map<string, string> raw_to_deobfuscated_name = 1;
+};
+
 // End of protos/perfetto/trace/translation/translation_table.proto
 
 // Begin of protos/perfetto/trace/trigger.proto
diff --git a/protos/perfetto/trace/ps/process_stats.proto b/protos/perfetto/trace/ps/process_stats.proto
index 5175625..f60cb68 100644
--- a/protos/perfetto/trace/ps/process_stats.proto
+++ b/protos/perfetto/trace/ps/process_stats.proto
@@ -83,6 +83,7 @@
     optional uint64 smr_pss_anon_kb = 18;
     optional uint64 smr_pss_file_kb = 19;
     optional uint64 smr_pss_shmem_kb = 20;
+    optional uint64 smr_swap_pss_kb = 23;
 
     // Time spent scheduled in user mode in nanoseconds. Parsed from utime in
     // /proc/pid/stat. Recorded if record_process_runtime config option is set.
diff --git a/protos/perfetto/trace/track_event/track_descriptor.proto b/protos/perfetto/trace/track_event/track_descriptor.proto
index d6db233..76890f2 100644
--- a/protos/perfetto/trace/track_event/track_descriptor.proto
+++ b/protos/perfetto/trace/track_event/track_descriptor.proto
@@ -37,7 +37,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 10.
+// Next id: 11.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -57,7 +57,12 @@
   // Name of the track. Optional - if unspecified, it may be derived from the
   // process/thread name (process/thread tracks), the first event's name (async
   // tracks), or counter name (counter tracks).
-  optional string name = 2;
+  oneof static_or_dynamic_name {
+    string name = 2;
+    // This field is only set by the SDK when perfetto::StaticString is
+    // provided.
+    string static_name = 10;
+  }
 
   // Associate the track with a process, making it the process-global track.
   // There should only be one such track per process (usually for instant
diff --git a/protos/perfetto/trace/translation/translation_table.proto b/protos/perfetto/trace/translation/translation_table.proto
index 06cef31..b934cf7 100644
--- a/protos/perfetto/trace/translation/translation_table.proto
+++ b/protos/perfetto/trace/translation/translation_table.proto
@@ -26,6 +26,7 @@
     ChromeUserEventTranslationTable chrome_user_event = 2;
     ChromePerformanceMarkTranslationTable chrome_performance_mark = 3;
     SliceNameTranslationTable slice_name = 4;
+    ProcessTrackNameTranslationTable process_track_name = 5;
   }
 }
 
@@ -49,3 +50,8 @@
 message SliceNameTranslationTable {
   map<string, string> raw_to_deobfuscated_name = 1;
 };
+
+// Raw -> deobfuscated process track name translation rules.
+message ProcessTrackNameTranslationTable {
+  map<string, string> raw_to_deobfuscated_name = 1;
+};
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 84c09ce..b499f0b 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -109,6 +109,8 @@
     "trace_processor_storage.cc",
     "trace_processor_storage_impl.cc",
     "trace_processor_storage_impl.h",
+    "trace_reader_registry.cc",
+    "trace_reader_registry.h",
     "virtual_destructors.cc",
   ]
   deps = [
@@ -132,6 +134,7 @@
     "util:descriptors",
     "util:gzip",
     "util:proto_to_args_parser",
+    "util:trace_type",
   ]
   public_deps = [ "../../include/perfetto/trace_processor:storage" ]
 }
@@ -190,6 +193,7 @@
       "util:protozero_to_text",
       "util:regex",
       "util:stdlib",
+      "util:trace_type",
     ]
     public_deps = [
       "../../gn:sqlite",  # iterator_impl.h includes sqlite3.h.
@@ -247,6 +251,7 @@
     "../../gn:default_deps",
     "../../gn:gtest_and_gmock",
     "../../include/perfetto/trace_processor",
+    "util:trace_type",
   ]
 
   if (enable_perfetto_trace_processor_json && !is_win) {
diff --git a/src/trace_processor/export_json_unittest.cc b/src/trace_processor/export_json_unittest.cc
index a8f7564..e9e8f2a 100644
--- a/src/trace_processor/export_json_unittest.cc
+++ b/src/trace_processor/export_json_unittest.cc
@@ -29,6 +29,7 @@
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/proto/track_event_tracker.h"
@@ -79,6 +80,8 @@
     context_.metadata_tracker.reset(
         new MetadataTracker(context_.storage.get()));
     context_.process_tracker.reset(new ProcessTracker(&context_));
+    context_.process_track_translation_table.reset(
+        new ProcessTrackTranslationTable(context_.storage.get()));
   }
 
   std::string ToJson(ArgumentFilterPredicate argument_filter = nullptr,
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 466ee96..369917d 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -16,29 +16,24 @@
 
 #include "src/trace_processor/forwarding_trace_parser.h"
 
+#include <memory>
+#include <optional>
+
 #include "perfetto/base/logging.h"
-#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/trace_reader_registry.h"
 #include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
 
 namespace perfetto {
 namespace trace_processor {
 namespace {
 
-const char kNoZlibErr[] =
-    "Cannot open compressed trace. zlib not enabled in the build config";
-
-inline bool isspace(unsigned char c) {
-  return ::isspace(c);
-}
-
-std::string RemoveWhitespace(std::string str) {
-  str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end());
-  return str;
-}
-
 TraceSorter::SortingMode ConvertSortingMode(SortingMode sorting_mode) {
   switch (sorting_mode) {
     case SortingMode::kDefaultHeuristics:
@@ -50,10 +45,30 @@
   PERFETTO_FATAL("For GCC");
 }
 
-// Fuchsia traces have a magic number as documented here:
-// https://fuchsia.googlesource.com/fuchsia/+/HEAD/docs/development/tracing/trace-format/README.md#magic-number-record-trace-info-type-0
-constexpr uint64_t kFuchsiaMagicNumber = 0x0016547846040010;
-constexpr char kPerfMagic[] = "PERFILE2";
+std::optional<TraceSorter::SortingMode> GetMinimumSortingMode(
+    TraceType trace_type,
+    const TraceProcessorContext& context) {
+  switch (trace_type) {
+    case kNinjaLogTraceType:
+    case kSystraceTraceType:
+    case kGzipTraceType:
+    case kCtraceTraceType:
+    case kAndroidBugreportTraceType:
+      return std::nullopt;
+
+    case kPerfDataTraceType:
+      return TraceSorter::SortingMode::kDefault;
+
+    case kUnknownTraceType:
+    case kJsonTraceType:
+    case kFuchsiaTraceType:
+      return TraceSorter::SortingMode::kFullSort;
+
+    case kProtoTraceType:
+      return ConvertSortingMode(context.config.sorting_mode);
+  }
+  PERFETTO_FATAL("For GCC");
+}
 
 }  // namespace
 
@@ -62,102 +77,63 @@
 
 ForwardingTraceParser::~ForwardingTraceParser() {}
 
+base::Status ForwardingTraceParser::Init(const TraceBlobView& blob) {
+  PERFETTO_CHECK(!reader_);
+
+  TraceType trace_type;
+  {
+    auto scoped_trace = context_->storage->TraceExecutionTimeIntoStats(
+        stats::guess_trace_type_duration_ns);
+    trace_type = GuessTraceType(blob.data(), blob.size());
+    context_->trace_type = trace_type;
+  }
+
+  if (trace_type == kUnknownTraceType) {
+    // If renaming this error message don't remove the "(ERR:fmt)" part.
+    // The UI's error_dialog.ts uses it to make the dialog more graceful.
+    return base::ErrStatus("Unknown trace type provided (ERR:fmt)");
+  }
+
+  base::StatusOr<std::unique_ptr<ChunkedTraceReader>> reader_or =
+      context_->reader_registry->CreateTraceReader(trace_type);
+  if (!reader_or.ok()) {
+    return reader_or.status();
+  }
+  reader_ = std::move(*reader_or);
+
+  PERFETTO_DLOG("%s detected", ToString(trace_type));
+  std::optional<TraceSorter::SortingMode> minimum_sorting_mode =
+      GetMinimumSortingMode(trace_type, *context_);
+
+  if (minimum_sorting_mode.has_value()) {
+    if (!context_->sorter) {
+      context_->sorter.reset(new TraceSorter(context_, *minimum_sorting_mode));
+    }
+
+    switch (context_->sorter->sorting_mode()) {
+      case TraceSorter::SortingMode::kDefault:
+        PERFETTO_CHECK(minimum_sorting_mode ==
+                       TraceSorter::SortingMode::kDefault);
+        break;
+      case TraceSorter::SortingMode::kFullSort:
+        break;
+    }
+  }
+
+  // TODO(carlscab) Make sure kProtoTraceType and kSystraceTraceType are parsed
+  // first so that we do not get issues with SetPidZeroIsUpidZeroIdleProcess()
+  if (trace_type == kProtoTraceType || trace_type == kSystraceTraceType) {
+    context_->process_tracker->SetPidZeroIsUpidZeroIdleProcess();
+  }
+
+  return base::OkStatus();
+}
+
 base::Status ForwardingTraceParser::Parse(TraceBlobView blob) {
   // If this is the first Parse() call, guess the trace type and create the
   // appropriate parser.
   if (!reader_) {
-    TraceType trace_type;
-    {
-      auto scoped_trace = context_->storage->TraceExecutionTimeIntoStats(
-          stats::guess_trace_type_duration_ns);
-      trace_type = GuessTraceType(blob.data(), blob.size());
-      context_->trace_type = trace_type;
-    }
-    switch (trace_type) {
-      case kJsonTraceType: {
-        PERFETTO_DLOG("JSON trace detected");
-        if (context_->json_trace_tokenizer && context_->json_trace_parser) {
-          reader_ = std::move(context_->json_trace_tokenizer);
-
-          // JSON traces have no guarantees about the order of events in them.
-          context_->sorter.reset(
-              new TraceSorter(context_, TraceSorter::SortingMode::kFullSort));
-          break;
-        }
-        return base::ErrStatus("JSON support is disabled");
-      }
-      case kProtoTraceType: {
-        PERFETTO_DLOG("Proto trace detected");
-        auto sorting_mode = ConvertSortingMode(context_->config.sorting_mode);
-        reader_.reset(new ProtoTraceReader(context_));
-        context_->sorter.reset(new TraceSorter(context_, sorting_mode));
-        context_->process_tracker->SetPidZeroIsUpidZeroIdleProcess();
-        break;
-      }
-      case kNinjaLogTraceType: {
-        PERFETTO_DLOG("Ninja log detected");
-        if (context_->ninja_log_parser) {
-          reader_ = std::move(context_->ninja_log_parser);
-          break;
-        }
-        return base::ErrStatus("Ninja support is disabled");
-      }
-      case kFuchsiaTraceType: {
-        PERFETTO_DLOG("Fuchsia trace detected");
-        if (context_->fuchsia_record_parser &&
-            context_->fuchsia_trace_tokenizer) {
-          reader_ = std::move(context_->fuchsia_trace_tokenizer);
-
-          // Fuschia traces can have massively out of order events.
-          context_->sorter.reset(
-              new TraceSorter(context_, TraceSorter::SortingMode::kFullSort));
-          break;
-        }
-        return base::ErrStatus("Fuchsia support is disabled");
-      }
-      case kSystraceTraceType:
-        PERFETTO_DLOG("Systrace trace detected");
-        context_->process_tracker->SetPidZeroIsUpidZeroIdleProcess();
-        if (context_->systrace_trace_parser) {
-          reader_ = std::move(context_->systrace_trace_parser);
-          break;
-        }
-        return base::ErrStatus("Systrace support is disabled");
-      case kGzipTraceType:
-      case kCtraceTraceType:
-        if (trace_type == kGzipTraceType) {
-          PERFETTO_DLOG("gzip trace detected");
-        } else {
-          PERFETTO_DLOG("ctrace trace detected");
-        }
-        if (context_->gzip_trace_parser) {
-          reader_ = std::move(context_->gzip_trace_parser);
-          break;
-        }
-        return base::ErrStatus(kNoZlibErr);
-      case kAndroidBugreportTraceType:
-        PERFETTO_DLOG("Android Bugreport detected");
-        if (context_->android_bugreport_parser) {
-          reader_ = std::move(context_->android_bugreport_parser);
-          break;
-        }
-        return base::ErrStatus("Android Bugreport support is disabled. %s",
-                               kNoZlibErr);
-      case kPerfDataTraceType:
-        PERFETTO_DLOG("perf data detected");
-        if (context_->perf_data_trace_tokenizer &&
-            context_->perf_record_parser) {
-          reader_ = std::move(context_->perf_data_trace_tokenizer);
-          context_->sorter.reset(
-              new TraceSorter(context_, TraceSorter::SortingMode::kDefault));
-          break;
-        }
-        return base::ErrStatus("perf.data parsing support is disabled.");
-      case kUnknownTraceType:
-        // If renaming this error message don't remove the "(ERR:fmt)" part.
-        // The UI's error_dialog.ts uses it to make the dialog more graceful.
-        return base::ErrStatus("Unknown trace type provided (ERR:fmt)");
-    }
+    RETURN_IF_ERROR(Init(blob));
   }
 
   return reader_->Parse(std::move(blob));
@@ -167,71 +143,5 @@
   reader_->NotifyEndOfFile();
 }
 
-TraceType GuessTraceType(const uint8_t* data, size_t size) {
-  if (size == 0)
-    return kUnknownTraceType;
-  std::string start(reinterpret_cast<const char*>(data),
-                    std::min<size_t>(size, kGuessTraceMaxLookahead));
-  if (size >= 8) {
-    uint64_t first_word;
-    memcpy(&first_word, data, sizeof(first_word));
-    if (first_word == kFuchsiaMagicNumber)
-      return kFuchsiaTraceType;
-  }
-  if (base::StartsWith(start, kPerfMagic)) {
-    return kPerfDataTraceType;
-  }
-  std::string start_minus_white_space = RemoveWhitespace(start);
-  if (base::StartsWith(start_minus_white_space, "{\""))
-    return kJsonTraceType;
-  if (base::StartsWith(start_minus_white_space, "[{\""))
-    return kJsonTraceType;
-
-  // Systrace with header but no leading HTML.
-  if (base::Contains(start, "# tracer"))
-    return kSystraceTraceType;
-
-  // Systrace with leading HTML.
-  // Both: <!DOCTYPE html> and <!DOCTYPE HTML> have been observed.
-  std::string lower_start = base::ToLower(start);
-  if (base::StartsWith(lower_start, "<!doctype html>") ||
-      base::StartsWith(lower_start, "<html>"))
-    return kSystraceTraceType;
-
-  // Traces obtained from atrace -z (compress).
-  // They all have the string "TRACE:" followed by 78 9C which is a zlib header
-  // for "deflate, default compression, window size=32K" (see b/208691037)
-  if (base::Contains(start, "TRACE:\n\x78\x9c"))
-    return kCtraceTraceType;
-
-  // Traces obtained from atrace without -z (no compression).
-  if (base::Contains(start, "TRACE:\n"))
-    return kSystraceTraceType;
-
-  // Ninja's build log (.ninja_log).
-  if (base::StartsWith(start, "# ninja log"))
-    return kNinjaLogTraceType;
-
-  // Systrace with no header or leading HTML.
-  if (base::StartsWith(start, " "))
-    return kSystraceTraceType;
-
-  // gzip'ed trace containing one of the other formats.
-  if (base::StartsWith(start, "\x1f\x8b"))
-    return kGzipTraceType;
-
-  if (base::StartsWith(start, "\x0a"))
-    return kProtoTraceType;
-
-  // Android bugreport.zip
-  // TODO(primiano). For now we assume any .zip file is a bugreport. In future,
-  // if we want to support different trace formats based on a .zip arachive we
-  // will need an extra layer similar to what we did kGzipTraceType.
-  if (base::StartsWith(start, "PK\x03\x04"))
-    return kAndroidBugreportTraceType;
-
-  return kUnknownTraceType;
-}
-
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/forwarding_trace_parser.h b/src/trace_processor/forwarding_trace_parser.h
index e63a272..1f32938 100644
--- a/src/trace_processor/forwarding_trace_parser.h
+++ b/src/trace_processor/forwarding_trace_parser.h
@@ -17,16 +17,14 @@
 #ifndef SRC_TRACE_PROCESSOR_FORWARDING_TRACE_PARSER_H_
 #define SRC_TRACE_PROCESSOR_FORWARDING_TRACE_PARSER_H_
 
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 
-#include "src/trace_processor/types/trace_processor_context.h"
-
 namespace perfetto {
 namespace trace_processor {
 
-constexpr size_t kGuessTraceMaxLookahead = 64;
-
-TraceType GuessTraceType(const uint8_t* data, size_t size);
+class TraceProcessorContext;
 
 class ForwardingTraceParser : public ChunkedTraceReader {
  public:
@@ -38,6 +36,7 @@
   void NotifyEndOfFile() override;
 
  private:
+  base::Status Init(const TraceBlobView&);
   TraceProcessorContext* const context_;
   std::unique_ptr<ChunkedTraceReader> reader_;
 };
diff --git a/src/trace_processor/forwarding_trace_parser_unittest.cc b/src/trace_processor/forwarding_trace_parser_unittest.cc
index 74cb631..42d577d 100644
--- a/src/trace_processor/forwarding_trace_parser_unittest.cc
+++ b/src/trace_processor/forwarding_trace_parser_unittest.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/forwarding_trace_parser.h"
 
+#include "src/trace_processor/util/trace_type.h"
 #include "test/gtest_and_gmock.h"
 
 namespace perfetto {
diff --git a/src/trace_processor/importers/common/BUILD.gn b/src/trace_processor/importers/common/BUILD.gn
index 43dfb23..dc3e11a 100644
--- a/src/trace_processor/importers/common/BUILD.gn
+++ b/src/trace_processor/importers/common/BUILD.gn
@@ -45,6 +45,8 @@
     "mapping_tracker.h",
     "metadata_tracker.cc",
     "metadata_tracker.h",
+    "process_track_translation_table.cc",
+    "process_track_translation_table.h",
     "process_tracker.cc",
     "process_tracker.h",
     "sched_event_state.h",
@@ -87,6 +89,7 @@
     "../../util:build_id",
     "../../util:profiler_util",
     "../fuchsia:fuchsia_record",
+    "../perf:record",
     "../systrace:systrace_line",
   ]
 }
@@ -116,6 +119,7 @@
     "deobfuscation_mapping_table_unittest.cc",
     "event_tracker_unittest.cc",
     "flow_tracker_unittest.cc",
+    "process_track_translation_table_unittest.cc",
     "process_tracker_unittest.cc",
     "slice_tracker_unittest.cc",
     "slice_translation_table_unittest.cc",
diff --git a/src/trace_processor/importers/common/async_track_set_tracker_unittest.cc b/src/trace_processor/importers/common/async_track_set_tracker_unittest.cc
index b10aec8..173fd89 100644
--- a/src/trace_processor/importers/common/async_track_set_tracker_unittest.cc
+++ b/src/trace_processor/importers/common/async_track_set_tracker_unittest.cc
@@ -18,6 +18,7 @@
 
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "test/gtest_and_gmock.h"
@@ -34,6 +35,8 @@
     context_.args_tracker.reset(new ArgsTracker(&context_));
     context_.track_tracker.reset(new TrackTracker(&context_));
     context_.async_track_set_tracker.reset(new AsyncTrackSetTracker(&context_));
+    context_.process_track_translation_table.reset(
+        new ProcessTrackTranslationTable(context_.storage.get()));
 
     storage_ = context_.storage.get();
     tracker_ = context_.async_track_set_tracker.get();
diff --git a/src/trace_processor/importers/common/mapping_tracker.cc b/src/trace_processor/importers/common/mapping_tracker.cc
index 0dec3e5..965c15f 100644
--- a/src/trace_processor/importers/common/mapping_tracker.cc
+++ b/src/trace_processor/importers/common/mapping_tracker.cc
@@ -164,5 +164,15 @@
       });
 }
 
+VirtualMemoryMapping* MappingTracker::GetDummyMapping() {
+  if (!dummy_mapping_) {
+    CreateMappingParams params;
+    params.memory_range =
+        AddressRange::FromStartAndSize(0, std::numeric_limits<uint64_t>::max());
+    dummy_mapping_ = &InternMemoryMapping(params);
+  }
+  return dummy_mapping_;
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/mapping_tracker.h b/src/trace_processor/importers/common/mapping_tracker.h
index bc45bef..08c767e 100644
--- a/src/trace_processor/importers/common/mapping_tracker.h
+++ b/src/trace_processor/importers/common/mapping_tracker.h
@@ -91,6 +91,10 @@
   // Jitted ranges will only be applied to UserMemoryMappings
   void AddJitRange(UniquePid upid, AddressRange range, JitCache* jit_cache);
 
+  // Sometimes we just need a mapping and we are lacking trace data to create a
+  // proper one. Use this mapping in those cases.
+  VirtualMemoryMapping* GetDummyMapping();
+
  private:
   template <typename MappingImpl>
   MappingImpl& AddMapping(std::unique_ptr<MappingImpl> mapping);
@@ -136,6 +140,8 @@
   KernelMemoryMapping* kernel_ = nullptr;
 
   base::FlatHashMap<UniquePid, AddressRangeMap<JitCache*>> jit_caches_;
+
+  VirtualMemoryMapping* dummy_mapping_;
 };
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/common/process_track_translation_table.cc b/src/trace_processor/importers/common/process_track_translation_table.cc
new file mode 100644
index 0000000..cee50c6
--- /dev/null
+++ b/src/trace_processor/importers/common/process_track_translation_table.cc
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
+
+namespace perfetto::trace_processor {
+
+ProcessTrackTranslationTable::ProcessTrackTranslationTable(TraceStorage* storage)
+    : storage_(storage) {}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/process_track_translation_table.h b/src/trace_processor/importers/common/process_track_translation_table.h
new file mode 100644
index 0000000..30f5f56
--- /dev/null
+++ b/src/trace_processor/importers/common/process_track_translation_table.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PROCESS_TRACK_TRANSLATION_TABLE_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PROCESS_TRACK_TRANSLATION_TABLE_H_
+
+#include <cstdint>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto::trace_processor {
+
+// Tracks and stores slice translation rules. It allows Trace Processor
+// to for example deobfuscate slice names.
+class ProcessTrackTranslationTable {
+ public:
+  ProcessTrackTranslationTable(TraceStorage* storage);
+
+  // If the name is not mapped to anything, assumes that no translation is
+  // necessry, and returns the raw_name.
+  StringId TranslateName(StringId raw_name) const {
+    const auto* mapped_name = raw_to_deobfuscated_name_.Find(raw_name);
+    return mapped_name ? *mapped_name : raw_name;
+  }
+
+  void AddNameTranslationRule(base::StringView raw,
+                              base::StringView deobfuscated) {
+    const StringId raw_id = storage_->InternString(raw);
+    const StringId deobfuscated_id = storage_->InternString(deobfuscated);
+    raw_to_deobfuscated_name_[raw_id] = deobfuscated_id;
+  }
+
+ private:
+  TraceStorage* storage_;
+  base::FlatHashMap<StringId, StringId> raw_to_deobfuscated_name_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_PROCESS_TRACK_TRANSLATION_TABLE_H_
diff --git a/src/trace_processor/importers/common/process_track_translation_table_unittest.cc b/src/trace_processor/importers/common/process_track_translation_table_unittest.cc
new file mode 100644
index 0000000..a947204
--- /dev/null
+++ b/src/trace_processor/importers/common/process_track_translation_table_unittest.cc
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+namespace {
+
+TEST(ProcessTrackTranslationTable, UnknownName) {
+  TraceStorage storage;
+  ProcessTrackTranslationTable table(&storage);
+  const StringId raw_name = storage.InternString("name1");
+  EXPECT_EQ(raw_name, table.TranslateName(raw_name));
+}
+
+TEST(ProcessTrackTranslationTable, MappedName) {
+  TraceStorage storage;
+  ProcessTrackTranslationTable table(&storage);
+  table.AddNameTranslationRule("raw_name1", "mapped_name1");
+  const StringId raw_name = storage.InternString("raw_name1");
+  const StringId mapped_name = storage.InternString("mapped_name1");
+  EXPECT_EQ(mapped_name, table.TranslateName(raw_name));
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index bf90c95..8e41a7d 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -20,8 +20,10 @@
 #include <stdint.h>
 #include <string>
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
+namespace perf_importer {
+struct Record;
+}
 
 class PacketSequenceStateGeneration;
 class TraceBlobView;
@@ -59,10 +61,9 @@
 class PerfRecordParser {
  public:
   virtual ~PerfRecordParser();
-  virtual void ParsePerfRecord(int64_t, TraceBlobView) = 0;
+  virtual void ParsePerfRecord(int64_t, perf_importer::Record) = 0;
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/common/track_tracker.cc b/src/trace_processor/importers/common/track_tracker.cc
index 5026391..4db3020 100644
--- a/src/trace_processor/importers/common/track_tracker.cc
+++ b/src/trace_processor/importers/common/track_tracker.cc
@@ -19,7 +19,9 @@
 #include <optional>
 
 #include "src/trace_processor/importers/common/args_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -140,7 +142,7 @@
 }
 
 TrackId TrackTracker::InternLegacyChromeAsyncTrack(
-    StringId name,
+    StringId raw_name,
     uint32_t upid,
     int64_t trace_id,
     bool trace_id_is_process_scoped,
@@ -151,6 +153,8 @@
   tuple.trace_id = trace_id;
   tuple.source_scope = source_scope;
 
+  const StringId name =
+      context_->process_track_translation_table->TranslateName(raw_name);
   auto it = chrome_tracks_.find(tuple);
   if (it != chrome_tracks_.end()) {
     if (name != kNullStringId) {
@@ -194,9 +198,11 @@
   return id;
 }
 
-TrackId TrackTracker::CreateProcessAsyncTrack(StringId name,
+TrackId TrackTracker::CreateProcessAsyncTrack(StringId raw_name,
                                               UniquePid upid,
                                               StringId source) {
+  const StringId name =
+      context_->process_track_translation_table->TranslateName(raw_name);
   tables::ProcessTrackTable::Row row(name);
   row.upid = upid;
   row.machine_id = context_->machine_id();
@@ -318,10 +324,12 @@
   return track;
 }
 
-TrackId TrackTracker::InternProcessCounterTrack(StringId name,
+TrackId TrackTracker::InternProcessCounterTrack(StringId raw_name,
                                                 UniquePid upid,
                                                 StringId unit,
                                                 StringId description) {
+  const StringId name =
+      context_->process_track_translation_table->TranslateName(raw_name);
   auto it = upid_counter_tracks_.find(std::make_pair(name, upid));
   if (it != upid_counter_tracks_.end()) {
     return it->second;
diff --git a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
index ca02bdc..90a289b 100644
--- a/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
+++ b/src/trace_processor/importers/fuchsia/fuchsia_parser_unittest.cc
@@ -28,6 +28,7 @@
 #include "src/trace_processor/importers/common/event_tracker.h"
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
@@ -248,6 +249,8 @@
     context_.ftrace_sched_tracker.reset(sched_);
     process_ = new NiceMock<MockProcessTracker>(&context_);
     context_.process_tracker.reset(process_);
+    context_.process_track_translation_table.reset(
+        new ProcessTrackTranslationTable(storage_));
     slice_ = new NiceMock<MockSliceTracker>(&context_);
     context_.slice_tracker.reset(slice_);
     context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
diff --git a/src/trace_processor/importers/perf/BUILD.gn b/src/trace_processor/importers/perf/BUILD.gn
index 376f9b4..7a3f2a3 100644
--- a/src/trace_processor/importers/perf/BUILD.gn
+++ b/src/trace_processor/importers/perf/BUILD.gn
@@ -16,6 +16,8 @@
 
 source_set("record") {
   sources = [
+    "perf_counter.cc",
+    "perf_counter.h",
     "perf_event.h",
     "perf_event_attr.cc",
     "perf_event_attr.h",
@@ -26,38 +28,44 @@
   ]
   deps = [
     "../../../../gn:default_deps",
+    "../../../../include/perfetto/ext/base:base",
+    "../../../../include/perfetto/trace_processor:trace_processor",
     "../../../../protos/perfetto/trace/profiling:zero",
-    "../../sorter",
     "../../storage",
     "../../tables:tables_python",
     "../../types",
-    "../common",
     "../common:parser_types",
   ]
 }
 source_set("perf") {
   sources = [
-    "perf_data_parser.cc",
-    "perf_data_parser.h",
-    "perf_data_reader.cc",
-    "perf_data_reader.h",
+    "attrs_section_reader.cc",
+    "attrs_section_reader.h",
+    "features.cc",
+    "features.h",
+    "mmap_record.cc",
+    "mmap_record.h",
     "perf_data_tokenizer.cc",
     "perf_data_tokenizer.h",
-    "perf_data_tracker.cc",
-    "perf_data_tracker.h",
     "perf_file.h",
-    "reader.h",
+    "record_parser.cc",
+    "record_parser.h",
+    "sample.cc",
+    "sample.h",
   ]
   public_deps = [ ":record" ]
   deps = [
     "../../../../gn:default_deps",
+    "../../../../protos/perfetto/trace:zero",
     "../../../../protos/perfetto/trace/profiling:zero",
     "../../sorter",
     "../../storage",
     "../../tables:tables_python",
     "../../types",
-    "../common",
-    "../common:parser_types",
+    "../../util:build_id",
+    "../../util:file_buffer",
+    "../../util:util",
+    "../common:common",
     "../proto:minimal",
   ]
 }
@@ -65,8 +73,6 @@
 perfetto_unittest_source_set("unittests") {
   testonly = true
   sources = [
-    "perf_data_reader_unittest.cc",
-    "perf_data_tracker_unittest.cc",
     "perf_session_unittest.cc",
     "reader_unittest.cc",
   ]
diff --git a/src/trace_processor/importers/perf/attrs_section_reader.cc b/src/trace_processor/importers/perf/attrs_section_reader.cc
new file mode 100644
index 0000000..f19ea3e
--- /dev/null
+++ b/src/trace_processor/importers/perf/attrs_section_reader.cc
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/attrs_section_reader.h"
+
+#include <cinttypes>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/perf_file.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+// static
+base::StatusOr<AttrsSectionReader> AttrsSectionReader::Create(
+    const PerfFile::Header& header,
+    TraceBlobView section) {
+  PERFETTO_CHECK(section.size() == header.attrs.size);
+
+  if (header.attr_size == 0) {
+    return base::ErrStatus("Invalid attr_size (0) in perf file header.");
+  }
+
+  if (header.attrs.size % header.attr_size != 0) {
+    return base::ErrStatus("Invalid attrs section size %" PRIu64
+                           " for attr_size %" PRIu64 " in perf file header.",
+                           header.attrs.size, header.attr_size);
+  }
+
+  const size_t num_attr = header.attrs.size / header.attr_size;
+
+  // Each entry is a perf_event_attr followed by a Section, but the size of
+  // the perf_event_attr struct written in the file might not be the same as
+  // sizeof(perf_event_attr) as this struct might grow over time (can be
+  // bigger or smaller).
+  static constexpr size_t kSectionSize = sizeof(PerfFile::Section);
+  if (header.attr_size < kSectionSize) {
+    return base::ErrStatus(
+        "Invalid attr_size in file header. Expected at least %zu, found "
+        "%" PRIu64,
+        kSectionSize, header.attr_size);
+  }
+  const size_t attr_size = header.attr_size - kSectionSize;
+
+  return AttrsSectionReader(std::move(section), num_attr, attr_size);
+}
+
+base::Status AttrsSectionReader::ReadNext(PerfFile::AttrsEntry& entry) {
+  PERFETTO_CHECK(reader_.ReadPerfEventAttr(entry.attr, attr_size_));
+
+  if (entry.attr.size != attr_size_) {
+    return base::ErrStatus(
+        "Invalid attr.size. Expected %zu, but found %" PRIu32, attr_size_,
+        entry.attr.size);
+  }
+
+  PERFETTO_CHECK(reader_.Read(entry.ids));
+  --num_attr_;
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/attrs_section_reader.h b/src/trace_processor/importers/perf/attrs_section_reader.h
new file mode 100644
index 0000000..a7eaaa3
--- /dev/null
+++ b/src/trace_processor/importers/perf/attrs_section_reader.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ATTRS_SECTION_READER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ATTRS_SECTION_READER_H_
+
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/perf_file.h"
+#include "src/trace_processor/importers/perf/reader.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+// Helper to read the attrs section of a perf file. Provides an iterator like
+// interface over the perf_event_attr entries.
+class AttrsSectionReader {
+ public:
+  // Creates a new iterator.
+  // `attrs_section` data contained in the attrs section of the perf file.
+  static base::StatusOr<AttrsSectionReader> Create(
+      const PerfFile::Header& header,
+      TraceBlobView attrs_section);
+
+  // Returns true while there are available entries to read via `ReadNext`.
+  bool CanReadNext() const { return num_attr_ != 0; }
+
+  // Reads the next entry. Can onlybe called if `HasMore` returns true.
+  base::Status ReadNext(PerfFile::AttrsEntry& entry);
+
+ private:
+  AttrsSectionReader(TraceBlobView section, size_t num_attr, size_t attr_size)
+      : reader_(std::move(section)),
+        num_attr_(num_attr),
+        attr_size_(attr_size) {}
+
+  Reader reader_;
+  size_t num_attr_;
+  const size_t attr_size_;
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_ATTRS_SECTION_READER_H_
diff --git a/src/trace_processor/importers/perf/features.cc b/src/trace_processor/importers/perf/features.cc
new file mode 100644
index 0000000..861a8bf
--- /dev/null
+++ b/src/trace_processor/importers/perf/features.cc
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/features.h"
+
+#include <cstdint>
+#include <utility>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/trace_processor/status.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto::trace_processor::perf_importer::feature {
+namespace {
+
+bool ParseString(Reader& reader, std::string& out) {
+  uint32_t len;
+  base::StringView str;
+  if (!reader.Read(len) || len == 0 || !reader.ReadStringView(str, len)) {
+    return false;
+  }
+
+  if (str.at(len - 1) != '\0') {
+    return false;
+  }
+
+  out = std::string(str.data(), len - 1);
+  return true;
+}
+
+bool ParseBuildId(const perf_event_header& header,
+                  TraceBlobView blob,
+                  BuildId& out) {
+  Reader reader(std::move(blob));
+  struct {
+    char data[20];
+    uint8_t size;
+    uint8_t reserved[3];
+  } build_id;
+
+  if (!reader.Read(out.pid) || !reader.Read(build_id) ||
+      !reader.ReadStringUntilEndOrNull(out.filename)) {
+    return false;
+  }
+
+  if (header.misc & PERF_RECORD_MISC_EXT_RESERVED) {
+    if (build_id.size > sizeof(build_id.data)) {
+      return false;
+    }
+  } else {
+    // Probably a simpleperf trace. Simpleperf fills build_ids with zeros up
+    // to a length of 20 and leaves the rest uninitialized :( so we can not read
+    // build_id.size or build_id.reserved to do any checks.
+    // TODO(b/334978369): We should be able to tell for sure whether this is
+    // simpleperf or not by checking the existence of SimpleperfMetaInfo.
+    build_id.size = 20;
+    // BuildIds are usually SHA-1 hashes (20 bytes), sometimes MD5 (16 bites).
+    // Simpleperf adds trailing zeros. But zeros could be in the MD5 hash.
+    // But it the last 4 bytes are zeros there is a high chance this was an
+    // MD5.
+    if (build_id.data[16] == 0 && build_id.data[17] == 0 &&
+        build_id.data[18] == 0 && build_id.data[19] == 0) {
+      build_id.size = 16;
+    }
+  }
+  out.build_id = std::string(build_id.data, build_id.size);
+  return true;
+}
+
+util::Status ParseEventTypeInfo(std::string value, SimpleperfMetaInfo& out) {
+  for (const auto& line : base::SplitString(value, "\n")) {
+    auto tokens = base::SplitString(line, ",");
+    if (tokens.size() != 3) {
+      return util::ErrStatus("Invalid event_type_info: '%s'", line.c_str());
+    }
+
+    auto type = base::StringToUInt32(tokens[1]);
+    if (!type) {
+      return util::ErrStatus("Could not parse type in event_type_info: '%s'",
+                             tokens[1].c_str());
+    }
+    auto config = base::StringToUInt64(tokens[2]);
+    if (!config) {
+      return util::ErrStatus("Could not parse config in event_type_info: '%s'",
+                             tokens[2].c_str());
+    }
+
+    out.event_type_info.Insert({*type, *config}, std::move(tokens[0]));
+  }
+
+  return util::OkStatus();
+}
+
+util::Status ParseSimpleperfMetaInfoEntry(
+    std::pair<std::string, std::string> entry,
+    SimpleperfMetaInfo& out) {
+  static constexpr char kEventTypeInfoKey[] = "event_type_info";
+  if (entry.first == kEventTypeInfoKey) {
+    return ParseEventTypeInfo(std::move(entry.second), out);
+  }
+
+  PERFETTO_CHECK(
+      out.entries.Insert(std::move(entry.first), std::move(entry.second))
+          .second);
+  return util::OkStatus();
+}
+
+}  // namespace
+
+// static
+util::Status BuildId::Parse(TraceBlobView bytes,
+                            std::function<util::Status(BuildId)> cb) {
+  Reader reader(std::move(bytes));
+  while (reader.size_left() != 0) {
+    perf_event_header header;
+    TraceBlobView payload;
+    if (!reader.Read(header)) {
+      return base::ErrStatus(
+          "Failed to parse feature BuildId. Could not read header.");
+    }
+    if (header.size < sizeof(header)) {
+      return base::ErrStatus(
+          "Failed to parse feature BuildId. Invalid size in header.");
+    }
+    if (!reader.ReadBlob(payload, header.size - sizeof(header))) {
+      return base::ErrStatus(
+          "Failed to parse feature BuildId. Could not read payload.");
+    }
+
+    BuildId build_id;
+    if (!ParseBuildId(header, std::move(payload), build_id)) {
+      return base::ErrStatus(
+          "Failed to parse feature BuildId. Could not read entry.");
+    }
+
+    RETURN_IF_ERROR(cb(std::move(build_id)));
+  }
+  return util::OkStatus();
+}
+
+// static
+util::Status SimpleperfMetaInfo::Parse(const TraceBlobView& bytes,
+                                       SimpleperfMetaInfo& out) {
+  auto* it_end = reinterpret_cast<const char*>(bytes.data() + bytes.size());
+  for (auto* it = reinterpret_cast<const char*>(bytes.data()); it != it_end;) {
+    auto end = std::find(it, it_end, '\0');
+    if (end == it_end) {
+      return util::ErrStatus("Failed to read key from Simpleperf MetaInfo");
+    }
+    std::string key(it, end);
+    it = end;
+    ++it;
+    if (it == it_end) {
+      return util::ErrStatus("Missing value in Simpleperf MetaInfo");
+    }
+    end = std::find(it, it_end, '\0');
+    if (end == it_end) {
+      return util::ErrStatus("Failed to read value from Simpleperf MetaInfo");
+    }
+    std::string value(it, end);
+    it = end;
+    ++it;
+
+    RETURN_IF_ERROR(ParseSimpleperfMetaInfoEntry(
+        std::make_pair(std::move(key), std::move(value)), out));
+  }
+  return util::OkStatus();
+}
+
+// static
+util::Status EventDescription::Parse(
+    TraceBlobView bytes,
+    std::function<util::Status(EventDescription)> cb) {
+  Reader reader(std::move(bytes));
+  uint32_t nr;
+  uint32_t attr_size;
+  if (!reader.Read(nr) || !reader.Read(attr_size)) {
+    return util::ErrStatus("Failed to parse header for PERF_EVENT_DESC");
+  }
+
+  for (; nr != 0; --nr) {
+    EventDescription desc;
+    uint32_t nr_ids;
+    if (!reader.ReadPerfEventAttr(desc.attr, attr_size) ||
+        !reader.Read(nr_ids) || !ParseString(reader, desc.event_string)) {
+      return util::ErrStatus("Failed to parse record for PERF_EVENT_DESC");
+    }
+
+    desc.ids.resize(nr_ids);
+    for (uint64_t& id : desc.ids) {
+      if (!reader.Read(id)) {
+        return util::ErrStatus("Failed to parse ids for PERF_EVENT_DESC");
+      }
+    }
+    RETURN_IF_ERROR(cb(std::move(desc)));
+  }
+  return util::OkStatus();
+}
+
+util::Status ParseSimpleperfFile2(
+    TraceBlobView bytes,
+    std::function<util::Status(TraceBlobView)> cb) {
+  Reader reader(std::move(bytes));
+  while (reader.size_left() != 0) {
+    uint32_t len;
+    if (!reader.Read(len)) {
+      return base::ErrStatus("Failed to parse len in FEATURE_SIMPLEPERF_FILE2");
+    }
+    TraceBlobView payload;
+    if (!reader.ReadBlob(payload, len)) {
+      return base::ErrStatus(
+          "Failed to parse payload in FEATURE_SIMPLEPERF_FILE2");
+    }
+    RETURN_IF_ERROR(cb(std::move(payload)));
+  }
+  return util::OkStatus();
+}
+
+// static
+util::Status HeaderGroupDesc::Parse(TraceBlobView bytes, HeaderGroupDesc& out) {
+  Reader reader(std::move(bytes));
+  uint32_t nr;
+  if (!reader.Read(nr)) {
+    return util::ErrStatus("Failed to parse header for HEADER_GROUP_DESC");
+  }
+
+  HeaderGroupDesc group_desc;
+  group_desc.entries.resize(nr);
+  for (auto& e : group_desc.entries) {
+    if (!ParseString(reader, e.string) || !reader.Read(e.leader_idx) ||
+        !reader.Read(e.nr_members)) {
+      return util::ErrStatus("Failed to parse HEADER_GROUP_DESC entry");
+    }
+  }
+  out = std::move(group_desc);
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer::feature
diff --git a/src/trace_processor/importers/perf/features.h b/src/trace_processor/importers/perf/features.h
new file mode 100644
index 0000000..2a6c22b
--- /dev/null
+++ b/src/trace_processor/importers/perf/features.h
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_FEATURES_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_FEATURES_H_
+
+#include <cstdint>
+#include <functional>
+#include <limits>
+#include <string>
+#include <vector>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/hash.h"
+#include "perfetto/trace_processor/status.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+
+namespace perfetto ::trace_processor {
+class TraceBlobView;
+
+namespace perf_importer::feature {
+
+enum Id : uint8_t {
+  ID_RESERVED = 0,
+  ID_TRACING_DATA = 1,
+  ID_BUILD_ID = 2,
+  ID_HOSTNAME = 3,
+  ID_OS_RELEASE = 4,
+  ID_VERSION = 5,
+  ID_ARCH = 6,
+  ID_NR_CPUS = 7,
+  ID_CPU_DESC = 8,
+  ID_CPU_ID = 9,
+  ID_TOTAL_MEM = 10,
+  ID_CMD_LINE = 11,
+  ID_EVENT_DESC = 12,
+  ID_CPU_TOPOLOGY = 13,
+  ID_NUMA_TOPOLOGY = 14,
+  ID_BRANCH_STACK = 15,
+  ID_PMU_MAPPINGS = 16,
+  ID_GROUP_DESC = 17,
+  ID_AUX_TRACE = 18,
+  ID_STAT = 19,
+  ID_CACHE = 20,
+  ID_SAMPLE_TIME = 21,
+  ID_SAMPLE_TOPOLOGY = 22,
+  ID_CLOCK_ID = 23,
+  ID_DIR_FORMAT = 24,
+  ID_BPF_PROG_INFO = 25,
+  ID_BPF_BTF = 26,
+  ID_COMPRESSED = 27,
+  ID_CPU_PUM_CAPS = 28,
+  ID_CLOCK_DATA = 29,
+  ID_HYBRID_TOPOLOGY = 30,
+  ID_PMU_CAPS = 31,
+  ID_SIMPLEPERF_FILE = 128,
+  ID_SIMPLEPERF_META_INFO = 129,
+  ID_SIMPLEPERF_FILE2 = 132,
+  ID_MAX = std::numeric_limits<uint8_t>::max(),
+};
+
+struct BuildId {
+  static util::Status Parse(TraceBlobView,
+                            std::function<util::Status(BuildId)> cb);
+  int32_t pid;
+  std::string build_id;
+  std::string filename;
+};
+
+struct HeaderGroupDesc {
+  static util::Status Parse(TraceBlobView, HeaderGroupDesc& out);
+  struct Entry {
+    std::string string;
+    uint32_t leader_idx;
+    uint32_t nr_members;
+  };
+  std::vector<Entry> entries;
+};
+
+struct EventDescription {
+  static util::Status Parse(TraceBlobView,
+                            std::function<util::Status(EventDescription)> cb);
+  perf_event_attr attr;
+  std::string event_string;
+  std::vector<uint64_t> ids;
+};
+
+struct SimpleperfMetaInfo {
+  static util::Status Parse(const TraceBlobView&, SimpleperfMetaInfo& out);
+  base::FlatHashMap<std::string, std::string> entries;
+  struct EventTypeAndConfig {
+    uint32_t type;
+    uint64_t config;
+    bool operator==(const EventTypeAndConfig& other) {
+      return type == other.type && config == other.config;
+    }
+    bool operator!=(const EventTypeAndConfig& other) {
+      return !(*this == other);
+    }
+    struct Hasher {
+      size_t operator()(const EventTypeAndConfig& o) const {
+        return static_cast<size_t>(base::Hasher::Combine(o.config, o.type));
+      }
+    };
+  };
+  using EventName = std::string;
+  base::FlatHashMap<EventTypeAndConfig, EventName, EventTypeAndConfig::Hasher>
+      event_type_info;
+};
+
+util::Status ParseSimpleperfFile2(
+    TraceBlobView,
+    std::function<util::Status(TraceBlobView)> cb);
+
+}  // namespace perf_importer::feature
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_FEATURES_H_
diff --git a/src/trace_processor/importers/perf/mmap_record.cc b/src/trace_processor/importers/perf/mmap_record.cc
new file mode 100644
index 0000000..d11fdd5
--- /dev/null
+++ b/src/trace_processor/importers/perf/mmap_record.cc
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/mmap_record.h"
+
+#include <optional>
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+base::Status MmapRecord::Parse(const Record& record) {
+  Reader reader(record.payload.copy());
+  if (!reader.Read(*static_cast<CommonMmapRecordFields*>(this)) ||
+      !reader.ReadCString(filename)) {
+    return base::ErrStatus("Failed to parse MMAP record");
+  }
+  cpu_mode = record.GetCpuMode();
+  return base::OkStatus();
+}
+
+base::Status Mmap2Record::Parse(const Record& record) {
+  Reader reader(record.payload.copy());
+  if (!reader.Read(*static_cast<BaseMmap2Record*>(this)) ||
+      !reader.ReadCString(filename)) {
+    return base::ErrStatus("Failed to parse MMAP record");
+  }
+
+  has_build_id = record.mmap_has_build_id();
+
+  if (has_build_id && build_id.build_id_size >
+                          BaseMmap2Record::BuildIdFields::kMaxBuildIdSize) {
+    return base::ErrStatus(
+        "Invalid build_id_size in MMAP2 record. Expected <= %zu but found "
+        "%" PRIu8,
+        BaseMmap2Record::BuildIdFields::kMaxBuildIdSize,
+        build_id.build_id_size);
+  }
+
+  cpu_mode = record.GetCpuMode();
+
+  return base::OkStatus();
+}
+
+std::optional<BuildId> Mmap2Record::GetBuildId() const {
+  return has_build_id ? std::make_optional(BuildId::FromRaw(std::string(
+                            build_id.build_id_buf, build_id.build_id_size)))
+                      : std::nullopt;
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/mmap_record.h b/src/trace_processor/importers/perf/mmap_record.h
new file mode 100644
index 0000000..37b3939
--- /dev/null
+++ b/src/trace_processor/importers/perf/mmap_record.h
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_MMAP_RECORD_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_MMAP_RECORD_H_
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include "perfetto/base/status.h"
+#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
+#include "src/trace_processor/util/build_id.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+
+struct CommonMmapRecordFields {
+  uint32_t pid;
+  uint32_t tid;
+  uint64_t addr;
+  uint64_t len;
+  uint64_t pgoff;
+};
+
+struct MmapRecord : public CommonMmapRecordFields {
+  std::string filename;
+  protos::pbzero::Profiling::CpuMode cpu_mode;
+
+  base::Status Parse(const Record& record);
+};
+
+struct BaseMmap2Record : public CommonMmapRecordFields {
+  struct BuildIdFields {
+    static constexpr size_t kMaxBuildIdSize = 20;
+    uint8_t build_id_size;
+    uint8_t reserved_1;
+    uint16_t reserved_2;
+    char build_id_buf[kMaxBuildIdSize];
+  };
+  struct InodeFields {
+    uint32_t maj;
+    uint32_t min;
+    int64_t ino;
+    uint64_t ino_generation;
+  };
+  static_assert(sizeof(BuildIdFields) == sizeof(InodeFields));
+
+  union {
+    BuildIdFields build_id;
+    InodeFields inode;
+  };
+  uint32_t prot;
+  uint32_t flags;
+};
+
+struct Mmap2Record : public BaseMmap2Record {
+  std::string filename;
+  protos::pbzero::Profiling::CpuMode cpu_mode;
+  bool has_build_id;
+
+  base::Status Parse(const Record& record);
+  std::optional<BuildId> GetBuildId() const;
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_MMAP_RECORD_H_
diff --git a/src/trace_processor/importers/perf/perf_counter.cc b/src/trace_processor/importers/perf/perf_counter.cc
new file mode 100644
index 0000000..685e940
--- /dev/null
+++ b/src/trace_processor/importers/perf/perf_counter.cc
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/perf_counter.h"
+
+#include <cstdint>
+
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/tables/counter_tables_py.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+void PerfCounter::AddDelta(int64_t ts, double delta) {
+  last_count_ += delta;
+  counter_table_.Insert({ts, track_id_, last_count_});
+}
+
+void PerfCounter::AddCount(int64_t ts, double count) {
+  PERFETTO_CHECK(count >= last_count_);
+  last_count_ = count;
+  counter_table_.Insert({ts, track_id_, last_count_});
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_counter.h b/src/trace_processor/importers/perf/perf_counter.h
new file mode 100644
index 0000000..fb7a28c
--- /dev/null
+++ b/src/trace_processor/importers/perf/perf_counter.h
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_COUNTER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_COUNTER_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/tables/counter_tables_py.h"
+#include "src/trace_processor/tables/track_tables_py.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+// Helper class to keep track of perf counters and convert delta values found in
+// perf files to absolute values needed for the perfetto counter table.
+class PerfCounter {
+ public:
+  PerfCounter(tables::CounterTable* counter_table,
+              const tables::PerfCounterTrackTable::ConstRowReference& track)
+      : counter_table_(*counter_table),
+        track_id_(track.id()),
+        is_timebase_(track.is_timebase()) {}
+
+  bool is_timebase() const { return is_timebase_; }
+
+  void AddDelta(int64_t ts, double delta);
+  void AddCount(int64_t ts, double count);
+
+ private:
+  tables::CounterTable& counter_table_;
+  tables::PerfCounterTrackTable::Id track_id_;
+  const bool is_timebase_;
+  double last_count_{0};
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_COUNTER_H_
diff --git a/src/trace_processor/importers/perf/perf_data_parser.cc b/src/trace_processor/importers/perf/perf_data_parser.cc
deleted file mode 100644
index e3dfef3..0000000
--- a/src/trace_processor/importers/perf/perf_data_parser.cc
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/perf/perf_data_parser.h"
-
-#include <optional>
-#include <string>
-#include <vector>
-#include "perfetto/base/logging.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
-#include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/profiler_tables_py.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-
-using FramesTable = tables::StackProfileFrameTable;
-using CallsitesTable = tables::StackProfileCallsiteTable;
-
-PerfDataParser::PerfDataParser(TraceProcessorContext* context)
-    : context_(context), tracker_(PerfDataTracker::GetOrCreate(context_)) {}
-
-PerfDataParser::~PerfDataParser() = default;
-
-base::StatusOr<PerfDataTracker::PerfSample> PerfDataParser::ParseSample(
-    TraceBlobView tbv) {
-  perf_importer::PerfDataReader reader(std::move(tbv));
-  return tracker_->ParseSample(reader);
-}
-
-void PerfDataParser::ParsePerfRecord(int64_t ts, TraceBlobView tbv) {
-  auto sample_status = ParseSample(std::move(tbv));
-  if (!sample_status.ok()) {
-    return;
-  }
-  PerfDataTracker::PerfSample sample = *sample_status;
-
-  // The sample has been validated in tokenizer so callchain shouldn't be empty.
-  PERFETTO_CHECK(!sample.callchain.empty());
-
-  // First instruction pointer in the callchain should be from kernel space, so
-  // it shouldn't be available in mappings.
-  UniquePid upid = context_->process_tracker->GetOrCreateProcess(*sample.pid);
-  if (context_->mapping_tracker->FindUserMappingForAddress(
-          upid, sample.callchain.front())) {
-    context_->storage->IncrementStats(stats::perf_samples_skipped);
-    return;
-  }
-
-  if (sample.callchain.size() == 1) {
-    context_->storage->IncrementStats(stats::perf_samples_skipped);
-    return;
-  }
-
-  std::vector<FramesTable::Row> frame_rows;
-  for (uint32_t i = 1; i < sample.callchain.size(); i++) {
-    UserMemoryMapping* mapping =
-        context_->mapping_tracker->FindUserMappingForAddress(
-            upid, sample.callchain[i]);
-    if (!mapping) {
-      context_->storage->IncrementStats(stats::perf_samples_skipped);
-      return;
-    }
-    FramesTable::Row new_row;
-    std::string mock_name =
-        base::StackString<1024>(
-            "%" PRIu64, sample.callchain[i] - mapping->memory_range().start())
-            .ToStdString();
-    new_row.name = context_->storage->InternString(mock_name.c_str());
-    new_row.mapping = mapping->mapping_id();
-    new_row.rel_pc =
-        static_cast<int64_t>(mapping->ToRelativePc(sample.callchain[i]));
-    frame_rows.push_back(new_row);
-  }
-
-  // Insert frames. We couldn't do it before as no frames should be added if the
-  // mapping couldn't be found for any of them.
-  const auto& frames = context_->storage->mutable_stack_profile_frame_table();
-  std::vector<FramesTable::Id> frame_ids;
-  for (const auto& row : frame_rows) {
-    frame_ids.push_back(frames->Insert(row).id);
-  }
-
-  // Insert callsites.
-  const auto& callsites =
-      context_->storage->mutable_stack_profile_callsite_table();
-
-  std::optional<CallsitesTable::Id> parent_callsite_id;
-  for (uint32_t i = 0; i < frame_ids.size(); i++) {
-    CallsitesTable::Row callsite_row;
-    callsite_row.frame_id = frame_ids[i];
-    callsite_row.depth = i;
-    callsite_row.parent_id = parent_callsite_id;
-    parent_callsite_id = callsites->Insert(callsite_row).id;
-  }
-
-  // Insert stack sample.
-  tables::PerfSampleTable::Row perf_sample_row;
-  perf_sample_row.callsite_id = parent_callsite_id;
-  perf_sample_row.ts = ts;
-  if (sample.cpu) {
-    perf_sample_row.cpu = *sample.cpu;
-  }
-  if (sample.tid) {
-    auto utid = context_->process_tracker->GetOrCreateThread(*sample.tid);
-    perf_sample_row.utid = utid;
-  }
-  context_->storage->mutable_perf_sample_table()->Insert(perf_sample_row);
-}
-
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/perf_data_parser.h b/src/trace_processor/importers/perf/perf_data_parser.h
deleted file mode 100644
index f2ab0a3..0000000
--- a/src/trace_processor/importers/perf/perf_data_parser.h
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_PARSER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_PARSER_H_
-
-#include <stdint.h>
-
-#include "perfetto/base/compiler.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/common/trace_parser.h"
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-
-// Parses samples from perf.data files.
-class PerfDataParser : public PerfRecordParser {
- public:
-  explicit PerfDataParser(TraceProcessorContext*);
-  ~PerfDataParser() override;
-
-  // The data in TraceBlobView has to be a perf.data sample.
-  void ParsePerfRecord(int64_t timestamp, TraceBlobView) override;
-
- private:
-  base::StatusOr<PerfDataTracker::PerfSample> ParseSample(TraceBlobView);
-
-  TraceProcessorContext* context_ = nullptr;
-  PerfDataTracker* tracker_ = nullptr;
-};
-
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_PARSER_H_
diff --git a/src/trace_processor/importers/perf/perf_data_reader.cc b/src/trace_processor/importers/perf/perf_data_reader.cc
deleted file mode 100644
index e061826..0000000
--- a/src/trace_processor/importers/perf/perf_data_reader.cc
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-
-#include <cstddef>
-#include <optional>
-#include <vector>
-#include "perfetto/base/logging.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-void PerfDataReader::SkipSlow(size_t bytes_to_skip) {
-  size_t bytes_in_buffer = BytesInBuffer();
-
-  // Size fits in buffer.
-  if (bytes_in_buffer >= bytes_to_skip) {
-    buffer_offset_ += bytes_to_skip;
-    return;
-  }
-
-  // Empty the buffer and increase the |blob_offset_|.
-  buffer_offset_ = 0;
-  buffer_.clear();
-  blob_offset_ += bytes_to_skip - bytes_in_buffer;
-}
-
-void PerfDataReader::PeekSlow(uint8_t* obj_data, size_t size) const {
-  size_t bytes_in_buffer = BytesInBuffer();
-
-  // Read from buffer.
-  if (bytes_in_buffer >= size) {
-    memcpy(obj_data, buffer_.data() + buffer_offset_, size);
-    return;
-  }
-
-  // Read from blob and buffer.
-  memcpy(obj_data, buffer_.data() + buffer_offset_, bytes_in_buffer);
-  memcpy(obj_data + bytes_in_buffer, tbv_.data() + blob_offset_,
-         size - bytes_in_buffer);
-}
-
-TraceBlobView PerfDataReader::PeekTraceBlobViewSlow(size_t size) const {
-  auto blob = TraceBlob::Allocate(size);
-  size_t bytes_in_buffer = BytesInBuffer();
-
-  // Data is in buffer, so we need to create a new TraceBlob from it.
-  if (bytes_in_buffer >= size) {
-    memcpy(blob.data(), buffer_.data() + buffer_offset_, size);
-    return TraceBlobView(std::move(blob));
-  }
-
-  // Data is in between blob and buffer and we need to dump data from buffer
-  // and blob to a new TraceBlob.
-  size_t bytes_from_blob = size - bytes_in_buffer;
-  memcpy(blob.data(), buffer_.data() + buffer_offset_, bytes_in_buffer);
-  memcpy(blob.data() + bytes_in_buffer, tbv_.data() + blob_offset_,
-         bytes_from_blob);
-  return TraceBlobView(std::move(blob));
-}
-
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/perf_data_reader.h b/src/trace_processor/importers/perf/perf_data_reader.h
deleted file mode 100644
index acf07d1..0000000
--- a/src/trace_processor/importers/perf/perf_data_reader.h
+++ /dev/null
@@ -1,187 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_READER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_READER_H_
-
-#include <stdint.h>
-#include <cstddef>
-#include <cstring>
-#include <optional>
-#include <vector>
-
-#include "perfetto/base/logging.h"
-#include "perfetto/trace_processor/trace_blob.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-
-// Reader class for tokenizing and parsing. Currently used by perf importer, but
-// it's design is not related to perf. Responsible for hiding away the
-// complexity of reading values from TraceBlobView and glueing the tbvs together
-// in case there is data between many of them.
-class PerfDataReader {
- public:
-  PerfDataReader() = default;
-  explicit PerfDataReader(TraceBlobView tbv) : tbv_(std::move(tbv)) {}
-
-  // Updates old TraceBlobView with new one. If there is data left in the old
-  // one, it will be saved in the buffer.
-  void Append(TraceBlobView tbv) {
-    uint64_t size_before = BytesAvailable();
-    buffer_.insert(buffer_.end(), tbv_.data() + blob_offset_,
-                   tbv_.data() + tbv_.size());
-    tbv_ = std::move(tbv);
-    blob_offset_ = 0;
-
-    // Post condition. Checks whether no data has been lost in the Append.
-    PERFETTO_DCHECK(BytesAvailable() == size_before + tbv.size());
-  }
-
-  // Reads the |obj| and updates |file_offset_| of the reader.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  template <typename T>
-  void Read(T& obj) {
-    Peek(obj);
-    Skip<T>();
-  }
-
-  // Reads the T value for std::optional<T>.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  template <typename T>
-  void ReadOptional(std::optional<T>& obj) {
-    T val;
-    Read(val);
-    obj = val;
-  }
-
-  // Reads all of the data in the |vec| and updates |file_offset_| of the
-  // reader.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  template <typename T>
-  void ReadVector(std::vector<T>& vec) {
-    PERFETTO_DCHECK(CanReadSize(sizeof(T) * vec.size()));
-    for (T& val : vec) {
-      Read(val);
-    }
-  }
-
-  // Updates the |file_offset_| by the sizeof(T).
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  template <typename T>
-  void Skip() {
-    Skip(sizeof(T));
-  }
-
-  // Updates the |file_offset_| by the |bytes_to_skip|.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  void Skip(uint64_t bytes_to_skip) {
-    uint64_t bytes_available_before = BytesAvailable();
-    PERFETTO_DCHECK(CanReadSize(bytes_to_skip));
-    size_t skip = static_cast<size_t>(bytes_to_skip);
-
-    // Incrementing file offset is not related to the way data is split.
-    file_offset_ += skip;
-    size_t bytes_in_buffer = BytesInBuffer();
-
-    // Empty buffer. Increment |blob_offset_|.
-    if (PERFETTO_LIKELY(bytes_in_buffer == 0)) {
-      buffer_offset_ = 0;
-      buffer_.clear();
-      blob_offset_ += skip;
-    } else {
-      SkipSlow(skip);
-    }
-    PERFETTO_DCHECK(BytesAvailable() == bytes_available_before - skip);
-  }
-
-  // Peeks the |obj| without updating the |file_offset_| of the reader.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  template <typename T>
-  void Peek(T& obj) const {
-    PERFETTO_DCHECK(CanReadSize(sizeof(T)));
-    size_t bytes_available_before = BytesAvailable();
-
-    // Read from blob.
-    if (PERFETTO_LIKELY(BytesInBuffer() == 0)) {
-      memcpy(&obj, tbv_.data() + blob_offset_, sizeof(T));
-    } else {
-      PeekSlow(reinterpret_cast<uint8_t*>(&obj), sizeof(T));
-    }
-
-    PERFETTO_DCHECK(BytesAvailable() == bytes_available_before);
-  }
-
-  // Creates TraceBlobView with data of |data_size| bytes from current offset.
-  // NOTE: Assumes count of bytes available is higher than sizeof(T).
-  TraceBlobView PeekTraceBlobView(uint64_t data_size) const {
-    PERFETTO_DCHECK(CanReadSize(data_size));
-    size_t size = static_cast<size_t>(data_size);
-    size_t bytes_in_buffer = BytesInBuffer();
-
-    // Data is in blob, so it's enough to slice the existing |tbv_|.
-    if (PERFETTO_LIKELY(bytes_in_buffer == 0)) {
-      return tbv_.slice(tbv_.data() + blob_offset_, size);
-    }
-    return PeekTraceBlobViewSlow(size);
-  }
-
-  // Returns if there is enough data to read offsets between |start| and |end|.
-  bool CanAccessFileRange(uint64_t start, uint64_t end) const {
-    return CanAccessFileOffset(static_cast<size_t>(start)) &&
-           CanAccessFileOffset(static_cast<size_t>(end));
-  }
-
-  // Returns if there is enough data to read |size| bytes.
-  bool CanReadSize(uint64_t size) const { return size <= BytesAvailable(); }
-
-  uint64_t current_file_offset() const { return file_offset_; }
-
- private:
-  void SkipSlow(size_t bytes_to_skip);
-
-  void PeekSlow(uint8_t* obj_data, size_t) const;
-
-  TraceBlobView PeekTraceBlobViewSlow(size_t) const;
-
-  size_t BytesInBuffer() const {
-    PERFETTO_DCHECK(buffer_.size() >= buffer_offset_);
-    return buffer_.size() - buffer_offset_;
-  }
-  size_t BytesInBlob() const { return tbv_.size() - blob_offset_; }
-  size_t BytesAvailable() const { return BytesInBuffer() + BytesInBlob(); }
-
-  bool CanAccessFileOffset(size_t off) const {
-    return off >= file_offset_ && off <= file_offset_ + BytesAvailable();
-  }
-
-  TraceBlobView tbv_;
-  std::vector<uint8_t> buffer_;
-
-  // Where we are in relation to the current blob.
-  size_t blob_offset_ = 0;
-  // Where we are in relation to the file.
-  size_t file_offset_ = 0;
-  // Where we are in relation to the buffer.
-  size_t buffer_offset_ = 0;
-};
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_READER_H_
diff --git a/src/trace_processor/importers/perf/perf_data_reader_unittest.cc b/src/trace_processor/importers/perf/perf_data_reader_unittest.cc
deleted file mode 100644
index 5bf3081..0000000
--- a/src/trace_processor/importers/perf/perf_data_reader_unittest.cc
+++ /dev/null
@@ -1,234 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-
-#include <stddef.h>
-
-#include "perfetto/base/build_config.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-
-namespace {
-template <typename T>
-TraceBlobView TraceBlobViewFromVector(std::vector<T> nums) {
-  size_t data_size = sizeof(T) * nums.size();
-  auto blob = TraceBlob::Allocate(data_size);
-  memcpy(blob.data(), nums.data(), data_size);
-  return TraceBlobView(std::move(blob));
-}
-}  // namespace
-
-TEST(PerfDataReaderUnittest, AppendToEmpty) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{1, 2, 3});
-  PerfDataReader reader;
-  EXPECT_FALSE(reader.CanReadSize(1));
-  reader.Append(std::move(tbv));
-  EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 2));
-}
-
-TEST(PerfDataReaderUnittest, Append) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{1, 2, 3});
-  PerfDataReader reader(std::move(tbv));
-
-  EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 3));
-  EXPECT_FALSE(reader.CanReadSize(sizeof(uint64_t) * 3 + 1));
-
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 2}));
-  EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 5));
-}
-
-TEST(PerfDataReaderUnittest, Read) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-  uint64_t val;
-  reader.Read(val);
-  EXPECT_EQ(val, 2u);
-}
-
-TEST(PerfDataReaderUnittest, ReadFromBuffer) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 6});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3}));
-
-  // Now the first vector should be in the buffer.
-  uint64_t val;
-  reader.Read(val);
-  EXPECT_EQ(val, 2u);
-}
-
-TEST(PerfDataReaderUnittest, ReadBetweenBufferAndBlob) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  struct Nums {
-    uint64_t x;
-    uint64_t y;
-    uint64_t z;
-  };
-
-  Nums nums;
-  reader.Read(nums);
-
-  EXPECT_EQ(nums.x, 2u);
-  EXPECT_EQ(nums.y, 4u);
-  EXPECT_EQ(nums.z, 1u);
-}
-
-TEST(PerfDataReaderUnittest, ReadOptional) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-  std::optional<uint64_t> val;
-  reader.ReadOptional(val);
-  EXPECT_EQ(val, 2u);
-}
-
-TEST(PerfDataReaderUnittest, ReadVector) {
-  TraceBlobView tbv =
-      TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8, 16, 32});
-  PerfDataReader reader(std::move(tbv));
-
-  std::vector<uint64_t> res(3);
-  reader.ReadVector(res);
-
-  std::vector<uint64_t> valid{2, 4, 8};
-  EXPECT_EQ(res, valid);
-}
-
-TEST(PerfDataReaderUnittest, Skip) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-
-  reader.Skip<uint64_t>();
-
-  uint64_t val;
-  reader.Read(val);
-  EXPECT_EQ(val, 4u);
-}
-
-TEST(PerfDataReaderUnittest, SkipInBuffer) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  reader.Skip<uint64_t>();
-  EXPECT_EQ(reader.current_file_offset(), sizeof(uint64_t));
-}
-
-TEST(PerfDataReaderUnittest, SkipBetweenBufferAndBlob) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  struct Nums {
-    uint64_t x;
-    uint64_t y;
-    uint64_t z;
-  };
-
-  reader.Skip<Nums>();
-  EXPECT_EQ(reader.current_file_offset(), sizeof(Nums));
-}
-
-TEST(PerfDataReaderUnittest, Peek) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-
-  uint64_t peek_val;
-  reader.Peek(peek_val);
-
-  uint64_t val;
-  reader.Read(val);
-  EXPECT_EQ(val, 2u);
-}
-
-TEST(PerfDataReaderUnittest, PeekFromBuffer) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 6});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3}));
-
-  uint64_t val;
-  reader.Peek(val);
-  EXPECT_EQ(val, 2u);
-}
-
-TEST(PerfDataReaderUnittest, PeekBetweenBufferAndBlob) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  struct Nums {
-    uint64_t x;
-    uint64_t y;
-    uint64_t z;
-  };
-
-  Nums nums;
-  reader.Peek(nums);
-
-  EXPECT_EQ(nums.x, 2u);
-  EXPECT_EQ(nums.y, 4u);
-  EXPECT_EQ(nums.z, 1u);
-}
-
-TEST(PerfDataReaderUnittest, GetTraceBlobView) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-  EXPECT_TRUE(reader.CanReadSize(sizeof(uint64_t) * 3));
-
-  TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 2);
-  PerfDataReader new_reader(std::move(new_tbv));
-  EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 2));
-  EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
-}
-
-TEST(PerfDataReaderUnittest, GetTraceBlobViewFromBuffer) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 2);
-  PerfDataReader new_reader(std::move(new_tbv));
-  EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 2));
-  EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
-}
-
-TEST(PerfDataReaderUnittest, GetTraceBlobViewFromBetweenBufferAndBlob) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4});
-  PerfDataReader reader(std::move(tbv));
-  reader.Append(TraceBlobViewFromVector(std::vector<uint64_t>{1, 3, 5}));
-
-  TraceBlobView new_tbv = reader.PeekTraceBlobView(sizeof(uint64_t) * 3);
-  PerfDataReader new_reader(std::move(new_tbv));
-  EXPECT_TRUE(new_reader.CanReadSize(sizeof(uint64_t) * 3));
-  EXPECT_FALSE(new_reader.CanReadSize(sizeof(uint64_t) * 4));
-}
-
-TEST(PerfDataReaderUnittest, CanAccessFileRange) {
-  TraceBlobView tbv = TraceBlobViewFromVector(std::vector<uint64_t>{2, 4, 8});
-  PerfDataReader reader(std::move(tbv));
-  EXPECT_TRUE(reader.CanAccessFileRange(2, sizeof(uint64_t) * 3));
-  EXPECT_FALSE(reader.CanAccessFileRange(2, sizeof(uint64_t) * 3 + 10));
-}
-
-}  // namespace perf_importer
-
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.cc b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
index 25b54b9..2dcc3e5 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.cc
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
@@ -16,56 +16,97 @@
 
 #include "src/trace_processor/importers/perf/perf_data_tokenizer.h"
 
+#include <cstddef>
 #include <cstdint>
 #include <cstring>
+#include <optional>
+#include <utility>
 #include <vector>
 
+#include "perfetto/base/flat_set.h"
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
 #include "perfetto/ext/base/status_or.h"
+#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "protos/perfetto/trace/clock_snapshot.pbzero.h"
+#include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
+#include "src/trace_processor/importers/perf/attrs_section_reader.h"
+#include "src/trace_processor/importers/perf/features.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/perf_file.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/util/status_macros.h"
 
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-
 namespace perfetto {
 namespace trace_processor {
 namespace perf_importer {
 namespace {
-protos::pbzero::Profiling::CpuMode GetCpuMode(const perf_event_header& header) {
-  switch (header.misc & kPerfRecordMiscCpumodeMask) {
-    case PERF_RECORD_MISC_KERNEL:
-      return protos::pbzero::Profiling::MODE_KERNEL;
-    case PERF_RECORD_MISC_USER:
-      return protos::pbzero::Profiling::MODE_USER;
-    case PERF_RECORD_MISC_HYPERVISOR:
-      return protos::pbzero::Profiling::MODE_HYPERVISOR;
-    case PERF_RECORD_MISC_GUEST_KERNEL:
-      return protos::pbzero::Profiling::MODE_GUEST_KERNEL;
-    case PERF_RECORD_MISC_GUEST_USER:
-      return protos::pbzero::Profiling::MODE_GUEST_USER;
-    default:
-      return protos::pbzero::Profiling::MODE_UNKNOWN;
+
+void AddIds(uint8_t id_offset,
+            uint64_t flags,
+            base::FlatSet<uint8_t>& feature_ids) {
+  for (size_t i = 0; i < sizeof(flags) * 8; ++i) {
+    if (flags & 1) {
+      feature_ids.insert(id_offset);
+    }
+    flags >>= 1;
+    ++id_offset;
   }
 }
+
+base::FlatSet<uint8_t> ExtractFeatureIds(const uint64_t& flags,
+                                         const uint64_t (&flags1)[3]) {
+  base::FlatSet<uint8_t> feature_ids;
+  AddIds(0, flags, feature_ids);
+  AddIds(64, flags1[0], feature_ids);
+  AddIds(128, flags1[1], feature_ids);
+  AddIds(192, flags1[2], feature_ids);
+  return feature_ids;
+}
+
+bool ReadTime(const Record& record, std::optional<uint64_t>& time) {
+  if (!record.attr) {
+    time = std::nullopt;
+    return true;
+  }
+  Reader reader(record.payload.copy());
+  if (record.header.type != PERF_RECORD_SAMPLE) {
+    std::optional<size_t> offset = record.attr->time_offset_from_end();
+    if (!offset.has_value()) {
+      time = std::nullopt;
+      return true;
+    }
+    if (*offset > reader.size_left()) {
+      return false;
+    }
+    return reader.Skip(reader.size_left() - *offset) &&
+           reader.ReadOptional(time);
+  }
+
+  std::optional<size_t> offset = record.attr->time_offset_from_start();
+  if (!offset.has_value()) {
+    time = std::nullopt;
+    return true;
+  }
+  return reader.Skip(*offset) && reader.ReadOptional(time);
+}
+
 }  // namespace
 
 PerfDataTokenizer::PerfDataTokenizer(TraceProcessorContext* ctx)
-    : context_(ctx),
-      tracker_(PerfDataTracker::GetOrCreate(context_)),
-      reader_() {}
+    : context_(ctx) {}
 
 PerfDataTokenizer::~PerfDataTokenizer() = default;
 
 // A normal perf.data consts of:
 // [ header ]
-// [ event ids (one array per attr) ]
 // [ attr section ]
 // [ data section ]
 // [ optional feature sections ]
@@ -75,223 +116,310 @@
 // Most file format documentation is outdated or misleading, instead see
 // perf_session__do_write_header() in linux/tools/perf/util/header.c.
 base::Status PerfDataTokenizer::Parse(TraceBlobView blob) {
-  reader_.Append(std::move(blob));
+  buffer_.PushBack(std::move(blob));
 
-  while (parsing_state_ != ParsingState::Records) {
-    base::StatusOr<ParsingResult> parsed = ParsingResult::Success;
+  base::StatusOr<ParsingResult> result = ParsingResult::kSuccess;
+  while (result.ok() && result.value() == ParsingResult::kSuccess &&
+         !buffer_.empty()) {
     switch (parsing_state_) {
-      case ParsingState::Records:
+      case ParsingState::kParseHeader:
+        result = ParseHeader();
         break;
-      case ParsingState::Header:
-        parsed = ParseHeader();
+
+      case ParsingState::kParseAttrs:
+        result = ParseAttrs();
         break;
-      case ParsingState::AfterHeaderBuffer:
-        parsed = ParseAfterHeaderBuffer();
+
+      case ParsingState::kSeekRecords:
+        result = SeekRecords();
         break;
-      case ParsingState::Attrs:
-        parsed = ParseAttrs();
+
+      case ParsingState::kParseRecords:
+        result = ParseRecords();
         break;
-      case ParsingState::AttrIdsFromBuffer:
-        parsed = ParseAttrIdsFromBuffer();
+
+      case ParsingState::kParseFeatures:
+        result = ParseFeatures();
         break;
-      case ParsingState::AttrIds:
-        parsed = ParseAttrIds();
+
+      case ParsingState::kParseFeatureSections:
+        result = ParseFeatureSections();
         break;
+
+      case ParsingState::kDone:
+        result = base::ErrStatus("Unexpected data");
     }
-
-    // There has been an error while parsing.
-    RETURN_IF_ERROR(parsed.status());
-
-    // There is not enough data to parse so we need to load another blob.
-    if (*parsed == ParsingResult::NoSpace)
-      return base::OkStatus();
   }
-
-  while (reader_.current_file_offset() < header_.data.end()) {
-    // Make sure |perf_event_header| of the sample is available.
-    if (!reader_.CanReadSize(sizeof(perf_event_header))) {
-      return base::OkStatus();
-    }
-
-    perf_event_header ev_header;
-    reader_.Peek(ev_header);
-    PERFETTO_CHECK(ev_header.size >= sizeof(perf_event_header));
-
-    if (!reader_.CanReadSize(ev_header.size)) {
-      return base::OkStatus();
-    }
-
-    reader_.Skip<perf_event_header>();
-    uint64_t record_offset = reader_.current_file_offset();
-    uint64_t record_size = ev_header.size - sizeof(perf_event_header);
-
-    switch (ev_header.type) {
-      case PERF_RECORD_SAMPLE: {
-        TraceBlobView tbv = reader_.PeekTraceBlobView(record_size);
-        auto sample_status = tracker_->ParseSample(reader_);
-        if (!sample_status.ok()) {
-          continue;
-        }
-        PerfDataTracker::PerfSample sample = *sample_status;
-        if (!ValidateSample(*sample_status)) {
-          continue;
-        }
-        context_->sorter->PushPerfRecord(
-            static_cast<int64_t>(*sample_status->ts), std::move(tbv));
-        break;
-      }
-      case PERF_RECORD_MMAP2: {
-        PERFETTO_CHECK(ev_header.size >=
-                       sizeof(PerfDataTracker::Mmap2Record::Numeric));
-        auto record = ParseMmap2Record(record_size);
-        RETURN_IF_ERROR(record.status());
-        record->cpu_mode = GetCpuMode(ev_header);
-        tracker_->PushMmap2Record(*record);
-        break;
-      }
-      default:
-        break;
-    }
-
-    reader_.Skip((record_offset + record_size) - reader_.current_file_offset());
-  }
-
-  return base::OkStatus();
+  return result.status();
 }
 
 base::StatusOr<PerfDataTokenizer::ParsingResult>
 PerfDataTokenizer::ParseHeader() {
-  if (!reader_.CanReadSize(sizeof(PerfHeader))) {
-    return ParsingResult::NoSpace;
+  auto tbv = buffer_.SliceOff(0, sizeof(header_));
+  if (!tbv) {
+    return ParsingResult::kMoreDataNeeded;
   }
-  reader_.Read(header_);
-  PERFETTO_CHECK(header_.size == sizeof(PerfHeader));
-  if (header_.attr_size !=
-      sizeof(perf_event_attr) + sizeof(PerfDataTracker::PerfFileSection)) {
-    return base::ErrStatus(
-        "Unsupported: perf.data collected with a different ABI version of "
-        "perf_event_attr.");
+  PERFETTO_CHECK(Reader(std::move(*tbv)).Read(header_));
+
+  // TODO: Check for endianess (big endian will have letters reversed);
+  if (memcmp(header_.magic, PerfFile::kPerfMagic,
+             sizeof(PerfFile::kPerfMagic)) != 0) {
+    return util::ErrStatus("Invalid magic string");
   }
 
-  if (header_.attrs.offset > header_.data.offset) {
-    return base::ErrStatus(
-        "Can only import files where samples are located after the metadata.");
+  if (header_.size != sizeof(PerfFile::Header)) {
+    return util::ErrStatus("Failed to perf file header size. Expected %" PRIu64
+                           ", found %zu",
+                           sizeof(PerfFile::Header));
   }
 
-  if (header_.size == header_.attrs.offset) {
-    parsing_state_ = ParsingState::Attrs;
-  } else {
-    parsing_state_ = ParsingState::AfterHeaderBuffer;
-  }
-  return ParsingResult::Success;
-}
+  feature_ids_ = ExtractFeatureIds(header_.flags, header_.flags1);
+  feature_headers_section_ = {header_.data.end(),
+                              feature_ids_.size() * sizeof(PerfFile::Section)};
+  context_->clock_tracker->SetTraceTimeClock(
+      protos::pbzero::ClockSnapshot::Clock::MONOTONIC);
 
-base::StatusOr<PerfDataTokenizer::ParsingResult>
-PerfDataTokenizer::ParseAfterHeaderBuffer() {
-  if (!reader_.CanAccessFileRange(header_.size, header_.attrs.offset)) {
-    return ParsingResult::NoSpace;
-  }
-  after_header_buffer_.resize(
-      static_cast<size_t>(header_.attrs.offset - header_.size));
-  reader_.ReadVector(after_header_buffer_);
-  parsing_state_ = ParsingState::Attrs;
-  return ParsingResult::Success;
+  PERFETTO_CHECK(buffer_.PopFrontUntil(sizeof(PerfFile::Header)));
+  parsing_state_ = ParsingState::kParseAttrs;
+  return ParsingResult::kSuccess;
 }
 
 base::StatusOr<PerfDataTokenizer::ParsingResult>
 PerfDataTokenizer::ParseAttrs() {
-  if (!reader_.CanAccessFileRange(header_.attrs.offset, header_.attrs.end())) {
-    return ParsingResult::NoSpace;
-  }
-  reader_.Skip(header_.attrs.offset - reader_.current_file_offset());
-  PerfDataTracker::PerfFileAttr attr;
-  for (uint64_t i = header_.attrs.offset; i < header_.attrs.end();
-       i += header_.attr_size) {
-    reader_.Read(attr);
-    PERFETTO_CHECK(attr.ids.size % sizeof(uint64_t) == 0);
-    ids_start_ = std::min(ids_start_, attr.ids.offset);
-    ids_end_ = std::max(ids_end_, attr.ids.end());
-    attrs_.push_back(attr);
+  std::optional<TraceBlobView> tbv =
+      buffer_.SliceOff(header_.attrs.offset, header_.attrs.size);
+  if (!tbv) {
+    return ParsingResult::kMoreDataNeeded;
   }
 
-  if (ids_start_ == header_.size && ids_end_ <= header_.attrs.offset) {
-    parsing_state_ = ParsingState::AttrIdsFromBuffer;
-  } else {
-    parsing_state_ = ParsingState::AttrIds;
+  ASSIGN_OR_RETURN(AttrsSectionReader attr_reader,
+                   AttrsSectionReader::Create(header_, std::move(*tbv)));
+
+  PerfSession::Builder builder(
+      context_, context_->perf_sample_tracker->CreatePerfSession());
+  while (attr_reader.CanReadNext()) {
+    PerfFile::AttrsEntry entry;
+    RETURN_IF_ERROR(attr_reader.ReadNext(entry));
+
+    if (entry.ids.size % sizeof(uint64_t) != 0) {
+      return base::ErrStatus("Invalid id section size: %" PRIu64,
+                             entry.ids.size);
+    }
+
+    tbv = buffer_.SliceOff(entry.ids.offset, entry.ids.size);
+    if (!tbv) {
+      return ParsingResult::kMoreDataNeeded;
+    }
+
+    std::vector<uint64_t> ids;
+    ids.resize(entry.ids.size / sizeof(uint64_t));
+    PERFETTO_CHECK(Reader(std::move(*tbv)).ReadVector(ids));
+
+    builder.AddAttrAndIds(entry.attr, std::move(ids));
   }
-  return ParsingResult::Success;
+
+  ASSIGN_OR_RETURN(perf_session_, builder.Build());
+  parsing_state_ = ParsingState::kSeekRecords;
+  return ParsingResult::kSuccess;
 }
 
 base::StatusOr<PerfDataTokenizer::ParsingResult>
-PerfDataTokenizer::ParseAttrIds() {
-  if (!reader_.CanAccessFileRange(ids_start_, ids_end_)) {
-    return ParsingResult::NoSpace;
+PerfDataTokenizer::SeekRecords() {
+  if (!buffer_.PopFrontUntil(header_.data.offset)) {
+    return ParsingResult::kMoreDataNeeded;
   }
-  for (const auto& attr_file : attrs_) {
-    reader_.Skip(attr_file.ids.offset - reader_.current_file_offset());
-    std::vector<uint64_t> ids(static_cast<size_t>(attr_file.ids.size) /
-                              sizeof(uint64_t));
-    reader_.ReadVector(ids);
-    tracker_->PushAttrAndIds({attr_file.attr, std::move(ids)});
-  }
-  tracker_->ComputeCommonSampleType();
-
-  reader_.Skip(header_.data.offset - reader_.current_file_offset());
-  parsing_state_ = ParsingState::Records;
-  return ParsingResult::Success;
+  parsing_state_ = ParsingState::kParseRecords;
+  return ParsingResult::kSuccess;
 }
 
 base::StatusOr<PerfDataTokenizer::ParsingResult>
-PerfDataTokenizer::ParseAttrIdsFromBuffer() {
-  // Each attribute points at an array of event ids. In this case, the ids are
-  // in |after_header_buffer_|, i.e. the file contents between the header and
-  // the start of the attr section.
-  for (const auto& attr_file : attrs_) {
-    size_t num_ids = static_cast<size_t>(attr_file.ids.size / sizeof(uint64_t));
-    std::vector<uint64_t> ids(num_ids);
-    size_t rd_offset = static_cast<size_t>(attr_file.ids.offset - ids_start_);
-    size_t rd_size = static_cast<size_t>(attr_file.ids.size);
-    PERFETTO_CHECK(rd_offset + rd_size <= after_header_buffer_.size());
-    memcpy(ids.data(), after_header_buffer_.data() + rd_offset, rd_size);
+PerfDataTokenizer::ParseRecords() {
+  while (buffer_.file_offset() < header_.data.end()) {
+    Record record;
 
-    tracker_->PushAttrAndIds({attr_file.attr, std::move(ids)});
+    if (auto res = ParseRecord(record);
+        !res.ok() || *res != ParsingResult::kSuccess) {
+      return res;
+    }
+
+    if (!PushRecord(std::move(record))) {
+      context_->storage->IncrementStats(stats::perf_record_skipped);
+    }
   }
-  after_header_buffer_.clear();
-  tracker_->ComputeCommonSampleType();
 
-  reader_.Skip(header_.data.offset - reader_.current_file_offset());
-  parsing_state_ = ParsingState::Records;
-  return ParsingResult::Success;
+  parsing_state_ = ParsingState::kParseFeatureSections;
+  return ParsingResult::kSuccess;
 }
 
-base::StatusOr<PerfDataTracker::Mmap2Record>
-PerfDataTokenizer::ParseMmap2Record(uint64_t record_size) {
-  uint64_t start_offset = reader_.current_file_offset();
-  PerfDataTracker::Mmap2Record record;
-  reader_.Read(record.num);
-  std::vector<char> filename_buffer(
-      static_cast<size_t>(record_size) -
-      sizeof(PerfDataTracker::Mmap2Record::Numeric));
-  reader_.ReadVector(filename_buffer);
-  if (filename_buffer.back() != '\0') {
-    return base::ErrStatus(
-        "Invalid MMAP2 record: filename is not null terminated.");
+base::StatusOr<PerfDataTokenizer::ParsingResult> PerfDataTokenizer::ParseRecord(
+    Record& record) {
+  record.session = perf_session_;
+  std::optional<TraceBlobView> tbv =
+      buffer_.SliceOff(buffer_.file_offset(), sizeof(record.header));
+  if (!tbv) {
+    return ParsingResult::kMoreDataNeeded;
   }
-  record.filename = std::string(filename_buffer.begin(), filename_buffer.end());
-  PERFETTO_CHECK(reader_.current_file_offset() == start_offset + record_size);
-  return record;
+  PERFETTO_CHECK(Reader(std::move(*tbv)).Read(record.header));
+
+  if (record.header.size < sizeof(record.header)) {
+    return base::ErrStatus("Invalid record size: %" PRIu16, record.header.size);
+  }
+
+  tbv = buffer_.SliceOff(buffer_.file_offset() + sizeof(record.header),
+                         record.header.size - sizeof(record.header));
+  if (!tbv) {
+    return ParsingResult::kMoreDataNeeded;
+  }
+
+  record.payload = std::move(*tbv);
+
+  base::StatusOr<RefPtr<const PerfEventAttr>> attr =
+      perf_session_->FindAttrForRecord(record.header, record.payload);
+  if (!attr.ok()) {
+    return base::ErrStatus("Unable to determine perf_event_attr for record. %s",
+                           attr.status().c_message());
+  }
+  record.attr = *attr;
+
+  buffer_.PopFrontBytes(record.header.size);
+  return ParsingResult::kSuccess;
 }
 
-bool PerfDataTokenizer::ValidateSample(
-    const PerfDataTracker::PerfSample& sample) {
-  if (!sample.cpu.has_value() || !sample.ts.has_value() ||
-      sample.callchain.empty() || !sample.pid.has_value()) {
-    context_->storage->IncrementStats(stats::perf_samples_skipped);
+base::StatusOr<int64_t> PerfDataTokenizer::ToTraceTimestamp(
+    std::optional<uint64_t> time) {
+  base::StatusOr<int64_t> trace_ts =
+      time.has_value()
+          ? context_->clock_tracker->ToTraceTime(
+                protos::pbzero::ClockSnapshot::Clock::MONOTONIC,
+                static_cast<int64_t>(*time))
+          : std::max(latest_timestamp_, context_->sorter->max_timestamp());
+
+  if (PERFETTO_LIKELY(trace_ts.ok())) {
+    latest_timestamp_ = std::max(latest_timestamp_, *trace_ts);
+  }
+
+  return trace_ts;
+}
+
+bool PerfDataTokenizer::PushRecord(Record record) {
+  std::optional<uint64_t> time;
+  if (!ReadTime(record, time)) {
     return false;
   }
+
+  base::StatusOr<int64_t> trace_ts = ToTraceTimestamp(time);
+  if (!trace_ts.ok()) {
+    return false;
+  }
+
+  switch (record.header.type) {
+    case PERF_RECORD_AUXTRACE_INFO:
+    case PERF_RECORD_AUXTRACE:
+    case PERF_RECORD_AUX:
+      break;
+    default:
+      context_->sorter->PushPerfRecord(*trace_ts, std::move(record));
+      break;
+  }
+
   return true;
 }
 
+base::StatusOr<PerfDataTokenizer::ParsingResult>
+PerfDataTokenizer::ParseFeatureSections() {
+  PERFETTO_CHECK(buffer_.file_offset() == header_.data.end());
+  auto tbv = buffer_.SliceOff(feature_headers_section_.offset,
+                              feature_headers_section_.size);
+  if (!tbv) {
+    return ParsingResult::kMoreDataNeeded;
+  }
+
+  Reader reader(std::move(*tbv));
+  for (auto feature_id : feature_ids_) {
+    feature_sections_.emplace_back(std::piecewise_construct,
+                                   std::forward_as_tuple(feature_id),
+                                   std::forward_as_tuple());
+    PERFETTO_CHECK(reader.Read(feature_sections_.back().second));
+  }
+
+  std::sort(feature_sections_.begin(), feature_sections_.end(),
+            [](const std::pair<uint8_t, PerfFile::Section>& lhs,
+               const std::pair<uint8_t, PerfFile::Section>& rhs) {
+              return lhs.second.offset > rhs.second.offset;
+            });
+
+  buffer_.PopFrontUntil(feature_headers_section_.end());
+  parsing_state_ = feature_sections_.empty() ? ParsingState::kDone
+                                             : ParsingState::kParseFeatures;
+  return ParsingResult::kSuccess;
+}
+
+base::StatusOr<PerfDataTokenizer::ParsingResult>
+PerfDataTokenizer::ParseFeatures() {
+  while (!feature_sections_.empty()) {
+    const auto feature_id = feature_sections_.back().first;
+    const auto& section = feature_sections_.back().second;
+    auto tbv = buffer_.SliceOff(section.offset, section.size);
+    if (!tbv) {
+      return ParsingResult::kMoreDataNeeded;
+    }
+
+    RETURN_IF_ERROR(ParseFeature(feature_id, std::move(*tbv)));
+    buffer_.PopFrontUntil(section.end());
+    feature_sections_.pop_back();
+  }
+
+  parsing_state_ = ParsingState::kDone;
+  return ParsingResult::kSuccess;
+}
+
+base::Status PerfDataTokenizer::ParseFeature(uint8_t feature_id,
+                                             TraceBlobView data) {
+  switch (feature_id) {
+    case feature::ID_EVENT_DESC:
+      return feature::EventDescription::Parse(
+          std::move(data), [&](feature::EventDescription desc) {
+            for (auto id : desc.ids) {
+              perf_session_->SetEventName(id, std::move(desc.event_string));
+            }
+            return base::OkStatus();
+          });
+
+    case feature::ID_BUILD_ID:
+      return feature::BuildId::Parse(
+          std::move(data), [](feature::BuildId) { return base::OkStatus(); });
+
+    case feature::ID_GROUP_DESC: {
+      feature::HeaderGroupDesc group_desc;
+      RETURN_IF_ERROR(
+          feature::HeaderGroupDesc::Parse(std::move(data), group_desc));
+      // TODO(carlscab): Do someting
+      break;
+    }
+
+    case feature::ID_SIMPLEPERF_META_INFO: {
+      feature::SimpleperfMetaInfo meta_info;
+      RETURN_IF_ERROR(
+          feature::SimpleperfMetaInfo::Parse(std::move(data), meta_info));
+      for (auto it = meta_info.event_type_info.GetIterator(); it; ++it) {
+        perf_session_->SetEventName(it.key().type, it.key().config, it.value());
+      }
+      break;
+    }
+    case feature::ID_SIMPLEPERF_FILE2: {
+      RETURN_IF_ERROR(feature::ParseSimpleperfFile2(
+          std::move(data), [&](TraceBlobView) { return util::OkStatus(); }));
+
+      break;
+    }
+    default:
+      context_->storage->IncrementIndexedStats(stats::perf_features_skipped,
+                                               feature_id);
+  }
+
+  return base::OkStatus();
+}
+
 void PerfDataTokenizer::NotifyEndOfFile() {}
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.h b/src/trace_processor/importers/perf/perf_data_tokenizer.h
index 7a54088..61c1308 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.h
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.h
@@ -18,45 +18,30 @@
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_TOKENIZER_H_
 
 #include <stdint.h>
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
-#include "src/trace_processor/importers/perf/perf_event.h"
-
-#include <limits>
-#include <map>
-#include <string>
+#include <cstdint>
+#include <optional>
 #include <vector>
 
+#include "perfetto/base/flat_set.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/importers/perf/perf_file.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+#include "src/trace_processor/util/file_buffer.h"
 
 namespace perfetto {
 namespace trace_processor {
+class TraceProcessorContext;
+
 namespace perf_importer {
 
-using Section = PerfDataTracker::PerfFileSection;
+struct Record;
 
 class PerfDataTokenizer : public ChunkedTraceReader {
  public:
-  struct PerfHeader {
-    static constexpr char PERF_MAGIC[] = "PERFILE2";
-
-    char magic[8];
-    uint64_t size;
-    // Size of PerfFileAttr struct and section pointing to ids.
-    uint64_t attr_size;
-    Section attrs;
-    Section data;
-    Section event_types;
-    uint64_t flags;
-    uint64_t flags1[3];
-
-    uint64_t num_attrs() const { return attrs.size / attr_size; }
-  };
-
   explicit PerfDataTokenizer(TraceProcessorContext*);
   ~PerfDataTokenizer() override;
   PerfDataTokenizer(const PerfDataTokenizer&) = delete;
@@ -68,39 +53,46 @@
 
  private:
   enum class ParsingState {
-    Header = 0,
-    AfterHeaderBuffer = 1,
-    Attrs = 2,
-    AttrIds = 3,
-    AttrIdsFromBuffer = 4,
-    Records = 5
+    kParseHeader,
+    kParseAttrs,
+    kSeekRecords,
+    kParseRecords,
+    kParseFeatureSections,
+    kParseFeatures,
+    kDone,
   };
-  enum class ParsingResult { NoSpace = 0, Success = 1 };
+  enum class ParsingResult { kMoreDataNeeded = 0, kSuccess = 1 };
 
   base::StatusOr<ParsingResult> ParseHeader();
-  base::StatusOr<ParsingResult> ParseAfterHeaderBuffer();
   base::StatusOr<ParsingResult> ParseAttrs();
-  base::StatusOr<ParsingResult> ParseAttrIds();
-  base::StatusOr<ParsingResult> ParseAttrIdsFromBuffer();
+  base::StatusOr<ParsingResult> SeekRecords();
+  base::StatusOr<ParsingResult> ParseRecords();
+  base::StatusOr<ParsingResult> ParseFeatureSections();
+  base::StatusOr<ParsingResult> ParseFeatures();
 
-  base::StatusOr<PerfDataTracker::Mmap2Record> ParseMmap2Record(
-      uint64_t record_size);
+  base::StatusOr<PerfDataTokenizer::ParsingResult> ParseRecord(Record& record);
+  bool PushRecord(Record record);
+  base::Status ParseFeature(uint8_t feature_id, TraceBlobView payload);
 
-  bool ValidateSample(const PerfDataTracker::PerfSample&);
+  base::StatusOr<int64_t> ToTraceTimestamp(std::optional<uint64_t> time);
 
   TraceProcessorContext* context_;
-  PerfDataTracker* tracker_;
 
-  ParsingState parsing_state_ = ParsingState::Header;
+  ParsingState parsing_state_ = ParsingState::kParseHeader;
 
-  PerfHeader header_;
+  PerfFile::Header header_;
+  base::FlatSet<uint8_t> feature_ids_;
+  PerfFile::Section feature_headers_section_;
+  // Sections for the features present in the perf file sorted by descending
+  // section offset. This is done so that we can pop from the back as we process
+  // the sections.
+  std::vector<std::pair<uint8_t, PerfFile::Section>> feature_sections_;
 
-  std::vector<PerfDataTracker::PerfFileAttr> attrs_;
-  uint64_t ids_start_ = std::numeric_limits<uint64_t>::max();
-  uint64_t ids_end_ = 0;
-  std::vector<uint8_t> after_header_buffer_;
+  RefPtr<PerfSession> perf_session_;
 
-  perf_importer::PerfDataReader reader_;
+  util::FileBuffer buffer_;
+
+  int64_t latest_timestamp_ = 0;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_data_tracker.cc b/src/trace_processor/importers/perf/perf_data_tracker.cc
deleted file mode 100644
index 8b3bc47..0000000
--- a/src/trace_processor/importers/perf/perf_data_tracker.cc
+++ /dev/null
@@ -1,182 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
-
-#include <optional>
-
-#include "perfetto/base/status.h"
-#include "src/trace_processor/importers/common/address_range.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
-#include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/storage/stats.h"
-#include "src/trace_processor/storage/trace_storage.h"
-
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-namespace {
-
-bool IsInKernel(protos::pbzero::Profiling::CpuMode cpu_mode) {
-  switch (cpu_mode) {
-    case protos::pbzero::Profiling::MODE_UNKNOWN:
-      PERFETTO_CHECK(false);
-    case protos::pbzero::Profiling::MODE_GUEST_KERNEL:
-    case protos::pbzero::Profiling::MODE_KERNEL:
-      return true;
-    case protos::pbzero::Profiling::MODE_USER:
-    case protos::pbzero::Profiling::MODE_HYPERVISOR:
-    case protos::pbzero::Profiling::MODE_GUEST_USER:
-      return false;
-  }
-  PERFETTO_CHECK(false);
-}
-
-CreateMappingParams BuildCreateMappingParams(
-    PerfDataTracker::Mmap2Record record) {
-  return {AddressRange::FromStartAndSize(record.num.addr, record.num.len),
-          record.num.pgoff,
-          // start_offset: This is the offset into the file where the ELF header
-          // starts. We assume all file mappings are ELF files an thus this
-          // offset is 0.
-          0,
-          // load_bias: This can only be read out of the actual ELF file, which
-          // we do not have here, so we set it to 0. When symbolizing we will
-          // hopefully have the real load bias and we can compensate there for a
-          // possible mismatch.
-          0, record.filename, std::nullopt};
-}
-}  // namespace
-
-PerfDataTracker::~PerfDataTracker() = default;
-
-uint64_t PerfDataTracker::ComputeCommonSampleType() {
-  if (attrs_.empty()) {
-    return 0;
-  }
-  common_sample_type_ = std::numeric_limits<uint64_t>::max();
-  for (const auto& a : attrs_) {
-    common_sample_type_ &= a.attr.sample_type;
-  }
-  return common_sample_type_;
-}
-
-const perf_event_attr* PerfDataTracker::FindAttrWithId(uint64_t id) const {
-  for (const auto& attr_and_ids : attrs_) {
-    if (auto x =
-            std::find(attr_and_ids.ids.begin(), attr_and_ids.ids.end(), id);
-        x == attr_and_ids.ids.end()) {
-      continue;
-    }
-    return &attr_and_ids.attr;
-  }
-  return nullptr;
-}
-
-void PerfDataTracker::PushMmap2Record(Mmap2Record record) {
-  if (IsInKernel(record.cpu_mode)) {
-    context_->mapping_tracker->CreateKernelMemoryMapping(
-        BuildCreateMappingParams(std::move(record)));
-  } else {
-    UniquePid upid =
-        context_->process_tracker->GetOrCreateProcess(record.num.pid);
-    context_->mapping_tracker->CreateUserMemoryMapping(
-        upid, BuildCreateMappingParams(std::move(record)));
-  }
-}
-
-base::StatusOr<PerfDataTracker::PerfSample> PerfDataTracker::ParseSample(
-    perfetto::trace_processor::perf_importer::PerfDataReader& reader) {
-  uint64_t sample_type = common_sample_type();
-  PerfDataTracker::PerfSample sample;
-
-  if (sample_type & PERF_SAMPLE_IDENTIFIER) {
-    reader.ReadOptional(sample.id);
-    if (auto attr = FindAttrWithId(*sample.id); attr) {
-      sample_type = attr->sample_type;
-    } else {
-      return base::ErrStatus("No attr for sample_id");
-    }
-  }
-
-  if (sample_type & PERF_SAMPLE_IP) {
-    reader.Skip<uint64_t>();
-  }
-
-  if (sample_type & PERF_SAMPLE_TID) {
-    reader.ReadOptional(sample.pid);
-    reader.ReadOptional(sample.tid);
-  }
-
-  if (sample_type & PERF_SAMPLE_TIME) {
-    reader.ReadOptional(sample.ts);
-  }
-
-  // Ignored. Checked because we need to access later parts of sample.
-  if (sample_type & PERF_SAMPLE_ADDR) {
-    reader.Skip<uint64_t>();
-  }
-
-  // The same value as PERF_SAMPLE_IDENTIFIER, so should be ignored.
-  if (sample_type & PERF_SAMPLE_ID) {
-    reader.Skip<uint64_t>();
-  }
-
-  // Ignored. Checked because we need to access later parts of sample.
-  if (sample_type & PERF_SAMPLE_STREAM_ID) {
-    reader.Skip<uint64_t>();
-  }
-
-  if (sample_type & PERF_SAMPLE_CPU) {
-    reader.ReadOptional(sample.cpu);
-    // Ignore next uint32_t res.
-    reader.Skip<uint32_t>();
-  }
-
-  // Ignored. Checked because we need to access later parts of sample.
-  if (sample_type & PERF_SAMPLE_PERIOD) {
-    reader.Skip<uint64_t>();
-  }
-
-  // Ignored.
-  // TODO(mayzner): Implement.
-  if (sample_type & PERF_SAMPLE_READ) {
-    context_->storage->IncrementStats(stats::perf_samples_skipped);
-    return base::ErrStatus("PERF_SAMPLE_READ is not supported");
-  }
-
-  if (sample_type & PERF_SAMPLE_CALLCHAIN) {
-    uint64_t vec_size;
-    reader.Read(vec_size);
-
-    sample.callchain.resize(static_cast<size_t>(vec_size));
-    reader.ReadVector(sample.callchain);
-  }
-
-  return sample;
-}
-
-PerfDataTracker* PerfDataTracker::GetOrCreate(TraceProcessorContext* context) {
-  if (!context->perf_data_tracker) {
-    context->perf_data_tracker.reset(new PerfDataTracker(context));
-  }
-  return static_cast<PerfDataTracker*>(context->perf_data_tracker.get());
-}
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/perf_data_tracker.h b/src/trace_processor/importers/perf/perf_data_tracker.h
deleted file mode 100644
index c96b08d..0000000
--- a/src/trace_processor/importers/perf/perf_data_tracker.h
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_TRACKER_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_TRACKER_H_
-
-#include <cstdint>
-#include <string>
-#include <vector>
-#include "perfetto/base/logging.h"
-#include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
-#include "perfetto/ext/base/status_or.h"
-#include "perfetto/ext/base/string_utils.h"
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-#include "src/trace_processor/importers/perf/perf_data_reader.h"
-#include "src/trace_processor/importers/perf/perf_event.h"
-#include "src/trace_processor/storage/trace_storage.h"
-#include "src/trace_processor/tables/profiler_tables_py.h"
-#include "src/trace_processor/types/destructible.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-
-using MappingTable = tables::StackProfileMappingTable;
-
-class PerfDataTracker : public Destructible {
- public:
-  struct PerfFileSection {
-    uint64_t offset;
-    uint64_t size;
-
-    uint64_t end() const { return offset + size; }
-  };
-  struct PerfFileAttr {
-    perf_event_attr attr;
-    PerfFileSection ids;
-  };
-  struct AttrAndIds {
-    perf_event_attr attr;
-    std::vector<uint64_t> ids;
-  };
-  struct PerfSample {
-    std::optional<uint64_t> id = 0;
-    std::optional<uint32_t> pid = 0;
-    std::optional<uint32_t> tid = 0;
-    std::optional<uint64_t> ts = 0;
-    std::optional<uint32_t> cpu = 0;
-    std::vector<uint64_t> callchain;
-  };
-  struct Mmap2Record {
-    struct Numeric {
-      uint32_t pid;
-      uint32_t tid;
-      uint64_t addr;
-      uint64_t len;
-      uint64_t pgoff;
-      uint32_t maj;
-      uint32_t min;
-      uint64_t ino;
-      uint64_t ino_generation;
-      uint32_t prot;
-      uint32_t flags;
-    };
-    protos::pbzero::Profiling::CpuMode cpu_mode;
-    Numeric num;
-    std::string filename;
-  };
-
-  PerfDataTracker(const PerfDataTracker&) = delete;
-  PerfDataTracker& operator=(const PerfDataTracker&) = delete;
-  explicit PerfDataTracker(TraceProcessorContext* context)
-      : context_(context) {}
-  ~PerfDataTracker() override;
-  static PerfDataTracker* GetOrCreate(TraceProcessorContext* context);
-
-  uint64_t ComputeCommonSampleType();
-
-  void PushAttrAndIds(AttrAndIds data) { attrs_.push_back(std::move(data)); }
-
-  void PushMmap2Record(Mmap2Record record);
-
-  uint64_t common_sample_type() { return common_sample_type_; }
-
-  base::StatusOr<PerfSample> ParseSample(
-      perfetto::trace_processor::perf_importer::PerfDataReader&);
-
- private:
-  const perf_event_attr* FindAttrWithId(uint64_t id) const;
-  TraceProcessorContext* context_;
-  std::vector<AttrAndIds> attrs_;
-
-  uint64_t common_sample_type_;
-};
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_DATA_TRACKER_H_
diff --git a/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc b/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc
deleted file mode 100644
index 923473f..0000000
--- a/src/trace_processor/importers/perf/perf_data_tracker_unittest.cc
+++ /dev/null
@@ -1,243 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#include "src/trace_processor/importers/perf/perf_data_tracker.h"
-
-#include <stddef.h>
-#include <cstring>
-#include <memory>
-#include <vector>
-
-#include "perfetto/base/build_config.h"
-#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
-#include "src/trace_processor/importers/common/address_range.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
-#include "src/trace_processor/importers/common/process_tracker.h"
-#include "src/trace_processor/importers/common/stack_profile_tracker.h"
-#include "src/trace_processor/importers/perf/perf_event.h"
-#include "test/gtest_and_gmock.h"
-
-namespace perfetto {
-namespace trace_processor {
-namespace perf_importer {
-namespace {
-
-class PerfDataTrackerUnittest : public testing::Test {
- public:
-  PerfDataTrackerUnittest() {
-    context_.storage = std::make_unique<TraceStorage>();
-    context_.process_tracker = std::make_unique<ProcessTracker>(&context_);
-    context_.stack_profile_tracker =
-        std::make_unique<StackProfileTracker>(&context_);
-    context_.mapping_tracker = std::make_unique<MappingTracker>(&context_);
-  }
-
- protected:
-  TraceProcessorContext context_;
-};
-
-TEST_F(PerfDataTrackerUnittest, ComputeCommonSampleType) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::AttrAndIds attr_and_ids;
-  attr_and_ids.attr.sample_type =
-      PERF_SAMPLE_CALLCHAIN | PERF_SAMPLE_CPU | PERF_SAMPLE_TIME;
-  tracker->PushAttrAndIds(attr_and_ids);
-
-  attr_and_ids.attr.sample_type = PERF_SAMPLE_ADDR | PERF_SAMPLE_CPU;
-  tracker->PushAttrAndIds(attr_and_ids);
-
-  tracker->ComputeCommonSampleType();
-  EXPECT_TRUE(tracker->common_sample_type() & PERF_SAMPLE_CPU);
-  EXPECT_FALSE(tracker->common_sample_type() & PERF_SAMPLE_CALLCHAIN);
-}
-
-TEST_F(PerfDataTrackerUnittest, FindMapping) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::Mmap2Record rec;
-  rec.cpu_mode = protos::pbzero::Profiling::MODE_USER;
-  rec.filename = "file1";
-  rec.num.addr = 1000;
-  rec.num.len = 100;
-  rec.num.pid = 1;
-  rec.cpu_mode = protos::pbzero::Profiling::MODE_USER;
-  tracker->PushMmap2Record(rec);
-
-  rec.num.addr = 2000;
-  tracker->PushMmap2Record(rec);
-
-  rec.num.addr = 3000;
-  tracker->PushMmap2Record(rec);
-
-  UserMemoryMapping* mapping =
-      context_.mapping_tracker->FindUserMappingForAddress(
-          context_.process_tracker->GetOrCreateProcess(1), 2050);
-  ASSERT_NE(mapping, nullptr);
-  EXPECT_EQ(mapping->memory_range().start(), 2000u);
-  EXPECT_EQ(mapping->memory_range().end(), 2100u);
-}
-
-TEST_F(PerfDataTrackerUnittest, FindMappingFalse) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::Mmap2Record rec;
-  rec.cpu_mode = protos::pbzero::Profiling::MODE_USER;
-  rec.filename = "file1";
-  rec.num.addr = 1000;
-  rec.num.len = 100;
-  rec.num.pid = 1;
-  rec.cpu_mode = protos::pbzero::Profiling::MODE_USER;
-  tracker->PushMmap2Record(rec);
-
-  UserMemoryMapping* mapping =
-      context_.mapping_tracker->FindUserMappingForAddress(
-          context_.process_tracker->GetOrCreateProcess(2), 2050);
-  EXPECT_EQ(mapping, nullptr);
-}
-
-TEST_F(PerfDataTrackerUnittest, ParseSampleTrivial) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::AttrAndIds attr_and_ids;
-  attr_and_ids.attr.sample_type = PERF_SAMPLE_TIME;
-  tracker->PushAttrAndIds(attr_and_ids);
-  tracker->ComputeCommonSampleType();
-
-  uint64_t ts = 100;
-
-  TraceBlob blob =
-      TraceBlob::CopyFrom(static_cast<const void*>(&ts), sizeof(uint64_t));
-  PerfDataReader reader(TraceBlobView(std::move(blob)));
-
-  auto parsed_sample = tracker->ParseSample(reader);
-  EXPECT_TRUE(parsed_sample.ok());
-  EXPECT_EQ(parsed_sample->ts, 100u);
-}
-
-TEST_F(PerfDataTrackerUnittest, ParseSampleCallchain) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::AttrAndIds attr_and_ids;
-  attr_and_ids.attr.sample_type = PERF_SAMPLE_CALLCHAIN;
-  tracker->PushAttrAndIds(attr_and_ids);
-  tracker->ComputeCommonSampleType();
-
-  struct Sample {
-    uint64_t callchain_size;         /* if PERF_SAMPLE_CALLCHAIN */
-    std::vector<uint64_t> callchain; /* if PERF_SAMPLE_CALLCHAIN */
-  };
-
-  Sample sample;
-  sample.callchain_size = 3;
-  sample.callchain = std::vector<uint64_t>{1, 2, 3};
-
-  TraceBlob blob = TraceBlob::Allocate(4 * sizeof(uint64_t));
-  memcpy(blob.data(), &sample.callchain_size, sizeof(uint64_t));
-  memcpy(blob.data() + sizeof(uint64_t), sample.callchain.data(),
-         sizeof(uint64_t) * 3);
-  PerfDataReader reader(TraceBlobView(std::move(blob)));
-
-  auto parsed_sample = tracker->ParseSample(reader);
-  EXPECT_TRUE(parsed_sample.ok());
-  EXPECT_EQ(parsed_sample->callchain.size(), 3u);
-}
-
-TEST_F(PerfDataTrackerUnittest, ParseSampleWithoutId) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::AttrAndIds attr_and_ids;
-  attr_and_ids.attr.sample_type = PERF_SAMPLE_TID | PERF_SAMPLE_TIME |
-                                  PERF_SAMPLE_CPU | PERF_SAMPLE_CALLCHAIN;
-  tracker->PushAttrAndIds(attr_and_ids);
-  tracker->ComputeCommonSampleType();
-
-  struct Sample {
-    uint32_t pid;            /* if PERF_SAMPLE_TID */
-    uint32_t tid;            /* if PERF_SAMPLE_TID */
-    uint64_t ts;             /* if PERF_SAMPLE_TIME */
-    uint32_t cpu;            /* if PERF_SAMPLE_CPU */
-    uint32_t res_ignore;     /* if PERF_SAMPLE_CPU */
-    uint64_t callchain_size; /* if PERF_SAMPLE_CALLCHAIN */
-  };
-
-  Sample sample;
-  sample.pid = 2;
-  sample.ts = 100;
-  sample.cpu = 1;
-  sample.callchain_size = 3;
-  std::vector<uint64_t> callchain{1, 2, 3};
-
-  TraceBlob blob = TraceBlob::Allocate(sizeof(Sample) + sizeof(uint64_t) * 3);
-  memcpy(blob.data(), &sample, sizeof(Sample));
-  memcpy(blob.data() + sizeof(Sample), callchain.data(), sizeof(uint64_t) * 3);
-
-  PerfDataReader reader(TraceBlobView(std::move(blob)));
-  EXPECT_TRUE(reader.CanReadSize(sizeof(Sample)));
-
-  auto parsed_sample = tracker->ParseSample(reader);
-  EXPECT_TRUE(parsed_sample.ok());
-  EXPECT_EQ(parsed_sample->callchain.size(), 3u);
-  EXPECT_EQ(sample.ts, parsed_sample->ts);
-}
-
-TEST_F(PerfDataTrackerUnittest, ParseSampleWithId) {
-  PerfDataTracker* tracker = PerfDataTracker::GetOrCreate(&context_);
-
-  PerfDataTracker::AttrAndIds attr_and_ids;
-  attr_and_ids.attr.sample_type = PERF_SAMPLE_CPU | PERF_SAMPLE_TID |
-                                  PERF_SAMPLE_IDENTIFIER | PERF_SAMPLE_ID |
-                                  PERF_SAMPLE_CALLCHAIN | PERF_SAMPLE_TIME;
-  attr_and_ids.ids.push_back(10);
-  tracker->PushAttrAndIds(attr_and_ids);
-  tracker->ComputeCommonSampleType();
-
-  struct Sample {
-    uint64_t identifier;     /* if PERF_SAMPLE_IDENTIFIER */
-    uint32_t pid;            /* if PERF_SAMPLE_TID */
-    uint32_t tid;            /* if PERF_SAMPLE_TID */
-    uint64_t ts;             /* if PERF_SAMPLE_TIME */
-    uint64_t id;             /* if PERF_SAMPLE_ID */
-    uint32_t cpu;            /* if PERF_SAMPLE_CPU */
-    uint32_t res_ignore;     /* if PERF_SAMPLE_CPU */
-    uint64_t callchain_size; /* if PERF_SAMPLE_CALLCHAIN */
-  };
-
-  Sample sample;
-  sample.id = 10;
-  sample.identifier = 10;
-  sample.cpu = 1;
-  sample.pid = 2;
-  sample.ts = 100;
-  sample.callchain_size = 3;
-  std::vector<uint64_t> callchain{1, 2, 3};
-
-  TraceBlob blob = TraceBlob::Allocate(sizeof(Sample) + sizeof(uint64_t) * 3);
-  memcpy(blob.data(), &sample, sizeof(Sample));
-  memcpy(blob.data() + sizeof(Sample), callchain.data(), sizeof(uint64_t) * 3);
-
-  PerfDataReader reader(TraceBlobView(std::move(blob)));
-
-  auto parsed_sample = tracker->ParseSample(reader);
-  EXPECT_TRUE(parsed_sample.ok());
-  EXPECT_EQ(parsed_sample->callchain.size(), 3u);
-  EXPECT_EQ(100u, parsed_sample->ts);
-}
-
-}  // namespace
-}  // namespace perf_importer
-}  // namespace trace_processor
-}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/perf_event.h b/src/trace_processor/importers/perf/perf_event.h
index 4763e23..58077be 100644
--- a/src/trace_processor/importers/perf/perf_event.h
+++ b/src/trace_processor/importers/perf/perf_event.h
@@ -238,6 +238,7 @@
   PERF_RECORD_MISC_GUEST_USER = 5,
 
   PERF_RECORD_MISC_MMAP_BUILD_ID = 1U << 14,
+  PERF_RECORD_MISC_EXT_RESERVED = 1U << 15,
 };
 
 enum perf_event_read_format {
@@ -250,7 +251,7 @@
   PERF_FORMAT_MAX = 1U << 5, /* non-ABI */
 };
 
-enum perf_callchain_context {
+enum perf_callchain_context : uint64_t {
   PERF_CONTEXT_HV = static_cast<uint64_t>(-32),
   PERF_CONTEXT_KERNEL = static_cast<uint64_t>(-128),
   PERF_CONTEXT_USER = static_cast<uint64_t>(-512),
diff --git a/src/trace_processor/importers/perf/perf_event_attr.cc b/src/trace_processor/importers/perf/perf_event_attr.cc
index 1abf8d0..03fe71f 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.cc
+++ b/src/trace_processor/importers/perf/perf_event_attr.cc
@@ -21,7 +21,11 @@
 #include <cstring>
 #include <optional>
 
+#include "perfetto/ext/base/string_view.h"
+#include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
 
 namespace perfetto::trace_processor::perf_importer {
 
@@ -90,11 +94,43 @@
 }
 }  // namespace
 
-PerfEventAttr::PerfEventAttr(perf_event_attr attr)
-    : attr_(std::move(attr)),
+PerfEventAttr::PerfEventAttr(TraceProcessorContext* context,
+                             uint32_t perf_session_id,
+                             perf_event_attr attr)
+    : context_(context),
+      perf_session_id_(perf_session_id),
+      attr_(std::move(attr)),
       time_offset_from_start_(TimeOffsetFromStartOfSampleRecord(attr_)),
       time_offset_from_end_(TimeOffsetFromEndOfNonSampleRecord(attr_)),
       id_offset_from_start_(IdOffsetFromStartOfSampleRecord(attr_)),
       id_offset_from_end_(IdOffsetFromEndOfNonSampleRecord(attr_)) {}
 
+PerfEventAttr::~PerfEventAttr() = default;
+
+PerfCounter& PerfEventAttr::GetOrCreateCounter(uint32_t cpu) const {
+  auto it = counters_.find(cpu);
+  if (it == counters_.end()) {
+    it = counters_.emplace(cpu, CreateCounter(cpu)).first;
+  }
+  return it->second;
+}
+
+PerfCounter PerfEventAttr::CreateCounter(uint32_t cpu) const {
+  return PerfCounter(
+      context_->storage->mutable_counter_table(),
+      context_->storage->mutable_perf_counter_track_table()
+          ->Insert({/*in_name=*/context_->storage->InternString(
+                        base::StringView(event_name_)),
+                    /*in_parent_id=*/std::nullopt,
+                    /*in_source_arg_set_id=*/std::nullopt,
+                    /*in_machine_id=*/std::nullopt,
+                    /*in_unit=*/
+                    context_->storage->InternString(base::StringView("")),
+                    /*in_description=*/
+                    context_->storage->InternString(base::StringView("")),
+                    /*in_perf_session_id=*/perf_session_id_, /*in_cpu=*/cpu,
+                    /*in_is_timebase=*/is_timebase()})
+          .row_reference);
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_event_attr.h b/src/trace_processor/importers/perf/perf_event_attr.h
index 4f219aa..f1af037 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.h
+++ b/src/trace_processor/importers/perf/perf_event_attr.h
@@ -21,17 +21,27 @@
 #include <cstddef>
 #include <cstdint>
 #include <optional>
+#include <unordered_map>
 
-#include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 
-namespace perfetto::trace_processor::perf_importer {
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+namespace perf_importer {
 
 // Wrapper around a `perf_event_attr` object that add some helper methods.
 class PerfEventAttr : public RefCounted {
  public:
-  explicit PerfEventAttr(perf_event_attr attr);
+  PerfEventAttr(TraceProcessorContext* context,
+                uint32_t perf_session_id_,
+                perf_event_attr attr);
+  ~PerfEventAttr();
+  uint32_t type() const { return attr_.type; }
+  uint64_t config() const { return attr_.config; }
   uint64_t sample_type() const { return attr_.sample_type; }
   uint64_t read_format() const { return attr_.read_format; }
   bool sample_id_all() const { return !!attr_.sample_id_all; }
@@ -48,12 +58,6 @@
     return attr_.freq ? std::make_optional(attr_.sample_freq) : std::nullopt;
   }
 
-  bool is_timebase() const {
-    // This is what simpleperf uses for events that are not supposed to sample
-    // TODO(b/334978369): Determine if there is a better way to figure this out.
-    return attr_.sample_period < (1ull << 62);
-  }
-
   // Offset from the end of a record's payload to the time filed (if present).
   // To be used with non `PERF_RECORD_SAMPLE` records
   std::optional<size_t> time_offset_from_end() const {
@@ -81,14 +85,33 @@
     return id_offset_from_end_;
   }
 
+  void set_event_name(std::string event_name) {
+    event_name_ = std::move(event_name);
+  }
+
+  PerfCounter& GetOrCreateCounter(uint32_t cpu) const;
+
  private:
+  bool is_timebase() const {
+    // This is what simpleperf uses for events that are not supposed to sample
+    // TODO(b/334978369): Determine if there is a better way to figure this out.
+    return attr_.sample_period < (1ull << 62);
+  }
+
+  PerfCounter CreateCounter(uint32_t cpu) const;
+
+  TraceProcessorContext* const context_;
+  uint32_t perf_session_id_;
   perf_event_attr attr_;
   std::optional<size_t> time_offset_from_start_;
   std::optional<size_t> time_offset_from_end_;
   std::optional<size_t> id_offset_from_start_;
   std::optional<size_t> id_offset_from_end_;
+  mutable std::unordered_map<uint32_t, PerfCounter> counters_;
+  std::string event_name_;
 };
 
-}  // namespace perfetto::trace_processor::perf_importer
+}  // namespace perf_importer
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_EVENT_ATTR_H_
diff --git a/src/trace_processor/importers/perf/perf_session.cc b/src/trace_processor/importers/perf/perf_session.cc
index 86d983d..db42ae7 100644
--- a/src/trace_processor/importers/perf/perf_session.cc
+++ b/src/trace_processor/importers/perf/perf_session.cc
@@ -46,11 +46,13 @@
     return base::ErrStatus("No perf_event_attr");
   }
 
-  const PerfEventAttr base_attr(attr_with_ids_[0].attr);
+  const PerfEventAttr base_attr(context_, perf_session_id_,
+                                attr_with_ids_[0].attr);
 
   base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id;
   for (const auto& entry : attr_with_ids_) {
-    RefPtr<PerfEventAttr> attr(new PerfEventAttr(entry.attr));
+    RefPtr<PerfEventAttr> attr(
+        new PerfEventAttr(context_, perf_session_id_, entry.attr));
     if (base_attr.sample_id_all() != attr->sample_id_all()) {
       return base::ErrStatus(
           "perf_event_attr with different sample_id_all values");
@@ -133,4 +135,22 @@
   return RefPtr<const PerfEventAttr>(it->get());
 }
 
+void PerfSession::SetEventName(uint64_t event_id, std::string name) {
+  auto it = attrs_by_id_.Find(event_id);
+  if (!it) {
+    return;
+  }
+  (*it)->set_event_name(std::move(name));
+}
+
+void PerfSession::SetEventName(uint32_t type,
+                               uint64_t config,
+                               const std::string& name) {
+  for (auto it = attrs_by_id_.GetIterator(); it; ++it) {
+    if (it.value()->type() == type && it.value()->config() == config) {
+      it.value()->set_event_name(name);
+    }
+  }
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.h b/src/trace_processor/importers/perf/perf_session.h
index abea41b..c2a9e66 100644
--- a/src/trace_processor/importers/perf/perf_session.h
+++ b/src/trace_processor/importers/perf/perf_session.h
@@ -20,7 +20,6 @@
 #include <sys/types.h>
 #include <cstddef>
 #include <cstdint>
-#include <optional>
 
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/status_or.h"
@@ -29,15 +28,19 @@
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/importers/perf/perf_event_attr.h"
 
-namespace perfetto::trace_processor::perf_importer {
+namespace perfetto::trace_processor {
+
+class TraceProcessorContext;
+
+namespace perf_importer {
 
 // Helper to deal with perf_event_attr instances in a perf file.
 class PerfSession : public RefCounted {
  public:
   class Builder {
    public:
-    explicit Builder(uint32_t perf_session_id)
-        : perf_session_id_(perf_session_id) {}
+    Builder(TraceProcessorContext* context, uint32_t perf_session_id)
+        : context_(context), perf_session_id_(perf_session_id) {}
     base::StatusOr<RefPtr<PerfSession>> Build();
     Builder& AddAttrAndIds(perf_event_attr attr, std::vector<uint64_t> ids) {
       attr_with_ids_.push_back({std::move(attr), std::move(ids)});
@@ -50,6 +53,7 @@
       std::vector<uint64_t> ids;
     };
 
+    TraceProcessorContext* const context_;
     uint32_t perf_session_id_;
     std::vector<PerfEventAttrWithIds> attr_with_ids_;
   };
@@ -62,6 +66,9 @@
       const perf_event_header& header,
       const TraceBlobView& payload) const;
 
+  void SetEventName(uint64_t event_id, std::string name);
+  void SetEventName(uint32_t type, uint64_t config, const std::string& name);
+
  private:
   PerfSession(uint32_t perf_session_id,
               base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id,
@@ -83,6 +90,7 @@
   bool has_single_perf_event_attr_;
 };
 
-}  // namespace perfetto::trace_processor::perf_importer
+}  // namespace perf_importer
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_PERF_SESSION_H_
diff --git a/src/trace_processor/importers/perf/perf_session_unittest.cc b/src/trace_processor/importers/perf/perf_session_unittest.cc
index 0ab85f2..fd6bfa3 100644
--- a/src/trace_processor/importers/perf/perf_session_unittest.cc
+++ b/src/trace_processor/importers/perf/perf_session_unittest.cc
@@ -41,12 +41,12 @@
 }
 
 TEST(PerfSessionTest, NoAttrBuildFails) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   EXPECT_FALSE(builder.Build().ok());
 }
 
 TEST(PerfSessionTest, OneAttrAndNoIdBuildSucceeds) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = false;
   attr.sample_type = PERF_SAMPLE_CALLCHAIN | PERF_SAMPLE_CPU | PERF_SAMPLE_TIME;
@@ -61,7 +61,7 @@
 }
 
 TEST(PerfSessionTest, MultipleAttrsAndNoIdBuildFails) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_CALLCHAIN | PERF_SAMPLE_CPU | PERF_SAMPLE_TIME;
@@ -71,7 +71,7 @@
 }
 
 TEST(PerfSessionTest, MultipleIdsSameAttrAndNoIdCanExtractAttrFromRecord) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_CPU | PERF_SAMPLE_TIME;
@@ -95,7 +95,7 @@
 }
 
 TEST(PerfSessionTest, NoCommonSampleIdAllBuildFails) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IDENTIFIER;
@@ -111,7 +111,7 @@
 }
 
 TEST(PerfSessionTest, NoCommonOffsetForSampleBuildFails) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_ID;
@@ -122,7 +122,7 @@
 }
 
 TEST(PerfSessionTest, NoCommonOffsetForNonSampleBuildFails) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_ID | PERF_SAMPLE_TID;
@@ -138,7 +138,7 @@
 }
 
 TEST(PerfSessionTest, NoCommonOffsetForNonSampleAndNoSampleIdAllBuildSucceeds) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = false;
   attr.sample_type = PERF_SAMPLE_IDENTIFIER | PERF_SAMPLE_TID;
@@ -149,7 +149,7 @@
 }
 
 TEST(PerfSessionTest, MultiplesessionBuildSucceeds) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_ID;
@@ -159,7 +159,7 @@
 }
 
 TEST(PerfSessionTest, FindAttrInRecordWithId) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IP | PERF_SAMPLE_ID;
@@ -194,7 +194,7 @@
 }
 
 TEST(PerfSessionTest, FindAttrInRecordWithIdentifier) {
-  PerfSession::Builder builder(0);
+  PerfSession::Builder builder(nullptr, 0);
   perf_event_attr attr;
   attr.sample_id_all = true;
   attr.sample_type = PERF_SAMPLE_IDENTIFIER | PERF_SAMPLE_IP;
diff --git a/src/trace_processor/importers/perf/reader.h b/src/trace_processor/importers/perf/reader.h
index faf31a2..91a4253 100644
--- a/src/trace_processor/importers/perf/reader.h
+++ b/src/trace_processor/importers/perf/reader.h
@@ -26,7 +26,9 @@
 #include <type_traits>
 #include <vector>
 
+#include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
 
 namespace perfetto::trace_processor::perf_importer {
 
@@ -45,6 +47,49 @@
   // methods are called.
   size_t size_left() const { return static_cast<size_t>(end_ - current_); }
 
+  bool ReadStringView(base::StringView& str, size_t size) {
+    if (size_left() < size) {
+      return false;
+    }
+    str = base::StringView(reinterpret_cast<const char*>(current_), size);
+    current_ += size;
+    return true;
+  }
+
+  bool ReadPerfEventAttr(perf_event_attr& attr, size_t attr_size) {
+    const size_t bytes_to_read = std::min(attr_size, sizeof(attr));
+    const size_t bytes_to_skip = attr_size - bytes_to_read;
+    static_assert(std::has_unique_object_representations_v<perf_event_attr>);
+
+    if (size_left() < bytes_to_read + bytes_to_skip) {
+      return false;
+    }
+
+    memset(&attr, 0, sizeof(attr));
+
+    return Read(&attr, bytes_to_read) && Skip(bytes_to_skip);
+  }
+
+  bool ReadBlob(TraceBlobView& blob, uint32_t size) {
+    if (size_left() < size) {
+      return false;
+    }
+    blob = TraceBlobView(buffer_, static_cast<size_t>(end_ - current_), size);
+    current_ += size;
+    return true;
+  }
+
+  bool ReadStringUntilEndOrNull(std::string& out) {
+    const uint8_t* ptr = current_;
+    while (ptr != end_ && *ptr != 0) {
+      ++ptr;
+    }
+    out = std::string(reinterpret_cast<const char*>(current_),
+                      static_cast<size_t>(ptr - current_));
+    current_ = ptr;
+    return true;
+  }
+
   template <typename T>
   bool Read(T& obj) {
     static_assert(std::has_unique_object_representations_v<T>);
diff --git a/src/trace_processor/importers/perf/record_parser.cc b/src/trace_processor/importers/perf/record_parser.cc
new file mode 100644
index 0000000..7371adf
--- /dev/null
+++ b/src/trace_processor/importers/perf/record_parser.cc
@@ -0,0 +1,310 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/record_parser.h"
+
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/public/compiler.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/common/mapping_tracker.h"
+#include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/importers/perf/perf_counter.h"
+#include "src/trace_processor/importers/perf/perf_event.h"
+#include "src/trace_processor/importers/perf/perf_event_attr.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/sample.h"
+#include "src/trace_processor/importers/proto/perf_sample_tracker.h"
+#include "src/trace_processor/importers/proto/profile_packet_utils.h"
+#include "src/trace_processor/storage/stats.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/tables/profiler_tables_py.h"
+#include "src/trace_processor/util/build_id.h"
+#include "src/trace_processor/util/status_macros.h"
+
+namespace perfetto {
+namespace trace_processor {
+namespace perf_importer {
+namespace {
+
+CreateMappingParams BuildCreateMappingParams(
+    const CommonMmapRecordFields& fields,
+    std::string filename,
+    std::optional<BuildId> build_id = std::nullopt) {
+  return {AddressRange::FromStartAndSize(fields.addr, fields.len), fields.pgoff,
+          // start_offset: This is the offset into the file where the ELF header
+          // starts. We assume all file mappings are ELF files an thus this
+          // offset is 0.
+          0,
+          // load_bias: This can only be read out of the actual ELF file, which
+          // we do not have here, so we set it to 0. When symbolizing we will
+          // hopefully have the real load bias and we can compensate there for a
+          // possible mismatch.
+          0, std::move(filename), std::move(build_id)};
+}
+
+bool IsInKernel(protos::pbzero::Profiling::CpuMode cpu_mode) {
+  switch (cpu_mode) {
+    case protos::pbzero::Profiling::MODE_UNKNOWN:
+      PERFETTO_FATAL("Unknown CPU mode");
+    case protos::pbzero::Profiling::MODE_GUEST_KERNEL:
+    case protos::pbzero::Profiling::MODE_KERNEL:
+      return true;
+    case protos::pbzero::Profiling::MODE_USER:
+    case protos::pbzero::Profiling::MODE_HYPERVISOR:
+    case protos::pbzero::Profiling::MODE_GUEST_USER:
+      return false;
+  }
+  PERFETTO_FATAL("For GCC.");
+}
+
+}  // namespace
+
+using FramesTable = tables::StackProfileFrameTable;
+using CallsitesTable = tables::StackProfileCallsiteTable;
+
+RecordParser::RecordParser(TraceProcessorContext* context)
+    : context_(context) {}
+
+RecordParser::~RecordParser() = default;
+
+void RecordParser::ParsePerfRecord(int64_t ts, Record record) {
+  if (base::Status status = ParseRecord(ts, std::move(record)); !status.ok()) {
+    context_->storage->IncrementStats(record.header.type == PERF_RECORD_SAMPLE
+                                          ? stats::perf_samples_skipped
+                                          : stats::perf_record_skipped);
+  }
+}
+
+base::Status RecordParser::ParseRecord(int64_t ts, Record record) {
+  switch (record.header.type) {
+    case PERF_RECORD_COMM:
+      return ParseComm(std::move(record));
+
+    case PERF_RECORD_SAMPLE:
+      return ParseSample(ts, std::move(record));
+
+    case PERF_RECORD_MMAP:
+      return ParseMmap(std::move(record));
+
+    case PERF_RECORD_MMAP2:
+      return ParseMmap2(std::move(record));
+
+    case PERF_RECORD_AUX:
+    case PERF_RECORD_AUXTRACE:
+    case PERF_RECORD_AUXTRACE_INFO:
+      // These should be dealt with at tokenization time
+      PERFETTO_FATAL("Unexpected record type at parsing time: %" PRIu32,
+                     record.header.type);
+
+    default:
+      context_->storage->IncrementIndexedStats(
+          stats::perf_unknown_record_type,
+          static_cast<int>(record.header.type));
+      return base::ErrStatus("Unknown PERF_RECORD with type %" PRIu32,
+                             record.header.type);
+  }
+}
+
+base::Status RecordParser::ParseSample(int64_t ts, Record record) {
+  Sample sample;
+  RETURN_IF_ERROR(sample.Parse(ts, record));
+
+  if (!sample.period.has_value() && record.attr != nullptr) {
+    sample.period = record.attr->sample_period();
+  }
+
+  return InternSample(std::move(sample));
+}
+
+base::Status RecordParser::InternSample(Sample sample) {
+  if (!sample.time.has_value()) {
+    // We do not really use this TS as this is using the perf clock, but we need
+    // it to be present so that we can compute the trace_ts done during
+    // tokenization. (Actually at tokenization time we do estimate a trace_ts if
+    // no perf ts is present, but for samples we want this to be as accurate as
+    // possible)
+    base::ErrStatus("Can not parse samples with no PERF_SAMPLE_TIME field");
+  }
+
+  if (!sample.pid_tid.has_value()) {
+    base::ErrStatus("Can not parse samples with no PERF_SAMPLE_TID field");
+  }
+
+  if (!sample.cpu.has_value()) {
+    base::ErrStatus("Can not parse samples with no PERF_SAMPLE_CPU field");
+  }
+
+  UniqueTid utid = context_->process_tracker->UpdateThread(sample.pid_tid->tid,
+                                                           sample.pid_tid->pid);
+  const auto upid = *context_->storage->thread_table()
+                         .FindById(tables::ThreadTable::Id(utid))
+                         ->upid();
+
+  if (sample.callchain.empty() && sample.ip.has_value()) {
+    sample.callchain.push_back(Sample::Frame{sample.cpu_mode, *sample.ip});
+  }
+  std::optional<CallsiteId> callsite_id =
+      InternCallchain(upid, sample.callchain);
+
+  context_->storage->mutable_perf_sample_table()->Insert(
+      {sample.trace_ts, utid, *sample.cpu,
+       context_->storage->InternString(
+           ProfilePacketUtils::StringifyCpuMode(sample.cpu_mode)),
+       callsite_id, std::nullopt, sample.perf_session->perf_session_id()});
+
+  return UpdateCounters(sample);
+}
+
+std::optional<CallsiteId> RecordParser::InternCallchain(
+    UniquePid upid,
+    const std::vector<Sample::Frame>& callchain) {
+  if (callchain.empty()) {
+    return std::nullopt;
+  }
+
+  auto& stack_profile_tracker = *context_->stack_profile_tracker;
+  auto& mapping_tracker = *context_->mapping_tracker;
+
+  std::optional<CallsiteId> parent;
+  uint32_t depth = 0;
+  for (auto it = callchain.rbegin(); it != callchain.rend(); ++it) {
+    VirtualMemoryMapping* mapping;
+    if (IsInKernel(it->cpu_mode)) {
+      mapping = mapping_tracker.FindKernelMappingForAddress(it->ip);
+    } else {
+      mapping = mapping_tracker.FindUserMappingForAddress(upid, it->ip);
+    }
+
+    if (!mapping) {
+      context_->storage->IncrementStats(stats::perf_dummy_mapping_used);
+      // Simpleperf will not create mappings for anonymous executable mappings
+      // which are used by JITted code (e.g. V8 JavaScript).
+      mapping = mapping_tracker.GetDummyMapping();
+    }
+
+    const FrameId frame_id =
+        mapping->InternFrame(mapping->ToRelativePc(it->ip), "");
+
+    parent = stack_profile_tracker.InternCallsite(parent, frame_id, depth);
+    depth++;
+  }
+  return parent;
+}
+
+base::Status RecordParser::ParseComm(Record record) {
+  Reader reader(record.payload.copy());
+  uint32_t pid;
+  uint32_t tid;
+  std::string comm;
+  if (!reader.Read(pid) || !reader.Read(tid) || !reader.ReadCString(comm)) {
+    return base::ErrStatus("Failed to parse PERF_RECORD_COMM");
+  }
+
+  context_->process_tracker->UpdateThread(tid, pid);
+  context_->process_tracker->UpdateThreadName(
+      tid, context_->storage->InternString(base::StringView(comm)),
+      ThreadNamePriority::kFtrace);
+
+  return base::OkStatus();
+}
+
+base::Status RecordParser::ParseMmap(Record record) {
+  MmapRecord mmap;
+  RETURN_IF_ERROR(mmap.Parse(record));
+  if (IsInKernel(record.GetCpuMode())) {
+    context_->mapping_tracker->CreateKernelMemoryMapping(
+        BuildCreateMappingParams(mmap, std::move(mmap.filename)));
+    return base::OkStatus();
+  }
+
+  context_->mapping_tracker->CreateUserMemoryMapping(
+      GetUpid(mmap), BuildCreateMappingParams(mmap, std::move(mmap.filename)));
+
+  return base::OkStatus();
+}
+
+util::Status RecordParser::ParseMmap2(Record record) {
+  Mmap2Record mmap2;
+  RETURN_IF_ERROR(mmap2.Parse(record));
+  if (IsInKernel(record.GetCpuMode())) {
+    context_->mapping_tracker->CreateKernelMemoryMapping(
+        BuildCreateMappingParams(mmap2, std::move(mmap2.filename)));
+    return base::OkStatus();
+  }
+
+  context_->mapping_tracker->CreateUserMemoryMapping(
+      GetUpid(mmap2), BuildCreateMappingParams(mmap2, std::move(mmap2.filename),
+                                               mmap2.GetBuildId()));
+
+  return base::OkStatus();
+}
+
+UniquePid RecordParser::GetUpid(const CommonMmapRecordFields& fields) const {
+  UniqueTid utid =
+      context_->process_tracker->UpdateThread(fields.tid, fields.pid);
+  auto upid = context_->storage->thread_table()
+                  .FindById(tables::ThreadTable::Id(utid))
+                  ->upid();
+  PERFETTO_CHECK(upid.has_value());
+  return *upid;
+}
+
+base::Status RecordParser::UpdateCounters(const Sample& sample) {
+  if (!sample.read_groups.empty()) {
+    return UpdateCountersInReadGroups(sample);
+  }
+
+  if (!sample.period.has_value() && !sample.attr->sample_period().has_value()) {
+    return base::ErrStatus("No period for sample");
+  }
+
+  uint64_t period = sample.period.has_value() ? *sample.period
+                                              : *sample.attr->sample_period();
+  sample.attr->GetOrCreateCounter(*sample.cpu)
+      .AddDelta(sample.trace_ts, static_cast<double>(period));
+  return base::OkStatus();
+}
+
+base::Status RecordParser::UpdateCountersInReadGroups(const Sample& sample) {
+  if (!sample.cpu.has_value()) {
+    return base::ErrStatus("No cpu for sample");
+  }
+
+  for (const auto& entry : sample.read_groups) {
+    RefPtr<const PerfEventAttr> attr =
+        sample.perf_session->FindAttrForEventId(*entry.event_id);
+    if (PERFETTO_UNLIKELY(!attr)) {
+      return base::ErrStatus("No perf_event_attr for id %" PRIu64,
+                             *entry.event_id);
+    }
+    attr->GetOrCreateCounter(*sample.cpu)
+        .AddCount(sample.trace_ts, static_cast<double>(entry.value));
+  }
+  return base::OkStatus();
+}
+
+}  // namespace perf_importer
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/perf/record_parser.h b/src/trace_processor/importers/perf/record_parser.h
new file mode 100644
index 0000000..0a71b49
--- /dev/null
+++ b/src/trace_processor/importers/perf/record_parser.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_RECORD_PARSER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_RECORD_PARSER_H_
+
+#include <stdint.h>
+#include <cstdint>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/importers/perf/mmap_record.h"
+#include "src/trace_processor/importers/perf/record.h"
+#include "src/trace_processor/importers/perf/sample.h"
+#include "src/trace_processor/storage/trace_storage.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TraceProcessorContext;
+
+namespace perf_importer {
+
+class PerfDataTracker;
+class Reader;
+
+// Parses samples from perf.data files.
+class RecordParser : public PerfRecordParser {
+ public:
+  explicit RecordParser(TraceProcessorContext*);
+  ~RecordParser() override;
+
+  void ParsePerfRecord(int64_t timestamp, Record record) override;
+
+ private:
+  base::Status ParseRecord(int64_t timestamp, Record record);
+  base::Status ParseSample(int64_t ts, Record record);
+  base::Status ParseComm(Record record);
+  base::Status ParseMmap(Record record);
+  base::Status ParseMmap2(Record record);
+
+  base::Status InternSample(Sample sample);
+
+  base::Status UpdateCounters(const Sample& sample);
+  base::Status UpdateCountersInReadGroups(const Sample& sample);
+
+  std::optional<CallsiteId> InternCallchain(
+      UniquePid upid,
+      const std::vector<Sample::Frame>& callchain);
+
+  UniquePid GetUpid(const CommonMmapRecordFields& fields) const;
+
+  TraceProcessorContext* context_ = nullptr;
+};
+
+}  // namespace perf_importer
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_RECORD_PARSER_H_
diff --git a/src/trace_processor/importers/perf/sample.cc b/src/trace_processor/importers/perf/sample.cc
new file mode 100644
index 0000000..63ded53
--- /dev/null
+++ b/src/trace_processor/importers/perf/sample.cc
@@ -0,0 +1,252 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/perf/sample.h"
+
+#include <cstdint>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/public/compiler.h"
+#include "src/trace_processor/importers/perf/reader.h"
+#include "src/trace_processor/importers/perf/record.h"
+
+namespace perfetto::trace_processor::perf_importer {
+namespace {
+
+bool ParseSampleReadGroup(Reader& reader,
+                          uint64_t read_format,
+                          uint64_t num_records,
+                          std::vector<Sample::ReadGroup>& out) {
+  out.resize(num_records);
+  for (auto& read : out) {
+    if (PERFETTO_UNLIKELY(!reader.Read(read.value))) {
+      return false;
+    }
+
+    if (read_format & PERF_FORMAT_ID) {
+      if (PERFETTO_UNLIKELY(!reader.ReadOptional(read.event_id))) {
+        return false;
+      }
+    }
+
+    if (read_format & PERF_FORMAT_LOST) {
+      uint64_t lost;
+      if (PERFETTO_UNLIKELY(!reader.Read(lost))) {
+        return false;
+      }
+    }
+  }
+
+  return true;
+}
+
+bool ParseSampleRead(Reader& reader,
+                     uint64_t read_format,
+                     std::vector<Sample::ReadGroup>& out) {
+  uint64_t value_or_nr;
+
+  if (PERFETTO_UNLIKELY(!reader.Read(value_or_nr))) {
+    return false;
+  }
+
+  if (read_format & PERF_FORMAT_TOTAL_TIME_ENABLED) {
+    uint64_t total_time_enabled;
+    if (PERFETTO_UNLIKELY(!reader.Read(total_time_enabled))) {
+      return false;
+    }
+  }
+
+  if (read_format & PERF_FORMAT_TOTAL_TIME_RUNNING) {
+    uint64_t total_time_running;
+    if (PERFETTO_UNLIKELY(!reader.Read(total_time_running))) {
+      return false;
+    }
+  }
+
+  if (read_format & PERF_FORMAT_GROUP) {
+    return ParseSampleReadGroup(reader, read_format, value_or_nr, out);
+  }
+
+  std::optional<uint64_t> event_id;
+  if (read_format & PERF_FORMAT_ID) {
+    event_id.emplace(0);
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(event_id))) {
+      return false;
+    }
+  }
+
+  if (read_format & PERF_FORMAT_LOST) {
+    uint64_t lost;
+    if (PERFETTO_UNLIKELY(!reader.Read(lost))) {
+      return false;
+    }
+  }
+
+  out.push_back({event_id, value_or_nr});
+
+  return true;
+}
+
+protos::pbzero::Profiling::CpuMode PerfCallchainContextToCpuMode(uint64_t ip) {
+  switch (ip) {
+    case PERF_CONTEXT_HV:
+      return protos::pbzero::Profiling::MODE_HYPERVISOR;
+    case PERF_CONTEXT_KERNEL:
+      return protos::pbzero::Profiling::MODE_KERNEL;
+    case PERF_CONTEXT_USER:
+      return protos::pbzero::Profiling::MODE_USER;
+    case PERF_CONTEXT_GUEST_KERNEL:
+      return protos::pbzero::Profiling::MODE_GUEST_KERNEL;
+    case PERF_CONTEXT_GUEST_USER:
+      return protos::pbzero::Profiling::MODE_GUEST_USER;
+    case PERF_CONTEXT_GUEST:
+    default:
+      return protos::pbzero::Profiling::MODE_UNKNOWN;
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+bool IsPerfContextMark(uint64_t ip) {
+  return ip >= PERF_CONTEXT_MAX;
+}
+
+bool ParseSampleCallchain(Reader& reader,
+                          protos::pbzero::Profiling::CpuMode cpu_mode,
+                          std::vector<Sample::Frame>& out) {
+  uint64_t nr;
+  if (PERFETTO_UNLIKELY(!reader.Read(nr))) {
+    return false;
+  }
+
+  std::vector<Sample::Frame> frames;
+  frames.reserve(nr);
+  for (; nr != 0; --nr) {
+    uint64_t ip;
+    if (PERFETTO_UNLIKELY(!reader.Read(ip))) {
+      return false;
+    }
+    if (PERFETTO_UNLIKELY(IsPerfContextMark(ip))) {
+      cpu_mode = PerfCallchainContextToCpuMode(ip);
+      continue;
+    }
+    frames.push_back({cpu_mode, ip});
+  }
+
+  out = std::move(frames);
+  return true;
+}
+}  // namespace
+
+base::Status Sample::Parse(int64_t in_trace_ts, const Record& record) {
+  PERFETTO_CHECK(record.attr);
+  const uint64_t sample_type = record.attr->sample_type();
+
+  trace_ts = in_trace_ts;
+  cpu_mode = record.GetCpuMode();
+  perf_session = record.session;
+  attr = record.attr;
+
+  Reader reader(record.payload.copy());
+
+  std::optional<uint64_t> identifier;
+  if (sample_type & PERF_SAMPLE_IDENTIFIER) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(identifier))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_IDENTIFIER");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_IP) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(ip))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_IP");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_TID) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(pid_tid))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_TID");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_TIME) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(time))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_TIME");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_ADDR) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(addr))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_ADDR");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_ID) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(id))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_ID");
+    }
+  }
+
+  if (identifier.has_value()) {
+    if (!id.has_value()) {
+      id = identifier;
+    } else if (PERFETTO_UNLIKELY(*identifier != *id)) {
+      return base::ErrStatus("ID and IDENTIFIER mismatch");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_STREAM_ID) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(stream_id))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_STREAM_ID");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_CPU) {
+    struct {
+      int32_t cpu;
+      int32_t unused;
+    } tmp;
+    if (PERFETTO_UNLIKELY(!reader.Read(tmp))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_CPU");
+    }
+    cpu = tmp.cpu;
+  }
+
+  if (sample_type & PERF_SAMPLE_PERIOD) {
+    if (PERFETTO_UNLIKELY(!reader.ReadOptional(period))) {
+      return base ::ErrStatus("Not enough data to read PERF_SAMPLE_PERIOD");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_READ) {
+    if (PERFETTO_UNLIKELY(
+            !ParseSampleRead(reader, attr->read_format(), read_groups))) {
+      return base::ErrStatus("Failed to read PERF_SAMPLE_READ field");
+    }
+    if (read_groups.empty()) {
+      return base::ErrStatus("No data in PERF_SAMPLE_READ field");
+    }
+  }
+
+  if (sample_type & PERF_SAMPLE_CALLCHAIN) {
+    if (PERFETTO_UNLIKELY(!ParseSampleCallchain(reader, cpu_mode, callchain))) {
+      return base::ErrStatus("Failed to read PERF_SAMPLE_CALLCHAIN field");
+    }
+  }
+
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/sample.h b/src/trace_processor/importers/perf/sample.h
new file mode 100644
index 0000000..532b613
--- /dev/null
+++ b/src/trace_processor/importers/perf/sample.h
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SAMPLE_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SAMPLE_H_
+
+#include <cstdint>
+#include <optional>
+#include <vector>
+
+#include "perfetto/base/status.h"
+#include "perfetto/trace_processor/ref_counted.h"
+#include "protos/perfetto/trace/profiling/profile_packet.pbzero.h"
+#include "src/trace_processor/importers/perf/perf_event_attr.h"
+#include "src/trace_processor/importers/perf/perf_session.h"
+
+namespace perfetto::trace_processor::perf_importer {
+
+struct Record;
+
+struct Sample {
+  struct Frame {
+    protos::pbzero::Profiling::CpuMode cpu_mode;
+    uint64_t ip;
+  };
+
+  struct PidTid {
+    uint32_t pid;
+    uint32_t tid;
+  };
+
+  struct ReadGroup {
+    std::optional<uint64_t> event_id;
+    uint64_t value;
+  };
+
+  int64_t trace_ts;
+  protos::pbzero::Profiling::CpuMode cpu_mode;
+  RefPtr<PerfSession> perf_session;
+  RefPtr<const PerfEventAttr> attr;
+
+  std::optional<uint64_t> ip;
+  std::optional<PidTid> pid_tid;
+  std::optional<uint64_t> time;
+  std::optional<uint64_t> addr;
+  std::optional<uint64_t> id;
+  std::optional<uint64_t> stream_id;
+  std::optional<uint32_t> cpu;
+  std::optional<uint64_t> period;
+  std::vector<ReadGroup> read_groups;
+  std::vector<Frame> callchain;
+
+  base::Status Parse(int64_t trace_ts, const Record& record);
+};
+
+}  // namespace perfetto::trace_processor::perf_importer
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PERF_SAMPLE_H_
diff --git a/src/trace_processor/importers/proto/chrome_system_probes_parser.h b/src/trace_processor/importers/proto/chrome_system_probes_parser.h
index ca68814..9c9397b 100644
--- a/src/trace_processor/importers/proto/chrome_system_probes_parser.h
+++ b/src/trace_processor/importers/proto/chrome_system_probes_parser.h
@@ -43,7 +43,7 @@
   // Maps a proto field number for memcounters in ProcessStats::Process to
   // their StringId. Keep kProcStatsProcessSize equal to 1 + max proto field
   // id of ProcessStats::Process. Also update SystemProbesParser.
-  static constexpr size_t kProcStatsProcessSize = 23;
+  static constexpr size_t kProcStatsProcessSize = 24;
   std::array<StringId, kProcStatsProcessSize> proc_stats_process_names_{};
 };
 
diff --git a/src/trace_processor/importers/proto/network_trace_module_unittest.cc b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
index f174b35..017b7f7 100644
--- a/src/trace_processor/importers/proto/network_trace_module_unittest.cc
+++ b/src/trace_processor/importers/proto/network_trace_module_unittest.cc
@@ -20,6 +20,7 @@
 #include "src/trace_processor/importers/common/args_translation_table.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
 #include "src/trace_processor/importers/common/global_args_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/slice_translation_table.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
@@ -45,6 +46,8 @@
     context_.args_tracker.reset(new ArgsTracker(&context_));
     context_.global_args_tracker.reset(new GlobalArgsTracker(storage_));
     context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
+    context_.process_track_translation_table.reset(
+        new ProcessTrackTranslationTable(storage_));
     context_.args_translation_table.reset(new ArgsTranslationTable(storage_));
     context_.async_track_set_tracker.reset(new AsyncTrackSetTracker(&context_));
     context_.proto_trace_parser.reset(new ProtoTraceParserImpl(&context_));
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
index 9d318a1..1845073 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.cc
@@ -44,7 +44,6 @@
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 
-#include "protos/perfetto/common/trace_stats.pbzero.h"
 #include "protos/perfetto/config/trace_config.pbzero.h"
 #include "protos/perfetto/trace/chrome/chrome_trace_event.pbzero.h"
 #include "protos/perfetto/trace/perfetto/perfetto_metatrace.pbzero.h"
@@ -85,9 +84,6 @@
     }
   }
 
-  if (packet.has_trace_stats())
-    ParseTraceStats(packet.trace_stats());
-
   if (packet.has_chrome_events()) {
     ParseChromeEvents(ts, packet.chrome_events());
   }
@@ -160,108 +156,6 @@
   context_->args_tracker->Flush();
 }
 
-void ProtoTraceParserImpl::ParseTraceStats(ConstBytes blob) {
-  protos::pbzero::TraceStats::Decoder evt(blob.data, blob.size);
-  auto* storage = context_->storage.get();
-  storage->SetStats(stats::traced_producers_connected,
-                    static_cast<int64_t>(evt.producers_connected()));
-  storage->SetStats(stats::traced_producers_seen,
-                    static_cast<int64_t>(evt.producers_seen()));
-  storage->SetStats(stats::traced_data_sources_registered,
-                    static_cast<int64_t>(evt.data_sources_registered()));
-  storage->SetStats(stats::traced_data_sources_seen,
-                    static_cast<int64_t>(evt.data_sources_seen()));
-  storage->SetStats(stats::traced_tracing_sessions,
-                    static_cast<int64_t>(evt.tracing_sessions()));
-  storage->SetStats(stats::traced_total_buffers,
-                    static_cast<int64_t>(evt.total_buffers()));
-  storage->SetStats(stats::traced_chunks_discarded,
-                    static_cast<int64_t>(evt.chunks_discarded()));
-  storage->SetStats(stats::traced_patches_discarded,
-                    static_cast<int64_t>(evt.patches_discarded()));
-  storage->SetStats(stats::traced_flushes_requested,
-                    static_cast<int64_t>(evt.flushes_requested()));
-  storage->SetStats(stats::traced_flushes_succeeded,
-                    static_cast<int64_t>(evt.flushes_succeeded()));
-  storage->SetStats(stats::traced_flushes_failed,
-                    static_cast<int64_t>(evt.flushes_failed()));
-
-  if (evt.has_filter_stats()) {
-    protos::pbzero::TraceStats::FilterStats::Decoder fstat(evt.filter_stats());
-    storage->SetStats(stats::filter_errors,
-                      static_cast<int64_t>(fstat.errors()));
-    storage->SetStats(stats::filter_input_bytes,
-                      static_cast<int64_t>(fstat.input_bytes()));
-    storage->SetStats(stats::filter_input_packets,
-                      static_cast<int64_t>(fstat.input_packets()));
-    storage->SetStats(stats::filter_output_bytes,
-                      static_cast<int64_t>(fstat.output_bytes()));
-    storage->SetStats(stats::filter_time_taken_ns,
-                      static_cast<int64_t>(fstat.time_taken_ns()));
-    for (auto [i, it] = std::tuple{0, fstat.bytes_discarded_per_buffer()}; it;
-         ++it, ++i) {
-      storage->SetIndexedStats(stats::traced_buf_bytes_filtered_out, i,
-                               static_cast<int64_t>(*it));
-    }
-  }
-
-  switch (evt.final_flush_outcome()) {
-    case protos::pbzero::TraceStats::FINAL_FLUSH_SUCCEEDED:
-      storage->IncrementStats(stats::traced_final_flush_succeeded, 1);
-      break;
-    case protos::pbzero::TraceStats::FINAL_FLUSH_FAILED:
-      storage->IncrementStats(stats::traced_final_flush_failed, 1);
-      break;
-    case protos::pbzero::TraceStats::FINAL_FLUSH_UNSPECIFIED:
-      break;
-  }
-
-  int buf_num = 0;
-  for (auto it = evt.buffer_stats(); it; ++it, ++buf_num) {
-    protos::pbzero::TraceStats::BufferStats::Decoder buf(*it);
-    storage->SetIndexedStats(stats::traced_buf_buffer_size, buf_num,
-                             static_cast<int64_t>(buf.buffer_size()));
-    storage->SetIndexedStats(stats::traced_buf_bytes_written, buf_num,
-                             static_cast<int64_t>(buf.bytes_written()));
-    storage->SetIndexedStats(stats::traced_buf_bytes_overwritten, buf_num,
-                             static_cast<int64_t>(buf.bytes_overwritten()));
-    storage->SetIndexedStats(stats::traced_buf_bytes_read, buf_num,
-                             static_cast<int64_t>(buf.bytes_read()));
-    storage->SetIndexedStats(stats::traced_buf_padding_bytes_written, buf_num,
-                             static_cast<int64_t>(buf.padding_bytes_written()));
-    storage->SetIndexedStats(stats::traced_buf_padding_bytes_cleared, buf_num,
-                             static_cast<int64_t>(buf.padding_bytes_cleared()));
-    storage->SetIndexedStats(stats::traced_buf_chunks_written, buf_num,
-                             static_cast<int64_t>(buf.chunks_written()));
-    storage->SetIndexedStats(stats::traced_buf_chunks_rewritten, buf_num,
-                             static_cast<int64_t>(buf.chunks_rewritten()));
-    storage->SetIndexedStats(stats::traced_buf_chunks_overwritten, buf_num,
-                             static_cast<int64_t>(buf.chunks_overwritten()));
-    storage->SetIndexedStats(stats::traced_buf_chunks_discarded, buf_num,
-                             static_cast<int64_t>(buf.chunks_discarded()));
-    storage->SetIndexedStats(stats::traced_buf_chunks_read, buf_num,
-                             static_cast<int64_t>(buf.chunks_read()));
-    storage->SetIndexedStats(
-        stats::traced_buf_chunks_committed_out_of_order, buf_num,
-        static_cast<int64_t>(buf.chunks_committed_out_of_order()));
-    storage->SetIndexedStats(stats::traced_buf_write_wrap_count, buf_num,
-                             static_cast<int64_t>(buf.write_wrap_count()));
-    storage->SetIndexedStats(stats::traced_buf_patches_succeeded, buf_num,
-                             static_cast<int64_t>(buf.patches_succeeded()));
-    storage->SetIndexedStats(stats::traced_buf_patches_failed, buf_num,
-                             static_cast<int64_t>(buf.patches_failed()));
-    storage->SetIndexedStats(stats::traced_buf_readaheads_succeeded, buf_num,
-                             static_cast<int64_t>(buf.readaheads_succeeded()));
-    storage->SetIndexedStats(stats::traced_buf_readaheads_failed, buf_num,
-                             static_cast<int64_t>(buf.readaheads_failed()));
-    storage->SetIndexedStats(stats::traced_buf_abi_violations, buf_num,
-                             static_cast<int64_t>(buf.abi_violations()));
-    storage->SetIndexedStats(
-        stats::traced_buf_trace_writer_packet_loss, buf_num,
-        static_cast<int64_t>(buf.trace_writer_packet_loss()));
-  }
-}
-
 void ProtoTraceParserImpl::ParseChromeEvents(int64_t ts, ConstBytes blob) {
   TraceStorage* storage = context_->storage.get();
   protos::pbzero::ChromeEventBundle::Decoder bundle(blob.data, blob.size);
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl.h b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
index e9bce35..2c4dc07 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl.h
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl.h
@@ -65,7 +65,6 @@
                               int64_t /*ts*/,
                               InlineSchedWaking data) override;
 
-  void ParseTraceStats(ConstBytes);
   void ParseChromeEvents(int64_t ts, ConstBytes);
   void ParseMetatraceEvent(int64_t ts, ConstBytes);
 
diff --git a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
index 95b16d1..a1a877d 100644
--- a/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
+++ b/src/trace_processor/importers/proto/proto_trace_parser_impl_unittest.cc
@@ -27,6 +27,7 @@
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
@@ -265,6 +266,8 @@
     context_.ftrace_sched_tracker.reset(sched_);
     process_ = new NiceMock<MockProcessTracker>(&context_);
     context_.process_tracker.reset(process_);
+    context_.process_track_translation_table.reset(
+        new ProcessTrackTranslationTable(storage_));
     slice_ = new NiceMock<MockSliceTracker>(&context_);
     context_.slice_tracker.reset(slice_);
     context_.slice_translation_table.reset(new SliceTranslationTable(storage_));
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 2e50f0c..d958c0b 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -21,6 +21,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/proto_decoder.h"
@@ -41,6 +42,7 @@
 #include "src/trace_processor/util/gzip_utils.h"
 
 #include "protos/perfetto/common/builtin_clock.pbzero.h"
+#include "protos/perfetto/common/trace_stats.pbzero.h"
 #include "protos/perfetto/config/trace_config.pbzero.h"
 #include "protos/perfetto/trace/clock_snapshot.pbzero.h"
 #include "protos/perfetto/trace/extension_descriptor.pbzero.h"
@@ -118,6 +120,16 @@
     HandlePreviousPacketDropped(decoder);
   }
 
+  uint32_t sequence_id = decoder.trusted_packet_sequence_id();
+  if (sequence_id) {
+    auto [data_loss, inserted] =
+        packet_sequence_data_loss_.Insert(sequence_id, 0);
+
+    if (!inserted && decoder.previous_packet_dropped()) {
+      *data_loss += 1;
+    }
+  }
+
   // It is important that we parse defaults before parsing other fields such as
   // the timestamp, since the defaults could affect them.
   if (decoder.has_trace_packet_defaults()) {
@@ -131,8 +143,11 @@
   }
 
   if (decoder.has_clock_snapshot()) {
-    return ParseClockSnapshot(decoder.clock_snapshot(),
-                              decoder.trusted_packet_sequence_id());
+    return ParseClockSnapshot(decoder.clock_snapshot(), sequence_id);
+  }
+
+  if (decoder.has_trace_stats()) {
+    ParseTraceStats(decoder.trace_stats());
   }
 
   if (decoder.has_service_event()) {
@@ -474,6 +489,125 @@
   return util::OkStatus();
 }
 
+void ProtoTraceReader::ParseTraceStats(ConstBytes blob) {
+  protos::pbzero::TraceStats::Decoder evt(blob.data, blob.size);
+  auto* storage = context_->storage.get();
+  storage->SetStats(stats::traced_producers_connected,
+                    static_cast<int64_t>(evt.producers_connected()));
+  storage->SetStats(stats::traced_producers_seen,
+                    static_cast<int64_t>(evt.producers_seen()));
+  storage->SetStats(stats::traced_data_sources_registered,
+                    static_cast<int64_t>(evt.data_sources_registered()));
+  storage->SetStats(stats::traced_data_sources_seen,
+                    static_cast<int64_t>(evt.data_sources_seen()));
+  storage->SetStats(stats::traced_tracing_sessions,
+                    static_cast<int64_t>(evt.tracing_sessions()));
+  storage->SetStats(stats::traced_total_buffers,
+                    static_cast<int64_t>(evt.total_buffers()));
+  storage->SetStats(stats::traced_chunks_discarded,
+                    static_cast<int64_t>(evt.chunks_discarded()));
+  storage->SetStats(stats::traced_patches_discarded,
+                    static_cast<int64_t>(evt.patches_discarded()));
+  storage->SetStats(stats::traced_flushes_requested,
+                    static_cast<int64_t>(evt.flushes_requested()));
+  storage->SetStats(stats::traced_flushes_succeeded,
+                    static_cast<int64_t>(evt.flushes_succeeded()));
+  storage->SetStats(stats::traced_flushes_failed,
+                    static_cast<int64_t>(evt.flushes_failed()));
+
+  if (evt.has_filter_stats()) {
+    protos::pbzero::TraceStats::FilterStats::Decoder fstat(evt.filter_stats());
+    storage->SetStats(stats::filter_errors,
+                      static_cast<int64_t>(fstat.errors()));
+    storage->SetStats(stats::filter_input_bytes,
+                      static_cast<int64_t>(fstat.input_bytes()));
+    storage->SetStats(stats::filter_input_packets,
+                      static_cast<int64_t>(fstat.input_packets()));
+    storage->SetStats(stats::filter_output_bytes,
+                      static_cast<int64_t>(fstat.output_bytes()));
+    storage->SetStats(stats::filter_time_taken_ns,
+                      static_cast<int64_t>(fstat.time_taken_ns()));
+    for (auto [i, it] = std::tuple{0, fstat.bytes_discarded_per_buffer()}; it;
+         ++it, ++i) {
+      storage->SetIndexedStats(stats::traced_buf_bytes_filtered_out, i,
+                               static_cast<int64_t>(*it));
+    }
+  }
+
+  switch (evt.final_flush_outcome()) {
+    case protos::pbzero::TraceStats::FINAL_FLUSH_SUCCEEDED:
+      storage->IncrementStats(stats::traced_final_flush_succeeded, 1);
+      break;
+    case protos::pbzero::TraceStats::FINAL_FLUSH_FAILED:
+      storage->IncrementStats(stats::traced_final_flush_failed, 1);
+      break;
+    case protos::pbzero::TraceStats::FINAL_FLUSH_UNSPECIFIED:
+      break;
+  }
+
+  int buf_num = 0;
+  for (auto it = evt.buffer_stats(); it; ++it, ++buf_num) {
+    protos::pbzero::TraceStats::BufferStats::Decoder buf(*it);
+    storage->SetIndexedStats(stats::traced_buf_buffer_size, buf_num,
+                             static_cast<int64_t>(buf.buffer_size()));
+    storage->SetIndexedStats(stats::traced_buf_bytes_written, buf_num,
+                             static_cast<int64_t>(buf.bytes_written()));
+    storage->SetIndexedStats(stats::traced_buf_bytes_overwritten, buf_num,
+                             static_cast<int64_t>(buf.bytes_overwritten()));
+    storage->SetIndexedStats(stats::traced_buf_bytes_read, buf_num,
+                             static_cast<int64_t>(buf.bytes_read()));
+    storage->SetIndexedStats(stats::traced_buf_padding_bytes_written, buf_num,
+                             static_cast<int64_t>(buf.padding_bytes_written()));
+    storage->SetIndexedStats(stats::traced_buf_padding_bytes_cleared, buf_num,
+                             static_cast<int64_t>(buf.padding_bytes_cleared()));
+    storage->SetIndexedStats(stats::traced_buf_chunks_written, buf_num,
+                             static_cast<int64_t>(buf.chunks_written()));
+    storage->SetIndexedStats(stats::traced_buf_chunks_rewritten, buf_num,
+                             static_cast<int64_t>(buf.chunks_rewritten()));
+    storage->SetIndexedStats(stats::traced_buf_chunks_overwritten, buf_num,
+                             static_cast<int64_t>(buf.chunks_overwritten()));
+    storage->SetIndexedStats(stats::traced_buf_chunks_discarded, buf_num,
+                             static_cast<int64_t>(buf.chunks_discarded()));
+    storage->SetIndexedStats(stats::traced_buf_chunks_read, buf_num,
+                             static_cast<int64_t>(buf.chunks_read()));
+    storage->SetIndexedStats(
+        stats::traced_buf_chunks_committed_out_of_order, buf_num,
+        static_cast<int64_t>(buf.chunks_committed_out_of_order()));
+    storage->SetIndexedStats(stats::traced_buf_write_wrap_count, buf_num,
+                             static_cast<int64_t>(buf.write_wrap_count()));
+    storage->SetIndexedStats(stats::traced_buf_patches_succeeded, buf_num,
+                             static_cast<int64_t>(buf.patches_succeeded()));
+    storage->SetIndexedStats(stats::traced_buf_patches_failed, buf_num,
+                             static_cast<int64_t>(buf.patches_failed()));
+    storage->SetIndexedStats(stats::traced_buf_readaheads_succeeded, buf_num,
+                             static_cast<int64_t>(buf.readaheads_succeeded()));
+    storage->SetIndexedStats(stats::traced_buf_readaheads_failed, buf_num,
+                             static_cast<int64_t>(buf.readaheads_failed()));
+    storage->SetIndexedStats(stats::traced_buf_abi_violations, buf_num,
+                             static_cast<int64_t>(buf.abi_violations()));
+    storage->SetIndexedStats(
+        stats::traced_buf_trace_writer_packet_loss, buf_num,
+        static_cast<int64_t>(buf.trace_writer_packet_loss()));
+  }
+
+  base::FlatHashMap<int32_t, int64_t> data_loss_per_buffer;
+
+  for (auto it = evt.writer_stats(); it; ++it) {
+    protos::pbzero::TraceStats::WriterStats::Decoder writer(*it);
+    auto* data_loss = packet_sequence_data_loss_.Find(
+        static_cast<uint32_t>(writer.sequence_id()));
+    if (data_loss) {
+      data_loss_per_buffer[static_cast<int32_t>(writer.buffer())] +=
+          static_cast<int64_t>(*data_loss);
+    }
+  }
+
+  for (auto it = data_loss_per_buffer.GetIterator(); it; ++it) {
+    storage->SetIndexedStats(stats::traced_buf_sequence_packet_loss, it.key(),
+                             it.value());
+  }
+}
+
 void ProtoTraceReader::NotifyEndOfFile() {}
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index a93ad98..4c4e2ca 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -78,6 +78,7 @@
   void ParseInternedData(const protos::pbzero::TracePacket_Decoder&,
                          TraceBlobView interned_data);
   void ParseTraceConfig(ConstBytes);
+  void ParseTraceStats(ConstBytes);
 
   std::optional<StringId> GetBuiltinClockNameOrNull(int64_t clock_id);
 
@@ -104,6 +105,8 @@
   base::FlatHashMap<uint32_t, PacketSequenceStateBuilder>
       packet_sequence_state_builders_;
 
+  base::FlatHashMap<uint32_t, size_t> packet_sequence_data_loss_;
+
   StringId skipped_packet_key_id_;
   StringId invalid_incremental_state_key_id_;
 };
diff --git a/src/trace_processor/importers/proto/system_probes_parser.cc b/src/trace_processor/importers/proto/system_probes_parser.cc
index 5b878ed..5cc02ff 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.cc
+++ b/src/trace_processor/importers/proto/system_probes_parser.cc
@@ -168,6 +168,8 @@
       context->storage->InternString("mem.smaps.pss.file");
   proc_stats_process_names_[ProcessStats::Process::kSmrPssShmemKbFieldNumber] =
       context->storage->InternString("mem.smaps.pss.shmem");
+  proc_stats_process_names_[ProcessStats::Process::kSmrSwapPssKbFieldNumber] =
+      context->storage->InternString("mem.smaps.swap.pss");
   proc_stats_process_names_
       [ProcessStats::Process::kRuntimeUserModeFieldNumber] =
           context->storage->InternString("runtime.user_ns");
diff --git a/src/trace_processor/importers/proto/system_probes_parser.h b/src/trace_processor/importers/proto/system_probes_parser.h
index a54beb7..472e334 100644
--- a/src/trace_processor/importers/proto/system_probes_parser.h
+++ b/src/trace_processor/importers/proto/system_probes_parser.h
@@ -75,7 +75,7 @@
   // their StringId. Keep kProcStatsProcessSize equal to 1 + max proto field
   // id of ProcessStats::Process. Also update the value in
   // ChromeSystemProbesParser.
-  static constexpr size_t kProcStatsProcessSize = 23;
+  static constexpr size_t kProcStatsProcessSize = 24;
   std::array<StringId, kProcStatsProcessSize> proc_stats_process_names_{};
 
   // Maps a SysStats::PsiSample::PsiResource type to its StringId.
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 03c8f21..657e308 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -30,6 +30,7 @@
 #include "src/trace_processor/importers/common/flow_tracker.h"
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/common/virtual_memory_mapping.h"
 #include "src/trace_processor/importers/json/json_utils.h"
@@ -1526,9 +1527,12 @@
   }
 
   // Override the name with the most recent name seen (after sorting by ts).
-  if (decoder.has_name()) {
+  if (decoder.has_name() || decoder.has_static_name()) {
     auto* tracks = context_->storage->mutable_track_table();
-    StringId name_id = context_->storage->InternString(decoder.name());
+    const StringId raw_name_id = context_->storage->InternString(
+        decoder.has_name() ? decoder.name() : decoder.static_name());
+    const StringId name_id =
+      context_->process_track_translation_table->TranslateName(raw_name_id);
     tracks->mutable_name()->Set(*tracks->id().IndexOf(track_id), name_id);
   }
 }
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index c836052..238e247 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -92,6 +92,8 @@
   StringId name_id = kNullStringId;
   if (track.has_name())
     name_id = context_->storage->InternString(track.name());
+  else if (track.has_static_name())
+    name_id = context_->storage->InternString(track.static_name());
 
   if (packet.has_trusted_pid()) {
     context_->process_tracker->UpdateTrustedPid(
diff --git a/src/trace_processor/importers/proto/track_event_tracker.cc b/src/trace_processor/importers/proto/track_event_tracker.cc
index 56e081d..e758b9d 100644
--- a/src/trace_processor/importers/proto/track_event_tracker.cc
+++ b/src/trace_processor/importers/proto/track_event_tracker.cc
@@ -18,6 +18,7 @@
 
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/args_translation_table.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/tables/track_tables_py.h"
@@ -201,7 +202,9 @@
       reservation_it->second.is_counter) {
     return track_id;
   }
-  tracks->mutable_name()->Set(row, event_name);
+  const StringId track_name =
+      context_->process_track_translation_table->TranslateName(event_name);
+  tracks->mutable_name()->Set(row, track_name);
   return track_id;
 }
 
diff --git a/src/trace_processor/importers/proto/translation_table_module.cc b/src/trace_processor/importers/proto/translation_table_module.cc
index 8684df0..f45e353 100644
--- a/src/trace_processor/importers/proto/translation_table_module.cc
+++ b/src/trace_processor/importers/proto/translation_table_module.cc
@@ -16,6 +16,7 @@
 #include "src/trace_processor/importers/proto/translation_table_module.h"
 
 #include "src/trace_processor/importers/common/args_translation_table.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/slice_translation_table.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 
@@ -54,6 +55,8 @@
         translation_table.chrome_performance_mark());
   } else if (translation_table.has_slice_name()) {
     ParseSliceNameRules(translation_table.slice_name());
+  } else if (translation_table.has_process_track_name()) {
+    ParseProcessTrackNameRules(translation_table.process_track_name());
   }
   return ModuleResult::Handled();
 }
@@ -113,5 +116,17 @@
   }
 }
 
+void TranslationTableModule::ParseProcessTrackNameRules(
+    protozero::ConstBytes bytes) {
+  const auto process_track_name =
+      protos::pbzero::ProcessTrackNameTranslationTable::Decoder(bytes);
+  for (auto it = process_track_name.raw_to_deobfuscated_name(); it; ++it) {
+    protos::pbzero::ProcessTrackNameTranslationTable::
+        RawToDeobfuscatedNameEntry::Decoder entry(*it);
+    context_->process_track_translation_table->AddNameTranslationRule(
+        entry.key(), entry.value());
+  }
+}
+
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/translation_table_module.h b/src/trace_processor/importers/proto/translation_table_module.h
index 2060f41..62d4c72 100644
--- a/src/trace_processor/importers/proto/translation_table_module.h
+++ b/src/trace_processor/importers/proto/translation_table_module.h
@@ -46,6 +46,7 @@
   void ParseChromeUserEventRules(protozero::ConstBytes bytes);
   void ParseChromePerformanceMarkRules(protozero::ConstBytes bytes);
   void ParseSliceNameRules(protozero::ConstBytes bytes);
+  void ParseProcessTrackNameRules(protozero::ConstBytes bytes);
 
   TraceProcessorContext* context_;
 };
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/auto/multiuser.sql b/src/trace_processor/perfetto_sql/stdlib/android/auto/multiuser.sql
index ea95dcc..2b408ef 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/auto/multiuser.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/auto/multiuser.sql
@@ -64,7 +64,7 @@
   FROM android_startups
   UNION
   SELECT
-    slice.ts as event_end_time,
+    slice.ts + slice.dur as event_end_time,
     slice.name as event_end_name
   FROM slice
   WHERE slice.name GLOB "finishUserStopped-10*"
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql b/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
index a50b033..a3d47f2 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/process_metadata.sql
@@ -20,6 +20,47 @@
 FROM package_list
 GROUP BY 1;
 
+CREATE PERFETTO FUNCTION _android_package_for_process(
+  uid INT,
+  uid_count INT,
+  process_name STRING
+)
+RETURNS TABLE(
+  package_name STRING,
+  version_code INT,
+  debuggable BOOL
+)
+AS
+WITH min_distance AS (
+  SELECT
+    -- SQLite allows omitting the group-by for the MIN: the other columns
+    -- will match the row with the minimum value.
+    MIN(LENGTH($process_name) - LENGTH(package_name)),
+    package_name,
+    version_code,
+    debuggable
+  FROM package_list
+  WHERE (
+    (
+      $uid = uid
+      AND (
+        -- unique match
+        $uid_count = 1
+        -- or process name is a prefix the package name
+        OR $process_name GLOB package_name || '*'
+      )
+    )
+    OR
+    (
+      -- isolated processes can only be matched based on the name
+      $uid >= 90000 AND $uid < 100000
+      AND STR_SPLIT($process_name, ':', 0) GLOB package_name || '*'
+    )
+  )
+)
+SELECT package_name, version_code, debuggable
+FROM min_distance;
+
 -- Data about packages running on the process.
 CREATE PERFETTO TABLE android_process_metadata(
   -- Process upid.
@@ -57,21 +98,6 @@
   plist.debuggable
 FROM process
 LEFT JOIN _uid_package_count ON process.android_appid = _uid_package_count.uid
-LEFT JOIN package_list plist
-  ON (
-    (
-      process.android_appid = plist.uid
-      AND _uid_package_count.uid = plist.uid
-      AND (
-        -- unique match
-        _uid_package_count.cnt = 1
-        -- or process name starts with the package name
-        OR process.name GLOB plist.package_name || '*')
-    )
-    OR
-    (
-      -- isolated processes can only be matched based on the name
-      process.android_appid >= 90000 AND process.android_appid < 100000
-      AND STR_SPLIT(process.name, ':', 0) = plist.package_name
-    )
-  );
+LEFT JOIN _android_package_for_process(
+  process.android_appid, _uid_package_count.cnt, process.name
+) AS plist;
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
index 0d31b0d..1551ea6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
@@ -73,7 +73,9 @@
 FROM awake_slice
 UNION ALL
 SELECT ts, dur, 'suspended' AS power_state
-FROM suspend_slice;
+FROM suspend_slice
+ORDER BY ts; -- Order by will cause Perfetto table to index by ts.
+
 
 -- Extracts the duration without counting CPU suspended time from an event.
 -- This is the same as converting an event duration from wall clock to monotonic clock.
diff --git a/src/trace_processor/read_trace.cc b/src/trace_processor/read_trace.cc
index 06bd39d..9d5a1f7 100644
--- a/src/trace_processor/read_trace.cc
+++ b/src/trace_processor/read_trace.cc
@@ -25,12 +25,12 @@
 
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
 #include "src/trace_processor/read_trace_internal.h"
 #include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_type.h"
 
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index 8d4c9f9..12ccf3985 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -32,6 +32,7 @@
     "../importers/common:parser_types",
     "../importers/common:trace_parser_hdr",
     "../importers/fuchsia:fuchsia_record",
+    "../importers/perf:record",
     "../importers/systrace:systrace_line",
     "../storage",
     "../types",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index a2d9861..9ccea77 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -15,20 +15,28 @@
  */
 
 #include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+#include <limits>
 #include <memory>
 #include <utility>
 
 #include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/public/compiler.h"
 #include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/perf/record.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
-#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/sorter/trace_token_buffer.h"
+#include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/bump_allocator.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 TraceSorter::TraceSorter(TraceProcessorContext* context,
                          SortingMode sorting_mode)
@@ -116,10 +124,12 @@
         auto& queue = sorter_data.queues[i];
         if (queue.events_.empty())
           continue;
-        all_queues_empty = false;
-
         PERFETTO_DCHECK(queue.max_ts_ <= append_max_ts_);
-        if (queue.min_ts_ < min_queue_ts[0]) {
+
+        // Checking for |all_queues_empty| is necessary here as in fuzzer cases
+        // we can end up with |int64::max()| as the value here.
+        // See https://crbug.com/oss-fuzz/69164 for an example.
+        if (all_queues_empty || queue.min_ts_ < min_queue_ts[0]) {
           min_queue_ts[1] = min_queue_ts[0];
           min_queue_ts[0] = queue.min_ts_;
           min_queue_idx = i;
@@ -127,6 +137,7 @@
         } else if (queue.min_ts_ < min_queue_ts[1]) {
           min_queue_ts[1] = queue.min_ts_;
         }
+        all_queues_empty = false;
       }
     }
     if (all_queues_empty)
@@ -188,7 +199,7 @@
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
     case TimestampedEvent::Type::kPerfRecord:
       context.perf_record_parser->ParsePerfRecord(
-          event.ts, token_buffer_.Extract<TraceBlobView>(id));
+          event.ts, token_buffer_.Extract<perf_importer::Record>(id));
       return;
     case TimestampedEvent::Type::kTracePacket:
       context.proto_trace_parser->ParseTracePacket(
@@ -275,10 +286,9 @@
     const TimestampedEvent& event) {
   TraceTokenBuffer::Id id = GetTokenBufferId(event);
   switch (static_cast<TimestampedEvent::Type>(event.event_type)) {
-    case TimestampedEvent::Type::kPerfRecord:
-      base::ignore_result(token_buffer_.Extract<TraceBlobView>(id));
-      return;
     case TimestampedEvent::Type::kTracePacket:
+    case TimestampedEvent::Type::kFtraceEvent:
+    case TimestampedEvent::Type::kEtwEvent:
       base::ignore_result(token_buffer_.Extract<TracePacketData>(id));
       return;
     case TimestampedEvent::Type::kTrackEvent:
@@ -299,11 +309,8 @@
     case TimestampedEvent::Type::kInlineSchedWaking:
       base::ignore_result(token_buffer_.Extract<InlineSchedWaking>(id));
       return;
-    case TimestampedEvent::Type::kFtraceEvent:
-      base::ignore_result(token_buffer_.Extract<TracePacketData>(id));
-      return;
-    case TimestampedEvent::Type::kEtwEvent:
-      base::ignore_result(token_buffer_.Extract<TracePacketData>(id));
+    case TimestampedEvent::Type::kPerfRecord:
+      base::ignore_result(token_buffer_.Extract<perf_importer::Record>(id));
       return;
   }
   PERFETTO_FATAL("For GCC");
@@ -342,5 +349,4 @@
   }
 }
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 5ea0e0d..dd81920 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -18,26 +18,33 @@
 #define SRC_TRACE_PROCESSOR_SORTER_TRACE_SORTER_H_
 
 #include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <limits>
 #include <memory>
 #include <optional>
+#include <string>
+#include <tuple>
+#include <type_traits>
 #include <utility>
 #include <vector>
 
+#include "perfetto/base/logging.h"
 #include "perfetto/ext/base/circular_queue.h"
-#include "perfetto/ext/base/utils.h"
 #include "perfetto/public/compiler.h"
-#include "perfetto/trace_processor/basic_types.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
+#include "src/trace_processor/importers/perf/record.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
 #include "src/trace_processor/sorter/trace_token_buffer.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/util/bump_allocator.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
 
 // This class takes care of sorting events parsed from the trace stream in
 // arbitrary order and pushing them to the next pipeline stages (parsing) in
@@ -104,7 +111,7 @@
 
   inline void PushPerfRecord(
       int64_t timestamp,
-      TraceBlobView record,
+      perf_importer::Record record,
       std::optional<MachineId> machine_id = std::nullopt) {
     TraceTokenBuffer::Id id = token_buffer_.Append(std::move(record));
     AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kPerfRecord, id,
@@ -218,7 +225,7 @@
     SortAndExtractEventsUntilAllocId(end_id);
     for (auto& sorter_data : sorter_data_by_machine_) {
       for (const auto& queue : sorter_data.queues) {
-        PERFETTO_DCHECK(queue.events_.empty());
+        PERFETTO_CHECK(queue.events_.empty());
       }
       sorter_data.queues.clear();
     }
@@ -294,13 +301,13 @@
 
   static_assert(sizeof(TimestampedEvent) == 16,
                 "TimestampedEvent must be equal to 16 bytes");
-  static_assert(std::is_trivially_copyable<TimestampedEvent>::value,
+  static_assert(std::is_trivially_copyable_v<TimestampedEvent>,
                 "TimestampedEvent must be trivially copyable");
-  static_assert(std::is_trivially_move_assignable<TimestampedEvent>::value,
+  static_assert(std::is_trivially_move_assignable_v<TimestampedEvent>,
                 "TimestampedEvent must be trivially move assignable");
-  static_assert(std::is_trivially_move_constructible<TimestampedEvent>::value,
+  static_assert(std::is_trivially_move_constructible_v<TimestampedEvent>,
                 "TimestampedEvent must be trivially move constructible");
-  static_assert(std::is_nothrow_swappable<TimestampedEvent>::value,
+  static_assert(std::is_nothrow_swappable_v<TimestampedEvent>,
                 "TimestampedEvent must be trivially swappable");
 
   struct Queue {
@@ -357,7 +364,7 @@
     auto* queues = &sorter_data_by_machine_[0].queues;
 
     // Find the TraceSorterData instance when |machine_id| is not nullopt.
-    if (PERFETTO_UNLIKELY(!!machine_id)) {
+    if (PERFETTO_UNLIKELY(machine_id.has_value())) {
       auto it = std::find_if(sorter_data_by_machine_.begin() + 1,
                              sorter_data_by_machine_.end(),
                              [machine_id](const TraceSorterData& item) {
@@ -400,7 +407,7 @@
                          const TimestampedEvent&);
   void ExtractAndDiscardTokenizedObject(const TimestampedEvent& event);
 
-  TraceTokenBuffer::Id GetTokenBufferId(const TimestampedEvent& event) {
+  static TraceTokenBuffer::Id GetTokenBufferId(const TimestampedEvent& event) {
     return TraceTokenBuffer::Id{event.alloc_id()};
   }
 
@@ -447,7 +454,6 @@
   int64_t latest_pushed_event_ts_ = std::numeric_limits<int64_t>::min();
 };
 
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_SORTER_TRACE_SORTER_H_
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index 60a8122..cc4f545 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -571,7 +571,7 @@
 
   // Distinct:
   idx_str += "D";
-  if (ob_idxes.size() == 1) {
+  if (ob_idxes.size() == 1 && PERFETTO_POPCOUNT(info->colUsed) == 1) {
     switch (sqlite3_vtab_distinct(info)) {
       case 0:
       case 1:
diff --git a/src/trace_processor/sqlite/module_lifecycle_manager.h b/src/trace_processor/sqlite/module_lifecycle_manager.h
index 66054d5..f1f93e8 100644
--- a/src/trace_processor/sqlite/module_lifecycle_manager.h
+++ b/src/trace_processor/sqlite/module_lifecycle_manager.h
@@ -67,6 +67,11 @@
  public:
   // Per-vtab state. The pointer to this class should be stored in the Vtab.
   struct PerVtabState {
+   private:
+    // The below fields should only be accessed by the manager, use GetState to
+    // access the state from outside this class.
+    friend class ModuleStateManager<Module>;
+
     ModuleStateManager* manager;
     bool disconnected = false;
     std::string table_name;
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 9067b09..c8d58ca 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -155,7 +155,13 @@
   F(traced_buf_patches_succeeded,         kIndexed, kInfo,     kTrace,    ""), \
   F(traced_buf_readaheads_failed,         kIndexed, kInfo,     kTrace,    ""), \
   F(traced_buf_readaheads_succeeded,      kIndexed, kInfo,     kTrace,    ""), \
-  F(traced_buf_trace_writer_packet_loss,  kIndexed, kDataLoss, kTrace,    ""), \
+  F(traced_buf_trace_writer_packet_loss,  kIndexed, kDataLoss, kTrace,         \
+      "The tracing service observed packet loss for this buffer during this "  \
+      "tracing session. This also counts packet loss that happened before "    \
+      "the RING_BUFFER start or after the DISCARD buffer end."),               \
+  F(traced_buf_sequence_packet_loss,      kIndexed, kDataLoss, kAnalysis,      \
+      "The number of groups of consecutive packets lost in each sequence for " \
+      "this buffer"), \
   F(traced_buf_write_wrap_count,          kIndexed, kInfo,     kTrace,    ""), \
   F(traced_chunks_discarded,              kSingle,  kInfo,     kTrace,    ""), \
   F(traced_data_sources_registered,       kSingle,  kInfo,     kTrace,    ""), \
@@ -262,8 +268,13 @@
   F(perf_process_shard_count,             kIndexed, kInfo,     kTrace,    ""), \
   F(perf_chosen_process_shard,            kIndexed, kInfo,     kTrace,    ""), \
   F(perf_guardrail_stop_ts,               kIndexed, kDataLoss, kTrace,    ""), \
-  F(perf_samples_skipped,                 kSingle,  kInfo,     kTrace,    ""), \
+  F(perf_unknown_record_type,             kIndexed, kInfo,     kAnalysis, ""), \
+  F(perf_record_skipped,                  kSingle,  kError,    kAnalysis, ""), \
+  F(perf_samples_skipped,                 kSingle,  kError,    kAnalysis, ""), \
+  F(perf_features_skipped,                kIndexed, kInfo,     kAnalysis, ""), \
   F(perf_samples_skipped_dataloss,        kSingle,  kDataLoss, kTrace,    ""), \
+  F(perf_dummy_mapping_used,              kSingle,  kInfo,     kAnalysis, ""), \
+  F(perf_invalid_event_id,                kSingle,  kError,    kTrace,    ""), \
   F(memory_snapshot_parser_failure,       kSingle,  kError,    kAnalysis, ""), \
   F(thread_time_in_state_out_of_order,    kSingle,  kError,    kAnalysis, ""), \
   F(thread_time_in_state_unknown_cpu_freq,                                     \
diff --git a/src/trace_processor/trace_processor_context.cc b/src/trace_processor/trace_processor_context.cc
index 2299a1b..ec20623 100644
--- a/src/trace_processor/trace_processor_context.cc
+++ b/src/trace_processor/trace_processor_context.cc
@@ -31,6 +31,7 @@
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/sched_event_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
@@ -45,6 +46,7 @@
 #include "src/trace_processor/importers/proto/track_event.descriptor.h"
 #include "src/trace_processor/importers/proto/track_event_module.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/trace_reader_registry.h"
 #include "src/trace_processor/types/destructible.h"
 
 namespace perfetto {
@@ -52,6 +54,7 @@
 
 TraceProcessorContext::TraceProcessorContext(const InitArgs& args)
     : config(args.config), storage(args.storage) {
+  reader_registry = std::make_unique<TraceReaderRegistry>(this);
   // Init the trackers.
   machine_tracker.reset(new MachineTracker(this, args.raw_machine_id));
   if (!machine_id()) {
@@ -67,6 +70,8 @@
   event_tracker.reset(new EventTracker(this));
   sched_event_tracker.reset(new SchedEventTracker(this));
   process_tracker.reset(new ProcessTracker(this));
+  process_track_translation_table.reset(
+      new ProcessTrackTranslationTable(storage.get()));
   clock_tracker.reset(new ClockTracker(this));
   clock_converter.reset(new ClockConverter(this));
   mapping_tracker.reset(new MappingTracker(this));
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 7bba0e9..fb9b294 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -46,6 +46,7 @@
 #include "src/trace_processor/importers/android_bugreport/android_bugreport_parser.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_trace_tokenizer.h"
 #include "src/trace_processor/importers/gzip/gzip_trace_parser.h"
@@ -53,8 +54,8 @@
 #include "src/trace_processor/importers/json/json_trace_tokenizer.h"
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/ninja/ninja_log_parser.h"
-#include "src/trace_processor/importers/perf/perf_data_parser.h"
 #include "src/trace_processor/importers/perf/perf_data_tokenizer.h"
+#include "src/trace_processor/importers/perf/record_parser.h"
 #include "src/trace_processor/importers/proto/additional_modules.h"
 #include "src/trace_processor/importers/proto/content_analyzer.h"
 #include "src/trace_processor/importers/systrace/systrace_trace_parser.h"
@@ -110,6 +111,7 @@
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tp_metatrace.h"
 #include "src/trace_processor/trace_processor_storage_impl.h"
+#include "src/trace_processor/trace_reader_registry.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "src/trace_processor/types/variadic.h"
 #include "src/trace_processor/util/descriptors.h"
@@ -339,27 +341,34 @@
 
 TraceProcessorImpl::TraceProcessorImpl(const Config& cfg)
     : TraceProcessorStorageImpl(cfg), config_(cfg) {
-  context_.fuchsia_trace_tokenizer =
-      std::make_unique<FuchsiaTraceTokenizer>(&context_);
+  context_.reader_registry->RegisterTraceReader<FuchsiaTraceTokenizer>(
+      kFuchsiaTraceType);
   context_.fuchsia_record_parser =
       std::make_unique<FuchsiaTraceParser>(&context_);
-  context_.ninja_log_parser = std::make_unique<NinjaLogParser>(&context_);
-  context_.systrace_trace_parser =
-      std::make_unique<SystraceTraceParser>(&context_);
-  context_.perf_data_trace_tokenizer =
-      std::make_unique<perf_importer::PerfDataTokenizer>(&context_);
+
+  context_.reader_registry->RegisterTraceReader<SystraceTraceParser>(
+      kSystraceTraceType);
+  context_.reader_registry->RegisterTraceReader<NinjaLogParser>(
+      kNinjaLogTraceType);
+
+  context_.reader_registry
+      ->RegisterTraceReader<perf_importer::PerfDataTokenizer>(
+          kPerfDataTraceType);
   context_.perf_record_parser =
-      std::make_unique<perf_importer::PerfDataParser>(&context_);
+      std::make_unique<perf_importer::RecordParser>(&context_);
 
   if (util::IsGzipSupported()) {
-    context_.gzip_trace_parser = std::make_unique<GzipTraceParser>(&context_);
-    context_.android_bugreport_parser =
-        std::make_unique<AndroidBugreportParser>(&context_);
+    context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
+        kGzipTraceType);
+    context_.reader_registry->RegisterTraceReader<GzipTraceParser>(
+        kCtraceTraceType);
+    context_.reader_registry->RegisterTraceReader<AndroidBugreportParser>(
+        kAndroidBugreportTraceType);
   }
 
   if (json::IsJsonSupported()) {
-    context_.json_trace_tokenizer =
-        std::make_unique<JsonTraceTokenizer>(&context_);
+    context_.reader_registry->RegisterTraceReader<JsonTraceTokenizer>(
+        kJsonTraceType);
     context_.json_trace_parser =
         std::make_unique<JsonTraceParserImpl>(&context_);
   }
diff --git a/src/trace_processor/trace_processor_storage_impl.cc b/src/trace_processor/trace_processor_storage_impl.cc
index 3a686e8..243e3da 100644
--- a/src/trace_processor/trace_processor_storage_impl.cc
+++ b/src/trace_processor/trace_processor_storage_impl.cc
@@ -30,6 +30,7 @@
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/metadata_tracker.h"
+#include "src/trace_processor/importers/common/process_track_translation_table.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/sched_event_tracker.h"
 #include "src/trace_processor/importers/common/slice_tracker.h"
@@ -45,6 +46,7 @@
 #include "src/trace_processor/importers/proto/proto_trace_reader.h"
 #include "src/trace_processor/importers/proto/track_event.descriptor.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/trace_reader_registry.h"
 #include "src/trace_processor/util/descriptors.h"
 
 namespace perfetto {
@@ -52,6 +54,8 @@
 
 TraceProcessorStorageImpl::TraceProcessorStorageImpl(const Config& cfg)
     : context_({cfg, std::make_shared<TraceStorage>(cfg)}) {
+  context_.reader_registry->RegisterTraceReader<ProtoTraceReader>(
+      kProtoTraceType);
   context_.proto_trace_parser =
       std::make_unique<ProtoTraceParserImpl>(&context_);
   RegisterDefaultModules(&context_);
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
new file mode 100644
index 0000000..09c5c7b
--- /dev/null
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/trace_reader_registry.h"
+#include "perfetto/base/logging.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/gzip_utils.h"
+#include "src/trace_processor/util/trace_type.h"
+
+namespace perfetto::trace_processor {
+namespace {
+const char kNoZlibErr[] =
+    "Cannot open compressed trace. zlib not enabled in the build config";
+
+bool RequiresZlibSupport(TraceType type) {
+  switch (type) {
+    case kGzipTraceType:
+    case kCtraceTraceType:
+    case kAndroidBugreportTraceType:
+      return true;
+
+    case kNinjaLogTraceType:
+    case kSystraceTraceType:
+    case kPerfDataTraceType:
+    case kUnknownTraceType:
+    case kJsonTraceType:
+    case kFuchsiaTraceType:
+    case kProtoTraceType:
+      return false;
+  }
+  PERFETTO_FATAL("For GCC");
+}
+}  // namespace
+
+void TraceReaderRegistry::RegisterFactory(TraceType trace_type,
+                                          Factory factory) {
+  PERFETTO_CHECK(factories_.Insert(trace_type, std::move(factory)).second);
+}
+
+base::StatusOr<std::unique_ptr<ChunkedTraceReader>>
+TraceReaderRegistry::CreateTraceReader(TraceType type) {
+  if (auto it = factories_.Find(type); it) {
+    return (*it)(context_);
+  }
+
+  if (RequiresZlibSupport(type) && !util::IsGzipSupported()) {
+    return base::ErrStatus("%s support is disabled. %s", ToString(type),
+                           kNoZlibErr);
+  }
+
+  return base::ErrStatus("%s support is disabled", ToString(type));
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/trace_reader_registry.h b/src/trace_processor/trace_reader_registry.h
new file mode 100644
index 0000000..8e9b039
--- /dev/null
+++ b/src/trace_processor/trace_reader_registry.h
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_TRACE_READER_REGISTRY_H_
+#define SRC_TRACE_PROCESSOR_TRACE_READER_REGISTRY_H_
+
+#include <functional>
+#include <memory>
+#include <optional>
+
+#include "perfetto/ext/base/flat_hash_map.h"
+
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/util/trace_type.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class ChunkedTraceReader;
+class TraceProcessorContext;
+
+// Maps `TraceType` values to `ChunkedTraceReader` subclasses.
+// This class is used to create `ChunkedTraceReader` instances for a given
+// `TraceType`.
+class TraceReaderRegistry {
+ public:
+  explicit TraceReaderRegistry(TraceProcessorContext* context)
+      : context_(context) {}
+
+  // Registers a mapping from `TraceType` value to `ChunkedTraceReader`
+  // subclass. Only one such mapping can be registered per `TraceType` value.
+  template <typename Reader>
+  void RegisterTraceReader(TraceType trace_type) {
+    RegisterFactory(trace_type, [](TraceProcessorContext* ctxt) {
+      return std::make_unique<Reader>(ctxt);
+    });
+  }
+
+  // Creates a new `ChunkedTraceReader` instance for the given `type`. Returns
+  // an error if no mapping has been previously registered.
+  base::StatusOr<std::unique_ptr<ChunkedTraceReader>> CreateTraceReader(
+      TraceType type);
+
+ private:
+  using Factory = std::function<std::unique_ptr<ChunkedTraceReader>(
+      TraceProcessorContext*)>;
+  void RegisterFactory(TraceType trace_type, Factory factory);
+
+  TraceProcessorContext* const context_;
+  base::FlatHashMap<TraceType,
+                    std::function<std::unique_ptr<ChunkedTraceReader>(
+                        TraceProcessorContext*)>>
+      factories_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_TRACE_READER_REGISTRY_H_
diff --git a/src/trace_processor/types/BUILD.gn b/src/trace_processor/types/BUILD.gn
index 6b66c53..9fbff7a 100644
--- a/src/trace_processor/types/BUILD.gn
+++ b/src/trace_processor/types/BUILD.gn
@@ -32,6 +32,7 @@
     "../../../include/perfetto/trace_processor",
     "../containers",
     "../tables:tables_python",
+    "../util:trace_type",
   ]
 }
 
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 5a91d78..fd72005 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -23,24 +23,11 @@
 #include "perfetto/trace_processor/basic_types.h"
 #include "src/trace_processor/tables/metadata_tables_py.h"
 #include "src/trace_processor/types/destructible.h"
+#include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto {
 namespace trace_processor {
 
-enum TraceType {
-  kUnknownTraceType,
-  kProtoTraceType,
-  kJsonTraceType,
-  kFuchsiaTraceType,
-  kSystraceTraceType,
-  kGzipTraceType,
-  kCtraceTraceType,
-  kNinjaLogTraceType,
-  kAndroidBugreportTraceType,
-  kPerfDataTraceType,
-};
-
-class AndroidProbesTracker;
 class ArgsTracker;
 class ArgsTranslationTable;
 class AsyncTrackSetTracker;
@@ -66,12 +53,14 @@
 class PerfRecordParser;
 class PerfSampleTracker;
 class ProcessTracker;
+class ProcessTrackTranslationTable;
 class ProtoImporterModule;
 class ProtoTraceParser;
 class SchedEventTracker;
 class SliceTracker;
 class SliceTranslationTable;
 class StackProfileTracker;
+class TraceReaderRegistry;
 class TraceSorter;
 class TraceStorage;
 class TrackEventModule;
@@ -99,6 +88,8 @@
   // |storage| is shared among multiple contexts in multi-machine tracing.
   std::shared_ptr<TraceStorage> storage;
 
+  std::unique_ptr<TraceReaderRegistry> reader_registry;
+
   std::unique_ptr<ChunkedTraceReader> chunk_reader;
 
   // The sorter is used to sort trace data by timestamp and is shared among
@@ -118,6 +109,7 @@
   std::unique_ptr<SliceTranslationTable> slice_translation_table;
   std::unique_ptr<FlowTracker> flow_tracker;
   std::unique_ptr<ProcessTracker> process_tracker;
+  std::unique_ptr<ProcessTrackTranslationTable> process_track_translation_table;
   std::unique_ptr<EventTracker> event_tracker;
   std::unique_ptr<SchedEventTracker> sched_event_tracker;
   std::unique_ptr<ClockTracker> clock_tracker;
@@ -153,17 +145,6 @@
   std::unique_ptr<Destructible> jit_tracker;               // JitTracker
   // clang-format on
 
-  // These fields are trace readers which will be called by |forwarding_parser|
-  // once the format of the trace is discovered. They are placed here as they
-  // are only available in the lib target.
-  std::unique_ptr<ChunkedTraceReader> json_trace_tokenizer;
-  std::unique_ptr<ChunkedTraceReader> fuchsia_trace_tokenizer;
-  std::unique_ptr<ChunkedTraceReader> ninja_log_parser;
-  std::unique_ptr<ChunkedTraceReader> android_bugreport_parser;
-  std::unique_ptr<ChunkedTraceReader> systrace_trace_parser;
-  std::unique_ptr<ChunkedTraceReader> gzip_trace_parser;
-  std::unique_ptr<ChunkedTraceReader> perf_data_trace_tokenizer;
-
   std::unique_ptr<ProtoTraceParser> proto_trace_parser;
 
   // These fields are trace parsers which will be called by |forwarding_parser|
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index 5ca39a1..6c40069 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -266,6 +266,17 @@
   ]
 }
 
+source_set("trace_type") {
+  sources = [
+    "trace_type.cc",
+    "trace_type.h",
+  ]
+  deps = [
+    "../../../gn:default_deps",
+    "../../../include/perfetto/ext/base",
+  ]
+}
+
 source_set("unittests") {
   sources = [
     "bump_allocator_unittest.cc",
diff --git a/src/trace_processor/util/file_buffer.cc b/src/trace_processor/util/file_buffer.cc
index aa92348..1dbd10b 100644
--- a/src/trace_processor/util/file_buffer.cc
+++ b/src/trace_processor/util/file_buffer.cc
@@ -39,10 +39,11 @@
   end_offset_ += size;
 }
 
-bool FileBuffer::PopFrontBytesUntil(const size_t target_offset) {
+bool FileBuffer::PopFrontUntil(const size_t target_offset) {
+  PERFETTO_CHECK(file_offset() <= target_offset);
   while (!data_.empty()) {
     Entry& entry = data_.front();
-    if (target_offset <= entry.file_offset) {
+    if (target_offset == entry.file_offset) {
       return true;
     }
     const size_t bytes_to_pop = target_offset - entry.file_offset;
@@ -110,9 +111,8 @@
   auto it = std::upper_bound(
       data_.begin(), data_.end(), offset,
       [](size_t offset, const Entry& rhs) { return offset < rhs.file_offset; });
-  if (it == data_.begin()) {
-    return end();
-  }
+  // This can only happen if too much data was popped.
+  PERFETTO_CHECK(it != data_.begin());
   return std::prev(it);
 }
 
diff --git a/src/trace_processor/util/file_buffer.h b/src/trace_processor/util/file_buffer.h
index 07de20f..35e7384 100644
--- a/src/trace_processor/util/file_buffer.h
+++ b/src/trace_processor/util/file_buffer.h
@@ -39,6 +39,8 @@
   // Trivial empty ctor.
   FileBuffer() = default;
 
+  bool empty() const { return data_.empty(); }
+
   // Returns the offset to the start of the buffered window of data.
   size_t file_offset() const {
     return data_.empty() ? end_offset_ : data_.front().file_offset;
@@ -47,10 +49,19 @@
   // Adds a `TraceBlobView` at the back.
   void PushBack(TraceBlobView view);
 
-  // Shrinks the buffer by dropping bytes from the front of the buffer until the
+  // Shrinks the buffer by dropping data from the front of the buffer until the
   // given offset is reached. If not enough data is present as much data as
   // possible will be dropped and `false` will be returned.
-  bool PopFrontBytesUntil(size_t offset);
+  // ATTENTION: If `offset` < 'file_offset()' (i.e. you try to access data
+  // previously popped) this method will CHECK fail.
+  bool PopFrontUntil(size_t offset);
+
+  // Shrinks the buffer by dropping `bytes` from the front of the buffer. If not
+  // enough data is present as much data as possible will be dropped and `false`
+  // will be returned.
+  bool PopFrontBytes(size_t bytes) {
+    return PopFrontUntil(file_offset() + bytes);
+  }
 
   // Similar to `TraceBlobView::slice_off`, creates a slice with data starting
   // at `offset` and of the given `length`. This method might need to allocate a
@@ -58,8 +69,8 @@
   // TraceBlobView instances). If not enough data is present `std::nullopt` is
   // returned.
   //
-  // ATTENTION: If `offset` < 'file_offset()' this method will never return a
-  // value.
+  // ATTENTION: If `offset` < 'file_offset()' (i.e. you try to access data
+  // previously popped) this method will CHECK fail.
   std::optional<TraceBlobView> SliceOff(size_t offset, size_t length) const;
 
  private:
diff --git a/src/trace_processor/util/file_buffer_unittest.cc b/src/trace_processor/util/file_buffer_unittest.cc
index 418cf76..1518fe4 100644
--- a/src/trace_processor/util/file_buffer_unittest.cc
+++ b/src/trace_processor/util/file_buffer_unittest.cc
@@ -16,6 +16,7 @@
 
 #include "src/trace_processor/util/file_buffer.h"
 
+#include <algorithm>
 #include <cstddef>
 #include <cstdint>
 #include <cstring>
@@ -43,11 +44,9 @@
         : expected_data_(expected_data) {}
     bool MatchAndExplain(const ArgType& arg,
                          ::testing ::MatchResultListener*) const override {
-      if (expected_data_.size() != arg.size()) {
-        return false;
-      }
-      return memcmp(expected_data_.data(), arg.data(), expected_data_.size()) ==
-             0;
+      return std::equal(expected_data_.data(),
+                        expected_data_.data() + expected_data_.size(),
+                        arg.data(), arg.data() + arg.size());
     }
     void DescribeTo(::std ::ostream*) const override {}
     void DescribeNegationTo(::std ::ostream*) const override {}
@@ -108,7 +107,7 @@
   FileBuffer buffer = CreateFileBuffer(Slice(expected_data, kChunkSize));
 
   for (size_t file_offset = 0; file_offset <= kExpectedSize; ++file_offset) {
-    EXPECT_TRUE(buffer.PopFrontBytesUntil(file_offset));
+    EXPECT_TRUE(buffer.PopFrontUntil(file_offset));
     for (size_t off = file_offset; off <= kExpectedSize; ++off) {
       auto expected = expected_data.slice_off(off, kExpectedSize - off);
       std::optional<TraceBlobView> tbv = buffer.SliceOff(off, expected.size());
@@ -143,18 +142,16 @@
 
   --expected_size;
   ++expected_file_offset;
-  buffer.PopFrontBytesUntil(expected_file_offset);
+  buffer.PopFrontUntil(expected_file_offset);
   EXPECT_THAT(buffer.file_offset(), Eq(expected_file_offset));
-  EXPECT_THAT(buffer.SliceOff(expected_file_offset - 1, 1), Eq(std::nullopt));
   EXPECT_THAT(buffer.SliceOff(expected_file_offset, expected_size),
               Optional(SameDataAs(expected_data.slice_off(
                   expected_data.size() - expected_size, expected_size))));
 
   expected_size -= kChunkSize;
   expected_file_offset += kChunkSize;
-  buffer.PopFrontBytesUntil(expected_file_offset);
+  buffer.PopFrontUntil(expected_file_offset);
   EXPECT_THAT(buffer.file_offset(), Eq(expected_file_offset));
-  EXPECT_THAT(buffer.SliceOff(expected_file_offset - 1, 1), Eq(std::nullopt));
   EXPECT_THAT(buffer.SliceOff(expected_file_offset, expected_size),
               Optional(SameDataAs(expected_data.slice_off(
                   expected_data.size() - expected_size, expected_size))));
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
new file mode 100644
index 0000000..d265b90
--- /dev/null
+++ b/src/trace_processor/util/trace_type.cc
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/util/trace_type.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstdint>
+#include <string>
+
+#include "perfetto/base/logging.h"
+#include "perfetto/ext/base/string_utils.h"
+
+namespace perfetto::trace_processor {
+namespace {
+// Fuchsia traces have a magic number as documented here:
+// https://fuchsia.googlesource.com/fuchsia/+/HEAD/docs/development/tracing/trace-format/README.md#magic-number-record-trace-info-type-0
+constexpr uint64_t kFuchsiaMagicNumber = 0x0016547846040010;
+constexpr char kPerfMagic[] = "PERFILE2";
+
+inline bool isspace(unsigned char c) {
+  return ::isspace(c);
+}
+
+std::string RemoveWhitespace(std::string str) {
+  str.erase(std::remove_if(str.begin(), str.end(), isspace), str.end());
+  return str;
+}
+
+}  // namespace
+
+const char* ToString(TraceType trace_type) {
+  switch (trace_type) {
+    case kJsonTraceType:
+      return "JSON trace";
+    case kProtoTraceType:
+      return "proto trace";
+    case kNinjaLogTraceType:
+      return "ninja log";
+    case kFuchsiaTraceType:
+      return "fuchsia trace";
+    case kSystraceTraceType:
+      return "systrace trace";
+    case kGzipTraceType:
+      return "gzip trace";
+    case kCtraceTraceType:
+      return "ctrace trace";
+    case kAndroidBugreportTraceType:
+      return "Android Bugreport";
+    case kPerfDataTraceType:
+      return "perf data";
+    case kUnknownTraceType:
+      return "unknown trace";
+  }
+  PERFETTO_FATAL("For GCC");
+}
+
+TraceType GuessTraceType(const uint8_t* data, size_t size) {
+  if (size == 0)
+    return kUnknownTraceType;
+  std::string start(reinterpret_cast<const char*>(data),
+                    std::min<size_t>(size, kGuessTraceMaxLookahead));
+  if (size >= 8) {
+    uint64_t first_word;
+    memcpy(&first_word, data, sizeof(first_word));
+    if (first_word == kFuchsiaMagicNumber)
+      return kFuchsiaTraceType;
+  }
+  if (base::StartsWith(start, kPerfMagic)) {
+    return kPerfDataTraceType;
+  }
+  std::string start_minus_white_space = RemoveWhitespace(start);
+  if (base::StartsWith(start_minus_white_space, "{\""))
+    return kJsonTraceType;
+  if (base::StartsWith(start_minus_white_space, "[{\""))
+    return kJsonTraceType;
+
+  // Systrace with header but no leading HTML.
+  if (base::Contains(start, "# tracer"))
+    return kSystraceTraceType;
+
+  // Systrace with leading HTML.
+  // Both: <!DOCTYPE html> and <!DOCTYPE HTML> have been observed.
+  std::string lower_start = base::ToLower(start);
+  if (base::StartsWith(lower_start, "<!doctype html>") ||
+      base::StartsWith(lower_start, "<html>"))
+    return kSystraceTraceType;
+
+  // Traces obtained from atrace -z (compress).
+  // They all have the string "TRACE:" followed by 78 9C which is a zlib header
+  // for "deflate, default compression, window size=32K" (see b/208691037)
+  if (base::Contains(start, "TRACE:\n\x78\x9c"))
+    return kCtraceTraceType;
+
+  // Traces obtained from atrace without -z (no compression).
+  if (base::Contains(start, "TRACE:\n"))
+    return kSystraceTraceType;
+
+  // Ninja's build log (.ninja_log).
+  if (base::StartsWith(start, "# ninja log"))
+    return kNinjaLogTraceType;
+
+  // Systrace with no header or leading HTML.
+  if (base::StartsWith(start, " "))
+    return kSystraceTraceType;
+
+  // gzip'ed trace containing one of the other formats.
+  if (base::StartsWith(start, "\x1f\x8b"))
+    return kGzipTraceType;
+
+  if (base::StartsWith(start, "\x0a"))
+    return kProtoTraceType;
+
+  // Android bugreport.zip
+  // TODO(primiano). For now we assume any .zip file is a bugreport. In future,
+  // if we want to support different trace formats based on a .zip arachive we
+  // will need an extra layer similar to what we did kGzipTraceType.
+  if (base::StartsWith(start, "PK\x03\x04"))
+    return kAndroidBugreportTraceType;
+
+  return kUnknownTraceType;
+}
+
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
new file mode 100644
index 0000000..fec7a74
--- /dev/null
+++ b/src/trace_processor/util/trace_type.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_UTIL_TRACE_TYPE_H_
+#define SRC_TRACE_PROCESSOR_UTIL_TRACE_TYPE_H_
+
+#include <cstddef>
+#include <cstdint>
+
+namespace perfetto::trace_processor {
+
+enum TraceType {
+  kUnknownTraceType,
+  kProtoTraceType,
+  kJsonTraceType,
+  kFuchsiaTraceType,
+  kSystraceTraceType,
+  kGzipTraceType,
+  kCtraceTraceType,
+  kNinjaLogTraceType,
+  kAndroidBugreportTraceType,
+  kPerfDataTraceType,
+};
+
+constexpr size_t kGuessTraceMaxLookahead = 64;
+TraceType GuessTraceType(const uint8_t* data, size_t size);
+const char* ToString(TraceType type);
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_UTIL_TRACE_TYPE_H_
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index eec75fd..7014c0d 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -79,10 +79,11 @@
   redactor.emplace_transform<PrunePackageList>();
   redactor.emplace_transform<ScrubProcessStats>();
 
+  auto* comms_harness = redactor.emplace_transform<RedactSchedSwitchHarness>();
+  comms_harness->emplace_transform<ClearComms>();
+
   auto* redact_ftrace_events = redactor.emplace_transform<RedactFtraceEvent>();
   redact_ftrace_events
-      ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
-  redact_ftrace_events
       ->emplace_back<RedactTaskNewTask::kFieldId, RedactTaskNewTask>();
   redact_ftrace_events
       ->emplace_back<RedactProcessFree::kFieldId, RedactProcessFree>();
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
index 83cb36f..87412c1 100644
--- a/src/trace_redaction/redact_sched_switch.cc
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -16,6 +16,8 @@
 
 #include "src/trace_redaction/redact_sched_switch.h"
 
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_processor/util/status_macros.h"
 #include "src/trace_redaction/proto_util.h"
 
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
@@ -25,19 +27,12 @@
 namespace perfetto::trace_redaction {
 
 namespace {
-
-// TODO(vaage): Merge with RedactComm in redact_task_newtask.cc.
-protozero::ConstChars RedactComm(const Context& context,
-                                 uint64_t ts,
-                                 int32_t pid,
-                                 protozero::ConstChars comm) {
-  if (context.timeline->PidConnectsToUid(ts, pid, *context.package_uid)) {
-    return comm;
-  }
-
-  return {};
+// TODO(vaage): While simple, this function saves us from declaring the sample
+// lambda each time we use the has_fields pattern. Once its usage increases, and
+// its value is obvious, remove this comment.
+bool IsTrue(bool value) {
+  return value;
 }
-
 }  // namespace
 
 // Redact sched switch trace events in an ftrace event bundle:
@@ -63,71 +58,170 @@
 // collection of ftrace event messages) because data in a sched_switch message
 // is needed in order to know if the event should be added to the bundle.
 
-base::Status RedactSchedSwitch::Redact(
-    const Context& context,
-    const protos::pbzero::FtraceEventBundle::Decoder&,
-    protozero::ProtoDecoder& event,
-    protos::pbzero::FtraceEvent* event_message) const {
-  if (!context.package_uid.has_value()) {
-    return base::ErrStatus("RedactSchedSwitch: missing package uid");
-  }
+SchedSwitchTransform::~SchedSwitchTransform() = default;
 
-  if (!context.timeline) {
-    return base::ErrStatus("RedactSchedSwitch: missing timeline");
-  }
+base::Status RedactSchedSwitchHarness::Transform(const Context& context,
+                                                 std::string* packet) const {
+  protozero::HeapBuffered<protos::pbzero::TracePacket> message;
+  protozero::ProtoDecoder decoder(*packet);
 
-  // The timestamp is needed to do the timeline look-up. If the packet has no
-  // timestamp, don't add the sched switch event. This is the safest option.
-  auto timestamp =
-      event.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
-  if (!timestamp.valid()) {
-    return base::OkStatus();
-  }
-
-  auto sched_switch =
-      event.FindField(protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
-  if (!sched_switch.valid()) {
-    return base::ErrStatus(
-        "RedactSchedSwitch: was used for unsupported field type");
-  }
-
-  protozero::ProtoDecoder sched_switch_decoder(sched_switch.as_bytes());
-
-  auto prev_pid = sched_switch_decoder.FindField(
-      protos::pbzero::SchedSwitchFtraceEvent::kPrevPidFieldNumber);
-  auto next_pid = sched_switch_decoder.FindField(
-      protos::pbzero::SchedSwitchFtraceEvent::kNextPidFieldNumber);
-
-  // There must be a prev pid and a next pid. Otherwise, the event is invalid.
-  // Dropping the event is the safest option.
-  if (!prev_pid.valid() || !next_pid.valid()) {
-    return base::OkStatus();
-  }
-
-  // Avoid making the message until we know that we have prev and next pids.
-  auto sched_switch_message = event_message->set_sched_switch();
-
-  for (auto field = sched_switch_decoder.ReadField(); field.valid();
-       field = sched_switch_decoder.ReadField()) {
-    switch (field.id()) {
-      case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber:
-        sched_switch_message->set_next_comm(
-            RedactComm(context, timestamp.as_uint64(), next_pid.as_int32(),
-                       field.as_string()));
-        break;
-
-      case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
-        sched_switch_message->set_prev_comm(
-            RedactComm(context, timestamp.as_uint64(), prev_pid.as_int32(),
-                       field.as_string()));
-        break;
-
-      default:
-        proto_util::AppendField(field, sched_switch_message);
-        break;
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      RETURN_IF_ERROR(
+          TransformFtraceEvents(context, field, message->set_ftrace_events()));
+    } else {
+      proto_util::AppendField(field, message.get());
     }
   }
 
+  packet->assign(message.SerializeAsString());
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvents(
+    const Context& context,
+    protozero::Field ftrace_events,
+    protos::pbzero::FtraceEventBundle* message) const {
+  PERFETTO_DCHECK(ftrace_events.id() ==
+                  protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+  protozero::ProtoDecoder decoder(ftrace_events.as_bytes());
+
+  auto cpu =
+      decoder.FindField(protos::pbzero::FtraceEventBundle::kCpuFieldNumber);
+  if (!cpu.valid()) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing cpu in ftrace event bundle.");
+  }
+
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+      RETURN_IF_ERROR(TransformFtraceEvent(context, cpu.as_int32(), field,
+                                           message->add_event()));
+      continue;
+    }
+
+    if (field.id() ==
+        protos::pbzero::FtraceEventBundle::kCompactSchedFieldNumber) {
+      // TODO(vaage): Replace this with logic specific to the comp sched data
+      // type.
+      proto_util::AppendField(field, message);
+      continue;
+    }
+
+    proto_util::AppendField(field, message);
+  }
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvent(
+    const Context& context,
+    int32_t cpu,
+    protozero::Field ftrace_event,
+    protos::pbzero::FtraceEvent* message) const {
+  PERFETTO_DCHECK(ftrace_event.id() ==
+                  protos::pbzero::FtraceEventBundle::kEventFieldNumber);
+
+  protozero::ProtoDecoder decoder(ftrace_event.as_bytes());
+
+  auto ts =
+      decoder.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+  if (!ts.valid()) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing timestamp in ftrace event.");
+  }
+
+  std::string scratch_str;
+
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+          field.as_bytes());
+      RETURN_IF_ERROR(TransformFtraceEventSchedSwitch(
+          context, ts.as_uint64(), cpu, sched_switch, &scratch_str,
+          message->set_sched_switch()));
+    } else {
+      proto_util::AppendField(field, message);
+    }
+  }
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEventSchedSwitch(
+    const Context& context,
+    uint64_t ts,
+    int32_t cpu,
+    protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+    std::string* scratch_str,
+    protos::pbzero::SchedSwitchFtraceEvent* message) const {
+  auto has_fields = {
+      sched_switch.has_prev_comm(), sched_switch.has_prev_pid(),
+      sched_switch.has_prev_prio(), sched_switch.has_prev_state(),
+      sched_switch.has_next_comm(), sched_switch.has_next_pid(),
+      sched_switch.has_next_prio()};
+
+  if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing required SchedSwitchFtraceEvent "
+        "field.");
+  }
+
+  auto prev_pid = sched_switch.prev_pid();
+  auto prev_comm = sched_switch.prev_comm();
+
+  auto next_pid = sched_switch.next_pid();
+  auto next_comm = sched_switch.next_comm();
+
+  // There are 7 values in a sched switch message. Since 4 of the 7 can be
+  // replaced, it is easier/cleaner to go value-by-value. Go in proto-defined
+  // order.
+
+  scratch_str->assign(prev_comm.data, prev_comm.size);
+
+  for (const auto& transform : transforms_) {
+    RETURN_IF_ERROR(
+        transform->Transform(context, ts, cpu, &prev_pid, scratch_str));
+  }
+
+  message->set_prev_comm(*scratch_str);                // FieldNumber = 1
+  message->set_prev_pid(prev_pid);                     // FieldNumber = 2
+  message->set_prev_prio(sched_switch.prev_prio());    // FieldNumber = 3
+  message->set_prev_state(sched_switch.prev_state());  // FieldNumber = 4
+
+  scratch_str->assign(next_comm.data, next_comm.size);
+
+  for (const auto& transform : transforms_) {
+    RETURN_IF_ERROR(
+        transform->Transform(context, ts, cpu, &next_pid, scratch_str));
+  }
+
+  message->set_next_comm(*scratch_str);              // FieldNumber = 5
+  message->set_next_pid(next_pid);                   // FieldNumber = 6
+  message->set_next_prio(sched_switch.next_prio());  // FieldNumber = 7
+
+  return base::OkStatus();
+}
+
+// Switch event transformation: Clear the comm value if the thread/process is
+// not part of the target packet.
+base::Status ClearComms::Transform(const Context& context,
+                                   uint64_t ts,
+                                   int32_t,
+                                   int32_t* pid,
+                                   std::string* comm) const {
+  PERFETTO_DCHECK(pid);
+  PERFETTO_DCHECK(comm);
+
+  if (!context.timeline->PidConnectsToUid(ts, *pid, *context.package_uid)) {
+    comm->clear();
+  }
+
   return base::OkStatus();
 }
 
diff --git a/src/trace_redaction/redact_sched_switch.h b/src/trace_redaction/redact_sched_switch.h
index bcfa30d..55f9a75 100644
--- a/src/trace_redaction/redact_sched_switch.h
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -17,23 +17,63 @@
 #ifndef SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
 #define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
 
-#include "src/trace_redaction/redact_ftrace_event.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 namespace perfetto::trace_redaction {
 
-// Goes through ftrace events and conditonally removes the comm values from
-// sched switch events.
-class RedactSchedSwitch : public FtraceEventRedaction {
+class SchedSwitchTransform {
  public:
-  static constexpr auto kFieldId =
-      protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber;
+  virtual ~SchedSwitchTransform();
+  virtual base::Status Transform(const Context& context,
+                                 uint64_t ts,
+                                 int32_t cpu,
+                                 int32_t* pid,
+                                 std::string* comm) const = 0;
+};
 
-  base::Status Redact(
+// Goes through all sched switch events are modifies them.
+class RedactSchedSwitchHarness : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+
+  template <class Transform>
+  void emplace_transform() {
+    transforms_.emplace_back(new Transform());
+  }
+
+ private:
+  base::Status TransformFtraceEvents(
       const Context& context,
-      const protos::pbzero::FtraceEventBundle::Decoder& bundle,
-      protozero::ProtoDecoder& event,
-      protos::pbzero::FtraceEvent* event_message) const override;
+      protozero::Field ftrace_events,
+      protos::pbzero::FtraceEventBundle* message) const;
+
+  base::Status TransformFtraceEvent(const Context& context,
+                                    int32_t cpu,
+                                    protozero::Field ftrace_event,
+                                    protos::pbzero::FtraceEvent* message) const;
+
+  // scratch_str is a reusable string, allowing comm modifications to be done in
+  // a shared buffer, avoiding allocations when processing ftrace events.
+  base::Status TransformFtraceEventSchedSwitch(
+      const Context& context,
+      uint64_t ts,
+      int32_t cpu,
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+      std::string* scratch_str,
+      protos::pbzero::SchedSwitchFtraceEvent* message) const;
+
+  std::vector<std::unique_ptr<SchedSwitchTransform>> transforms_;
+};
+
+class ClearComms : public SchedSwitchTransform {
+  base::Status Transform(const Context& context,
+                         uint64_t ts,
+                         int32_t cpu,
+                         int32_t* pid,
+                         std::string* comm) const override;
 };
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
index f14d6e9..247d44b 100644
--- a/src/trace_redaction/redact_sched_switch_integrationtest.cc
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -16,9 +16,9 @@
 
 #include <cstdint>
 #include <string>
+#include <unordered_map>
 
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/base/test/status_matchers.h"
 #include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/find_package_uid.h"
@@ -36,23 +36,6 @@
 
 namespace perfetto::trace_redaction {
 
-class RedactSchedSwitchIntegrationTest
-    : public testing::Test,
-      protected TraceRedactionIntegrationFixure {
- protected:
-  void SetUp() override {
-    trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<CollectTimelineEvents>();
-
-    auto* ftrace_event_redactions =
-        trace_redactor()->emplace_transform<RedactFtraceEvent>();
-    ftrace_event_redactions
-        ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
-
-    context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
-  }
-};
-
 // >>> SELECT uid
 // >>>   FROM package_list
 // >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
@@ -103,6 +86,35 @@
 //     | 7950 | UnityGfxDeviceW |
 //     | 7969 | UnityGfxDeviceW |
 //     +------+-----------------+
+class RedactSchedSwitchIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
+ protected:
+  void SetUp() override {
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
+
+    auto* harness =
+        trace_redactor()->emplace_transform<RedactSchedSwitchHarness>();
+    harness->emplace_transform<ClearComms>();
+
+    context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
+  }
+
+  std::unordered_map<int32_t, std::string> expected_names_ = {
+      {7120, "Binder:7105_2"},   {7127, "UnityMain"},
+      {7142, "Job.worker 0"},    {7143, "Job.worker 1"},
+      {7144, "Job.worker 2"},    {7145, "Job.worker 3"},
+      {7146, "Job.worker 4"},    {7147, "Job.worker 5"},
+      {7148, "Job.worker 6"},    {7150, "Background Job."},
+      {7151, "Background Job."}, {7167, "UnityGfxDeviceW"},
+      {7172, "AudioTrack"},      {7174, "FMOD stream thr"},
+      {7180, "Binder:7105_3"},   {7184, "UnityChoreograp"},
+      {7945, "Filter0"},         {7946, "Filter1"},
+      {7947, "Thread-7"},        {7948, "FMOD mixer thre"},
+      {7950, "UnityGfxDeviceW"}, {7969, "UnityGfxDeviceW"},
+  };
+};
 
 TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
   auto result = Redact();
@@ -114,30 +126,6 @@
   auto redacted = LoadRedacted();
   ASSERT_OK(redacted) << redacted.status().c_message();
 
-  base::FlatHashMap<int32_t, std::string> expected_names;
-  expected_names.Insert(7120, "Binder:7105_2");
-  expected_names.Insert(7127, "UnityMain");
-  expected_names.Insert(7142, "Job.worker 0");
-  expected_names.Insert(7143, "Job.worker 1");
-  expected_names.Insert(7144, "Job.worker 2");
-  expected_names.Insert(7145, "Job.worker 3");
-  expected_names.Insert(7146, "Job.worker 4");
-  expected_names.Insert(7147, "Job.worker 5");
-  expected_names.Insert(7148, "Job.worker 6");
-  expected_names.Insert(7150, "Background Job.");
-  expected_names.Insert(7151, "Background Job.");
-  expected_names.Insert(7167, "UnityGfxDeviceW");
-  expected_names.Insert(7172, "AudioTrack");
-  expected_names.Insert(7174, "FMOD stream thr");
-  expected_names.Insert(7180, "Binder:7105_3");
-  expected_names.Insert(7184, "UnityChoreograp");
-  expected_names.Insert(7945, "Filter0");
-  expected_names.Insert(7946, "Filter1");
-  expected_names.Insert(7947, "Thread-7");
-  expected_names.Insert(7948, "FMOD mixer thre");
-  expected_names.Insert(7950, "UnityGfxDeviceW");
-  expected_names.Insert(7969, "UnityGfxDeviceW");
-
   auto redacted_trace_data = LoadRedacted();
   ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
 
@@ -164,29 +152,29 @@
           event_decoder.sched_switch());
 
       ASSERT_TRUE(sched_decoder.has_next_pid());
-      ASSERT_TRUE(sched_decoder.has_prev_pid());
-
-      auto next_pid = sched_decoder.next_pid();
-      auto prev_pid = sched_decoder.prev_pid();
+      ASSERT_TRUE(sched_decoder.has_next_comm());
 
       // If the pid is expected, make sure it has the right now. If it is not
       // expected, it should be missing.
-      const auto* next_comm = expected_names.Find(next_pid);
-      const auto* prev_comm = expected_names.Find(prev_pid);
+      auto next_pid = sched_decoder.next_pid();
+      auto next_comm = expected_names_.find(next_pid);
 
-      EXPECT_TRUE(sched_decoder.has_next_comm());
-      EXPECT_TRUE(sched_decoder.has_prev_comm());
-
-      if (next_comm) {
-        EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+      if (next_comm == expected_names_.end()) {
+        ASSERT_EQ(sched_decoder.next_comm().size, 0u);
       } else {
-        EXPECT_EQ(sched_decoder.next_comm().size, 0u);
+        ASSERT_EQ(sched_decoder.next_comm().ToStdString(), next_comm->second);
       }
 
-      if (prev_comm) {
-        EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+      ASSERT_TRUE(sched_decoder.has_prev_pid());
+      ASSERT_TRUE(sched_decoder.has_prev_comm());
+
+      auto prev_pid = sched_decoder.prev_pid();
+      auto prev_comm = expected_names_.find(prev_pid);
+
+      if (prev_comm == expected_names_.end()) {
+        ASSERT_EQ(sched_decoder.prev_comm().size, 0u);
       } else {
-        EXPECT_EQ(sched_decoder.prev_comm().size, 0u);
+        ASSERT_EQ(sched_decoder.prev_comm().ToStdString(), prev_comm->second);
       }
     }
   }
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
index 72b65f4..88280d7 100644
--- a/src/trace_redaction/redact_sched_switch_unittest.cc
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -15,7 +15,6 @@
  */
 
 #include "src/trace_redaction/redact_sched_switch.h"
-#include "perfetto/protozero/scattered_heap_buffer.h"
 #include "src/base/test/status_matchers.h"
 #include "test/gtest_and_gmock.h"
 
@@ -28,6 +27,7 @@
 namespace perfetto::trace_redaction {
 
 namespace {
+
 constexpr uint64_t kUidA = 1;
 constexpr uint64_t kUidB = 2;
 constexpr uint64_t kUidC = 3;
@@ -36,167 +36,130 @@
 constexpr int32_t kPidA = 11;
 constexpr int32_t kPidB = 12;
 
-constexpr std::string_view kCommA = "comm-a";
-constexpr std::string_view kCommB = "comm-b";
+constexpr int32_t kCpuA = 0;
+
+constexpr uint64_t kFullStep = 1000;
+constexpr uint64_t kTimeA = 0;
+constexpr uint64_t kTimeB = kFullStep;
+
+constexpr auto kCommA = "comm-a";
+constexpr auto kCommB = "comm-b";
+constexpr auto kCommNone = "";
 
 }  // namespace
 
-// Tests which nested messages and fields are removed.
 class RedactSchedSwitchTest : public testing::Test {
  protected:
   void SetUp() override {
-    auto* event = bundle_.add_event();
+    // Create a packet where two pids are swapping back-and-forth.
+    auto* bundle = packet_.mutable_ftrace_events();
+    bundle->set_cpu(kCpuA);
 
-    event->set_timestamp(123456789);
-    event->set_pid(kPidA);
+    {
+      auto* event = bundle->add_event();
 
-    auto* sched_switch = event->mutable_sched_switch();
-    sched_switch->set_prev_comm(std::string(kCommA));
-    sched_switch->set_prev_pid(kPidA);
-    sched_switch->set_next_comm(std::string(kCommB));
-    sched_switch->set_next_pid(kPidB);
+      event->set_timestamp(kTimeA);
+      event->set_pid(kPidA);
+
+      auto* sched_switch = event->mutable_sched_switch();
+      sched_switch->set_prev_comm(kCommA);
+      sched_switch->set_prev_pid(kPidA);
+      sched_switch->set_prev_prio(0);
+      sched_switch->set_prev_state(0);
+      sched_switch->set_next_comm(kCommB);
+      sched_switch->set_next_pid(kPidB);
+      sched_switch->set_next_prio(0);
+    }
+
+    {
+      auto* event = bundle->add_event();
+
+      event->set_timestamp(kTimeB);
+      event->set_pid(kPidB);
+
+      auto* sched_switch = event->mutable_sched_switch();
+      sched_switch->set_prev_comm(kCommB);
+      sched_switch->set_prev_pid(kPidB);
+      sched_switch->set_prev_prio(0);
+      sched_switch->set_prev_state(0);
+      sched_switch->set_next_comm(kCommA);
+      sched_switch->set_next_pid(kPidA);
+      sched_switch->set_next_prio(0);
+    }
+
+    // PID A and PID B need to be attached to different packages (UID) so that
+    // its possible to include one but not the other.
+    context_.timeline = std::make_unique<ProcessThreadTimeline>();
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(kTimeA, kPidA, kNoParent, kUidA));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(kTimeA, kPidB, kNoParent, kUidB));
+    context_.timeline->Sort();
   }
 
-  base::Status Redact(const Context& context,
-                      protos::pbzero::FtraceEvent* event_message) {
-    RedactSchedSwitch redact;
-
-    auto bundle_str = bundle_.SerializeAsString();
-    protos::pbzero::FtraceEventBundle::Decoder bundle_decoder(bundle_str);
-
-    auto event_str = bundle_.event().back().SerializeAsString();
-    protos::pbzero::FtraceEvent::Decoder event_decoder(event_str);
-
-    return redact.Redact(context, bundle_decoder, event_decoder, event_message);
-  }
-
-  const std::string& event_string() const { return event_string_; }
-
-  // This test breaks the rules for task_newtask and the timeline. The
-  // timeline will report the task existing before the new task event. This
-  // should not happen in the field, but it makes the test more robust.
-  std::unique_ptr<ProcessThreadTimeline> CreatePopulatedTimeline() {
-    auto timeline = std::make_unique<ProcessThreadTimeline>();
-
-    timeline->Append(
-        ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
-    timeline->Append(
-        ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
-    timeline->Sort();
-
-    return timeline;
-  }
-
- private:
-  std::string event_string_;
-
-  std::unique_ptr<ProcessThreadTimeline> timeline_;
-
-  protos::gen::FtraceEventBundle bundle_;
+  protos::gen::TracePacket packet_;
+  Context context_;
 };
 
-TEST_F(RedactSchedSwitchTest, RejectMissingPackageUid) {
-  RedactSchedSwitch redact;
+// In this case, the target uid will be UID A. That means the comm values for
+// PID B should be removed, and the comm values for PID A should remain.
+TEST_F(RedactSchedSwitchTest, KeepsTargetCommValues) {
+  RedactSchedSwitchHarness redact;
+  redact.emplace_transform<ClearComms>();
 
-  Context context;
-  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context_.package_uid = kUidA;
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_FALSE(result.ok());
+  auto packet_buffer = packet_.SerializeAsString();
+
+  ASSERT_OK(redact.Transform(context_, &packet_buffer));
+
+  protos::gen::TracePacket packet;
+  ASSERT_TRUE(packet.ParseFromString(packet_buffer));
+
+  const auto& bundle = packet.ftrace_events();
+  const auto& events = bundle.event();
+
+  ASSERT_EQ(events.size(), 2u);
+
+  ASSERT_EQ(events[0].sched_switch().prev_pid(), kPidA);
+  ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommA);
+
+  ASSERT_EQ(events[0].sched_switch().next_pid(), kPidB);
+  ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
+
+  ASSERT_EQ(events[1].sched_switch().prev_pid(), kPidB);
+  ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+
+  ASSERT_EQ(events[1].sched_switch().next_pid(), kPidA);
+  ASSERT_EQ(events[1].sched_switch().next_comm(), kCommA);
 }
 
-TEST_F(RedactSchedSwitchTest, RejectMissingTimeline) {
-  RedactSchedSwitch redact;
+// This case is very similar to the "some are connected", expect that it
+// verifies all comm values will be removed when testing against an unused
+// uid.
+TEST_F(RedactSchedSwitchTest, RemovesAllCommsIfPackageDoesntExist) {
+  RedactSchedSwitchHarness redact;
+  redact.emplace_transform<ClearComms>();
 
-  Context context;
-  context.package_uid = kUidA;
+  context_.package_uid = kUidC;
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_FALSE(result.ok());
-}
+  auto packet_buffer = packet_.SerializeAsString();
 
-TEST_F(RedactSchedSwitchTest, ReplacePrevAndNextWithEmptyStrings) {
-  RedactSchedSwitch redact;
+  ASSERT_OK(redact.Transform(context_, &packet_buffer));
 
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
+  protos::gen::TracePacket packet;
+  ASSERT_TRUE(packet.ParseFromString(packet_buffer));
 
-  // Neither pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidC;
+  const auto& bundle = packet.ftrace_events();
+  const auto& events = bundle.event();
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_OK(result) << result.c_message();
+  ASSERT_EQ(events.size(), 2u);
 
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
+  ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommNone);
+  ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
 
-  ASSERT_TRUE(event.has_sched_switch());
-
-  // Cleared prev and next comm.
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_TRUE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplacePrevWithEmptyStrings) {
-  RedactSchedSwitch redact;
-
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
-
-  // Only next pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidB;
-
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-
-  ASSERT_OK(result) << result.c_message();
-
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
-
-  ASSERT_TRUE(event.has_sched_switch());
-
-  // Only cleared the prev comm.
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_FALSE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplaceNextWithEmptyStrings) {
-  RedactSchedSwitch redact;
-
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
-
-  // Only prev pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidA;
-
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_OK(result) << result.c_message();
-
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
-
-  ASSERT_TRUE(event.has_sched_switch());
-
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_FALSE(event.sched_switch().prev_comm().empty());
-
-  // Only cleared the next comm.
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_TRUE(event.sched_switch().next_comm().empty());
+  ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+  ASSERT_EQ(events[1].sched_switch().next_comm(), kCommNone);
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/traceconv/BUILD.gn b/src/traceconv/BUILD.gn
index 8cde4f4..22c02a6 100644
--- a/src/traceconv/BUILD.gn
+++ b/src/traceconv/BUILD.gn
@@ -91,6 +91,7 @@
     "../../src/trace_processor/util:descriptors",
     "../../src/trace_processor/util:gzip",
     "../../src/trace_processor/util:protozero_to_text",
+    "../../src/trace_processor/util:trace_type",
   ]
   sources = [
     "deobfuscate_profile.cc",
diff --git a/src/traceconv/trace_to_text.cc b/src/traceconv/trace_to_text.cc
index ef7e03a..3fc82fe 100644
--- a/src/traceconv/trace_to_text.cc
+++ b/src/traceconv/trace_to_text.cc
@@ -26,10 +26,10 @@
 #include "protos/perfetto/trace/trace.pbzero.h"
 #include "protos/perfetto/trace/trace_packet.pbzero.h"
 
-#include "src/trace_processor/forwarding_trace_parser.h"
 #include "src/trace_processor/util/descriptors.h"
 #include "src/trace_processor/util/gzip_utils.h"
 #include "src/trace_processor/util/protozero_to_text.h"
+#include "src/trace_processor/util/trace_type.h"
 
 namespace perfetto {
 namespace trace_to_text {
diff --git a/src/traced/probes/ps/process_stats_data_source.cc b/src/traced/probes/ps/process_stats_data_source.cc
index 9a27d29..91c72a9 100644
--- a/src/traced/probes/ps/process_stats_data_source.cc
+++ b/src/traced/probes/ps/process_stats_data_source.cc
@@ -720,6 +720,12 @@
           GetOrCreateStatsProcess(pid)->set_smr_pss_shmem_kb(counter);
           cached.smr_pss_shmem_kb = counter;
         }
+      } else if (strcmp(key.data(), "SwapPss") == 0) {
+        auto counter = ToUInt32(value.data());
+        if (counter != cached.smr_swap_pss_kb) {
+          GetOrCreateStatsProcess(pid)->set_smr_swap_pss_kb(counter);
+          cached.smr_swap_pss_kb = counter;
+        }
       }
 
       key.clear();
diff --git a/src/traced/probes/ps/process_stats_data_source.h b/src/traced/probes/ps/process_stats_data_source.h
index 9ec28bc..0224dd1 100644
--- a/src/traced/probes/ps/process_stats_data_source.h
+++ b/src/traced/probes/ps/process_stats_data_source.h
@@ -90,6 +90,7 @@
     uint32_t smr_pss_anon_kb = std::numeric_limits<uint32_t>::max();
     uint32_t smr_pss_file_kb = std::numeric_limits<uint32_t>::max();
     uint32_t smr_pss_shmem_kb = std::numeric_limits<uint32_t>::max();
+    uint32_t smr_swap_pss_kb = std::numeric_limits<uint32_t>::max();
     uint64_t runtime_user_mode_ns = std::numeric_limits<uint64_t>::max();
     uint64_t runtime_kernel_mode_ns = std::numeric_limits<uint64_t>::max();
     // file descriptors
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 5e1e277..5072c3d 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -2485,7 +2485,7 @@
         EXPECT_FALSE(found_counter_track_descriptor);
         found_counter_track_descriptor = true;
         thread_time_counter_uuid = packet.track_descriptor().uuid();
-        EXPECT_EQ("thread_time", packet.track_descriptor().name());
+        EXPECT_EQ("thread_time", packet.track_descriptor().static_name());
         auto counter = packet.track_descriptor().counter();
         EXPECT_EQ(
             perfetto::protos::gen::
@@ -5795,8 +5795,10 @@
       auto& desc = packet.track_descriptor();
       if (!desc.has_counter())
         continue;
-      counter_names[desc.uuid()] = desc.name();
-      EXPECT_EQ((desc.name() != "Framerate3"), desc.counter().is_incremental());
+      counter_names[desc.uuid()] =
+          desc.has_name() ? desc.name() : desc.static_name();
+      EXPECT_EQ((desc.static_name() != "Framerate3"),
+                desc.counter().is_incremental());
     }
     if (packet.has_track_event()) {
       auto event = packet.track_event();
@@ -5869,7 +5871,8 @@
       continue;
     }
     auto desc = packet.track_descriptor();
-    counter_names[desc.uuid()] = desc.name();
+    counter_names[desc.uuid()] =
+        desc.has_name() ? desc.name() : desc.static_name();
     if (desc.name() == "Framerate") {
       EXPECT_EQ("fps", desc.counter().unit_name());
     } else if (desc.name() == "Goats teleported") {
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index 02e6e03..dc02609 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -118,8 +118,13 @@
 
 protos::gen::TrackDescriptor CounterTrack::Serialize() const {
   auto desc = Track::Serialize();
-  desc.set_name(name_);
   auto* counter = desc.mutable_counter();
+  if (static_name_) {
+    desc.set_static_name(static_name_.value);
+  } else {
+    desc.set_name(dynamic_name_.value);
+  }
+
   if (category_)
     counter->add_categories(category_);
   if (unit_ != perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED)
diff --git a/src/tracing/track_event_state_tracker.cc b/src/tracing/track_event_state_tracker.cc
index 1574292..e65d86f 100644
--- a/src/tracing/track_event_state_tracker.cc
+++ b/src/tracing/track_event_state_tracker.cc
@@ -246,7 +246,11 @@
       track.index = static_cast<uint32_t>(session_state->tracks.size() + 1);
     track.uuid = track_descriptor.uuid();
 
-    track.name = track_descriptor.name().ToStdString();
+    if (track_descriptor.has_name()) {
+      track.name = track_descriptor.name().ToStdString();
+    } else if (track_descriptor.has_static_name()) {
+      track.name = track_descriptor.static_name().ToStdString();
+    }
     track.pid = 0;
     track.tid = 0;
     if (track_descriptor.has_process()) {
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 38e3ba2..45c972f 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -75,6 +75,7 @@
 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_traced_stats import ParsingTracedStats
 from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
 from diff_tests.parser.power.tests_energy_breakdown import PowerEnergyBreakdown
 from diff_tests.parser.power.tests_entity_state_residency import EntityStateResidency
@@ -137,6 +138,7 @@
 
 sys.path.pop()
 
+
 def fetch_all_diff_tests(index_path: str) -> List['testing.TestCase']:
   parser_tests = [
       *AndroidBugreport(index_path, 'parser/android',
@@ -187,11 +189,11 @@
       *SmokeJson(index_path, 'parser/smoke', 'SmokeJson').fetch(),
       *SmokeSchedEvents(index_path, 'parser/smoke', 'SmokeSchedEvents').fetch(),
       *InputMethodClients(index_path, 'parser/android',
-                            'InputMethodClients').fetch(),
+                          'InputMethodClients').fetch(),
       *InputMethodManagerService(index_path, 'parser/android',
-                            'InputMethodManagerService').fetch(),
+                                 'InputMethodManagerService').fetch(),
       *InputMethodService(index_path, 'parser/android',
-                            'InputMethodService').fetch(),
+                          'InputMethodService').fetch(),
       *SurfaceFlingerLayers(index_path, 'parser/android',
                             'SurfaceFlingerLayers').fetch(),
       *SurfaceFlingerTransactions(index_path, 'parser/android',
@@ -212,6 +214,8 @@
       *ParsingMemoryCounters(index_path, 'parser/parsing',
                              'ParsingMemoryCounters').fetch(),
       *FtraceCrop(index_path, 'parser/ftrace', 'FtraceCrop').fetch(),
+      *ParsingTracedStats(index_path, 'parser/parsing',
+                          'ParsingTracedStats').fetch(),
   ]
 
   metrics_tests = [
diff --git a/test/trace_processor/diff_tests/metrics/android/android_auto_multiuser.textproto b/test/trace_processor/diff_tests/metrics/android/android_auto_multiuser.textproto
index c33839e..e14c077 100644
--- a/test/trace_processor/diff_tests/metrics/android/android_auto_multiuser.textproto
+++ b/test/trace_processor/diff_tests/metrics/android/android_auto_multiuser.textproto
@@ -11,6 +11,16 @@
       uid: 1000010
       cmdline: "dummy:2"
     }
+    processes {
+      pid: 12
+      uid: 1300010
+      cmdline: "dummy:3"
+    }
+    processes {
+      pid: 20
+      uid: 1000
+      cmdline: "finishUserStopped-10"
+    }
   }
 }
 packet {
@@ -66,6 +76,38 @@
   }
 }
 packet {
+  ftrace_events {
+    cpu: 1
+    event {
+      timestamp: 5000000001
+      pid: 10
+      sched_switch {
+        prev_comm: "dummy:3"
+        prev_pid: 12
+        prev_state: 2
+        next_comm: "dummy:2"
+        next_pid: 11
+      }
+    }
+  }
+}
+packet {
+  ftrace_events {
+    cpu: 1
+    event {
+      timestamp: 5010000000
+      pid: 11
+      sched_switch {
+        prev_comm: "dummy:2"
+        prev_pid: 11
+        prev_state: 2
+        next_comm: "dummy:3"
+        next_pid: 13
+      }
+    }
+  }
+}
+packet {
   timestamp: 3000000001
   process_stats {
     processes {
@@ -102,6 +144,15 @@
   }
 }
 packet {
+  timestamp: 5000000002
+  process_stats {
+    processes {
+      pid: 11
+      vm_rss_kb: 3000
+    }
+  }
+}
+packet {
   ftrace_events {
     cpu: 1
     event {
@@ -124,4 +175,23 @@
       }
     }
   }
-}
\ No newline at end of file
+}
+packet {
+  ftrace_events {
+    cpu: 1
+    event {
+      timestamp: 5000000000
+      pid: 20
+      print {
+        buf: "B|20|finishUserStopped-10-[stopUser]\n"
+      }
+    }
+    event {
+      timestamp: 5100000000
+      pid: 20
+      print {
+        buf: "E|20\n"
+      }
+    }
+  }
+}
diff --git a/test/trace_processor/diff_tests/metrics/android/tests.py b/test/trace_processor/diff_tests/metrics/android/tests.py
index a1c2448..c3667fa 100644
--- a/test/trace_processor/diff_tests/metrics/android/tests.py
+++ b/test/trace_processor/diff_tests/metrics/android/tests.py
@@ -326,9 +326,34 @@
                 total_memory_usage_kb: 2048
             }
          }
+          user_switch {
+             user_id: 11
+             start_event: "UserController.startUser-11-fg-start-mode-1"
+             end_event: "finishUserStopped-10-[stopUser]"
+             duration_ms: 2100
+             previous_user_info {
+                 user_id: 10
+                 total_cpu_time_ms: 19
+                 total_memory_usage_kb: 3072
+             }
+          }
        }
        """))
 
+  def test_android_auto_multiuser_timing_table(self):
+      return DiffTestBlueprint(
+        trace=Path("android_auto_multiuser.textproto"),
+        query="""
+        INCLUDE PERFETTO MODULE android.auto.multiuser;
+        SELECT * FROM android_auto_multiuser_timing;
+        """,
+        out=Csv("""
+        "event_start_user_id","event_start_time","event_end_time","event_end_name","event_start_name","duration"
+        "11",3000000000,3999999999,"com.android.car.carlauncher","UserController.startUser-11-fg-start-mode-1",999999999
+        "11",3000000000,5100000000,"finishUserStopped-10-[stopUser]","UserController.startUser-11-fg-start-mode-1",2100000000
+        """)
+      )
+
   def test_android_oom_adjuster(self):
     return DiffTestBlueprint(
       trace=DataPath('android_postboot_unlock.pftrace'),
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
new file mode 100644
index 0000000..af2f362
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ParsingTracedStats(TestSuite):
+  # Check that `previous_packed_dropped: true` maps to
+  # `traced_buf_sequence_packet_loss`.
+  def test_sequence_packet_loss(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          trusted_packet_sequence_id: 2
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+        }
+        packet {
+          trusted_uid: 9999
+          trusted_packet_sequence_id: 1
+          trace_stats {
+            writer_stats {
+              sequence_id: 2
+              buffer: 0
+            }
+            writer_stats {
+              sequence_id: 3
+              buffer: 1
+            }
+            writer_stats {
+              sequence_id: 4
+              buffer: 2
+            }
+            writer_stats {
+              sequence_id: 5
+              buffer: 2
+            }
+          }
+        }
+        """),
+        query="""
+          SELECT idx, value
+          FROM stats
+          WHERE name = 'traced_buf_sequence_packet_loss'
+          ORDER BY idx;
+        """,
+        out=Csv("""
+        "idx","value"
+        0,0
+        1,1
+        2,2
+        """))
diff --git a/test/trace_processor/diff_tests/parser/translated_args/process_track_name.textproto b/test/trace_processor/diff_tests/parser/translated_args/process_track_name.textproto
new file mode 100644
index 0000000..22dc8fb
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/translated_args/process_track_name.textproto
@@ -0,0 +1,100 @@
+packet {
+  translation_table {
+    process_track_name {
+      raw_to_deobfuscated_name { key: "raw_track_name" value: "explicitly_renamed" }
+      raw_to_deobfuscated_name { key: "raw_slice1" value: "should_not_be_renamed" }
+      raw_to_deobfuscated_name { key: "raw_slice2" value: "implicitly_renamed" }
+      raw_to_deobfuscated_name { key: "raw_counter" value: "renamed_counter" }
+    }
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1000
+  track_descriptor {
+    uuid: 1
+    process: {
+      process_name: "exampleProcess"
+      pid: 1234
+    }
+  }
+}
+# Define a named track
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1001
+  track_descriptor {
+    uuid: 2
+    name: "raw_track_name"
+    parent_uuid: 1
+  }
+}
+# define a track who's name will be implicitly defined by slice names
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1002
+  track_descriptor {
+    uuid: 3
+    parent_uuid: 1
+  }
+}
+# Named track for the counter
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1002
+  track_descriptor {
+    uuid: 4
+    name: "raw_counter"
+    parent_uuid: 1
+    counter {
+    }
+  }
+}
+# Counter Event
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1004
+  track_event {
+    track_uuid: 4
+    type: 4
+    counter_value: 99
+  }
+}
+
+# begin/end pair for explicitly named track
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 1500
+  track_event {
+    track_uuid: 2
+    name: "raw_slice1"
+    type: 1
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 2000
+  track_event {
+    track_uuid: 2
+    type: 2
+  }
+}
+
+# begin/end pair for implicitly named slice
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 3000
+  track_event {
+    track_uuid: 3
+    name: "raw_slice2"
+    type: 1
+  }
+}
+packet {
+  trusted_packet_sequence_id: 1
+  timestamp: 4000
+  track_event {
+    track_uuid: 3
+    type: 2
+  }
+}
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/parser/translated_args/tests.py b/test/trace_processor/diff_tests/parser/translated_args/tests.py
index 7e28f38..3bada0d 100644
--- a/test/trace_processor/diff_tests/parser/translated_args/tests.py
+++ b/test/trace_processor/diff_tests/parser/translated_args/tests.py
@@ -121,6 +121,25 @@
         "slice_begin"
         """))
 
+  def test_process_track_name(self):
+    return DiffTestBlueprint(
+        trace=Path('process_track_name.textproto'),
+        query="""
+        SELECT
+          name
+        FROM track
+        WHERE
+          name IS NOT NULL
+          AND type in ('process_track', 'process_counter_track')
+        ORDER BY name;
+        """,
+        out=Csv("""
+        "name"
+        "explicitly_renamed"
+        "implicitly_renamed"
+        "renamed_counter"
+        """))
+
   def test_native_symbol_arg(self):
     return DiffTestBlueprint(
         trace=Path('native_symbol_arg.textproto'),
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index 7de63e9..5785b6f 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -178,6 +178,37 @@
         3073,8,4529,8
         """))
 
+  def test_distinct_multi_column(self):
+    return DiffTestBlueprint(
+        trace=TextProto(''),
+        query="""
+        CREATE PERFETTO TABLE foo AS
+        WITH data(a, b) AS (
+          VALUES
+            -- Needed to defeat any id/sorted detection.
+            (2, 3),
+            (0, 2),
+            (0, 1)
+        )
+        SELECT * FROM data;
+
+        CREATE TABLE bar AS
+        SELECT 1 AS b;
+
+        WITH multi_col_distinct AS (
+          SELECT DISTINCT a FROM foo CROSS JOIN bar USING (b)
+        ), multi_col_group_by AS (
+          SELECT a FROM foo CROSS JOIN bar USING (b) GROUP BY a
+        )
+        SELECT
+          (SELECT COUNT(*) FROM multi_col_distinct) AS cnt_distinct,
+          (SELECT COUNT(*) FROM multi_col_group_by) AS cnt_group_by
+        """,
+        out=Csv("""
+        "cnt_distinct","cnt_group_by"
+        1,1
+        """))
+
   def test_limit(self):
     return DiffTestBlueprint(
         trace=TextProto(''),
diff --git a/tools/bisect_ui_releases b/tools/bisect_ui_releases
new file mode 100755
index 0000000..ca0679a
--- /dev/null
+++ b/tools/bisect_ui_releases
@@ -0,0 +1,116 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Runs a bisection on the autopush ui.perfetto.dev builds
+
+Similar to git bisect, but bisects UI releases rather than commits.
+This works only for autopush builds from main, ignores canary and stable
+channels, as they make the history non-linear.
+
+How it works:
+- It first obtains an unordered list of versions from gs://ui.perfetto.dev
+- Then obtains the list of ordered commits from git
+- Intersects the two lists, keeping only git commits that have a corresponding
+  ui autopush release.
+- Proceeds with a guided bisect in the range.
+"""
+
+import argparse
+import sys
+
+from subprocess import check_output
+
+COMMIT_ABBR_LEN = 9  # UI truncates commitish to 9 chars, e.g. v45.0-38b7c2b12.
+
+
+def main():
+  parser = argparse.ArgumentParser()
+  parser.add_argument(
+      '--good',
+      default=None,
+      help='Last good release (e.g. v44.0-257a02990).' +
+      'Defaults to the first verion available')
+  parser.add_argument(
+      '--bad',
+      default=None,
+      help='First bad release. Defaults to the latest version available')
+  args = parser.parse_args()
+
+  print('Fetching list of UI releases from GCS...')
+  rev_list = check_output(['gsutil.py', 'ls', 'gs://ui.perfetto.dev/']).decode()
+  ui_map = {}  # maps '38b7c2b12' -> 'v45.0-38b7c2b12'
+  for line in rev_list.split():
+    version = line.split('/')[3]
+    if '-' not in version:
+      continue
+    ver_hash = version.split('-')[1]
+    ui_map[ver_hash] = version
+  print('Found %d UI versions' % len(ui_map))
+
+  # Get the linear history of all commits.
+  print('Fetching revision history from git...')
+  ui_versions = []
+  git_out = check_output(['git', 'rev-list', '--left-only',
+                          'origin/main']).decode()
+  for line in git_out.split():
+    line = line.strip()
+    rev = line[0:COMMIT_ABBR_LEN]
+    if rev not in ui_map:
+      continue  # Not all perfetto commits have a UI autopush build.
+    ui_versions.append(ui_map[rev])
+
+  # git rev-list emits entries in recent -> older versions. Reverse it.
+  ui_versions.reverse()
+
+  # Note that not all the entries in ui_map will be present in ui_versions.
+  # This is because ui_map contains also builds coming from canary and stable
+  # branches, that we ignore here.
+
+  start = ui_versions.index(args.good) if args.good else 0
+  end = ui_versions.index(args.bad) if args.bad else len(ui_versions) - 1
+  while end - start > 1:
+    print('\033c', end='')  # clear terminal.
+    print(
+        'Bisecting from %s (last good) to %s (first bad), %d revisions to go' %
+        (ui_versions[start], ui_versions[end], end - start + 1))
+    mid = (end + start) // 2
+
+    # Print a visual indication of where we are in the bisect.
+    for i in reversed(range(start, end + 1)):
+      sfx = ''
+      if i == start:
+        sfx = ' GOOD --------------'
+      elif i == end:
+        sfx = ' BAD ---------------'
+      elif i == mid:
+        sfx = ' <- version to test'
+      print(ui_versions[i] + sfx)
+
+    user_feedback = input(
+        'https://ui.perfetto.dev/%s/. Type g for good and b for bad: ' %
+        ui_versions[mid])
+    if user_feedback == 'b':
+      end = mid
+    elif user_feedback == 'g':
+      start = mid
+    else:
+      print('Unrecognised key "%d", try again' % user_feedback)
+
+  print('First bad UI release %s' % ui_versions[end])
+  print('You should now inspect the individual commits via git log good..bad')
+
+
+if __name__ == '__main__':
+  sys.exit(main())
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index a0d92ed..2aa0b7f 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -137,7 +137,7 @@
         ]
     },
     'config': {
-        'types': ['lite'],
+        'types': ['filegroup'],
         'targets': [
             '//protos/perfetto/config:source_set',
         ]
diff --git a/tools/install-build-deps b/tools/install-build-deps
index 8e1d88f..31972d4 100755
--- a/tools/install-build-deps
+++ b/tools/install-build-deps
@@ -186,7 +186,7 @@
     Dependency(
         'buildtools/protobuf',
         'https://chromium.googlesource.com/external/github.com/protocolbuffers/protobuf.git',
-        'fe271ab76f2ad2b2b28c10443865d2af21e27e0e',  # refs/tags/v3.20.3
+        'f0dc78d7e6e331b8c6bb2d5283e06aa26883ca7c',  # refs/tags/v21.12
         'all',
         'all'),
 
diff --git a/ui/build.js b/ui/build.js
index 56745df..b675d4f 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -127,7 +127,7 @@
     f: copyUiTestArtifactsAssets,
   },
   {r: /.*\/dist\/.+\/(?!manifest\.json).*/, f: genServiceWorkerManifestJson},
-  {r: /.*\/dist\/.*/, f: notifyLiveServer},
+  {r: /.*\/dist\/.*[.](js|html|css|wasm)$/, f: notifyLiveServer},
 ];
 
 const tasks = [];
@@ -412,10 +412,7 @@
     'protos/perfetto/config/perfetto_config.proto',
     'protos/perfetto/ipc/consumer_port.proto',
     'protos/perfetto/ipc/wire_protocol.proto',
-    'protos/perfetto/metrics/metrics.proto',
     'protos/perfetto/trace/perfetto/perfetto_metatrace.proto',
-    'protos/perfetto/trace/trace.proto',
-    'protos/perfetto/trace/trace_packet.proto',
     'protos/perfetto/trace_processor/trace_processor.proto',
   ];
   // Can't put --no-comments here - The comments are load bearing for
diff --git a/ui/src/assets/brand.png b/ui/src/assets/brand.png
index dc6f8b6..63fe6f3 100644
--- a/ui/src/assets/brand.png
+++ b/ui/src/assets/brand.png
Binary files differ
diff --git a/ui/src/assets/favicon.png b/ui/src/assets/favicon.png
index 837b75b..2844520 100644
--- a/ui/src/assets/favicon.png
+++ b/ui/src/assets/favicon.png
Binary files differ
diff --git a/ui/src/assets/logo-128.png b/ui/src/assets/logo-128.png
index 43a2cd1..6d13da5 100644
--- a/ui/src/assets/logo-128.png
+++ b/ui/src/assets/logo-128.png
Binary files differ
diff --git a/ui/src/assets/logo-3d.png b/ui/src/assets/logo-3d.png
index b832ae6..9528c48 100644
--- a/ui/src/assets/logo-3d.png
+++ b/ui/src/assets/logo-3d.png
Binary files differ
diff --git a/ui/src/assets/rec_atrace.png b/ui/src/assets/rec_atrace.png
index d63f2a2..9a6baa2 100644
--- a/ui/src/assets/rec_atrace.png
+++ b/ui/src/assets/rec_atrace.png
Binary files differ
diff --git a/ui/src/assets/rec_battery_counters.png b/ui/src/assets/rec_battery_counters.png
index b73603d..3f1557b 100644
--- a/ui/src/assets/rec_battery_counters.png
+++ b/ui/src/assets/rec_battery_counters.png
Binary files differ
diff --git a/ui/src/assets/rec_board_voltage.png b/ui/src/assets/rec_board_voltage.png
index bc4dd12..d5f5b42 100644
--- a/ui/src/assets/rec_board_voltage.png
+++ b/ui/src/assets/rec_board_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_coarse.png b/ui/src/assets/rec_cpu_coarse.png
index b7241bf..9296a19 100644
--- a/ui/src/assets/rec_cpu_coarse.png
+++ b/ui/src/assets/rec_cpu_coarse.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_fine.png b/ui/src/assets/rec_cpu_fine.png
index 3c8df8b..6d069c2 100644
--- a/ui/src/assets/rec_cpu_fine.png
+++ b/ui/src/assets/rec_cpu_fine.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_freq.png b/ui/src/assets/rec_cpu_freq.png
index 23d86f8..5bd9e7b 100644
--- a/ui/src/assets/rec_cpu_freq.png
+++ b/ui/src/assets/rec_cpu_freq.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_voltage.png b/ui/src/assets/rec_cpu_voltage.png
index d82d5f3..7e31de9 100644
--- a/ui/src/assets/rec_cpu_voltage.png
+++ b/ui/src/assets/rec_cpu_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_frame_timeline.png b/ui/src/assets/rec_frame_timeline.png
index 2c83762..da98c40 100644
--- a/ui/src/assets/rec_frame_timeline.png
+++ b/ui/src/assets/rec_frame_timeline.png
Binary files differ
diff --git a/ui/src/assets/rec_ftrace.png b/ui/src/assets/rec_ftrace.png
index a907f9e..e956d05 100644
--- a/ui/src/assets/rec_ftrace.png
+++ b/ui/src/assets/rec_ftrace.png
Binary files differ
diff --git a/ui/src/assets/rec_gpu_mem_total.png b/ui/src/assets/rec_gpu_mem_total.png
index 4b5a44a..537ce9f 100644
--- a/ui/src/assets/rec_gpu_mem_total.png
+++ b/ui/src/assets/rec_gpu_mem_total.png
Binary files differ
diff --git a/ui/src/assets/rec_java_heap_dump.png b/ui/src/assets/rec_java_heap_dump.png
index 229ebe0..e3ee7c4 100644
--- a/ui/src/assets/rec_java_heap_dump.png
+++ b/ui/src/assets/rec_java_heap_dump.png
Binary files differ
diff --git a/ui/src/assets/rec_lmk.png b/ui/src/assets/rec_lmk.png
index 7324cf9..3a42b2d 100644
--- a/ui/src/assets/rec_lmk.png
+++ b/ui/src/assets/rec_lmk.png
Binary files differ
diff --git a/ui/src/assets/rec_logcat.png b/ui/src/assets/rec_logcat.png
index b3b4905..dda6526 100644
--- a/ui/src/assets/rec_logcat.png
+++ b/ui/src/assets/rec_logcat.png
Binary files differ
diff --git a/ui/src/assets/rec_long_trace.png b/ui/src/assets/rec_long_trace.png
index 23aad0b..f801756 100644
--- a/ui/src/assets/rec_long_trace.png
+++ b/ui/src/assets/rec_long_trace.png
Binary files differ
diff --git a/ui/src/assets/rec_mem_hifreq.png b/ui/src/assets/rec_mem_hifreq.png
index f36ef3e..f2909f8 100644
--- a/ui/src/assets/rec_mem_hifreq.png
+++ b/ui/src/assets/rec_mem_hifreq.png
Binary files differ
diff --git a/ui/src/assets/rec_meminfo.png b/ui/src/assets/rec_meminfo.png
index 4280675..43f968e 100644
--- a/ui/src/assets/rec_meminfo.png
+++ b/ui/src/assets/rec_meminfo.png
Binary files differ
diff --git a/ui/src/assets/rec_native_heap_profiler.png b/ui/src/assets/rec_native_heap_profiler.png
index bb3b010..4915c2b 100644
--- a/ui/src/assets/rec_native_heap_profiler.png
+++ b/ui/src/assets/rec_native_heap_profiler.png
Binary files differ
diff --git a/ui/src/assets/rec_one_shot.png b/ui/src/assets/rec_one_shot.png
index 7539c72..8981075 100644
--- a/ui/src/assets/rec_one_shot.png
+++ b/ui/src/assets/rec_one_shot.png
Binary files differ
diff --git a/ui/src/assets/rec_profiling.png b/ui/src/assets/rec_profiling.png
index 385670c..024d9f5 100644
--- a/ui/src/assets/rec_profiling.png
+++ b/ui/src/assets/rec_profiling.png
Binary files differ
diff --git a/ui/src/assets/rec_ps_stats.png b/ui/src/assets/rec_ps_stats.png
index e37bab1..df02761 100644
--- a/ui/src/assets/rec_ps_stats.png
+++ b/ui/src/assets/rec_ps_stats.png
Binary files differ
diff --git a/ui/src/assets/rec_ring_buf.png b/ui/src/assets/rec_ring_buf.png
index a2490fa..9bbe231 100644
--- a/ui/src/assets/rec_ring_buf.png
+++ b/ui/src/assets/rec_ring_buf.png
Binary files differ
diff --git a/ui/src/assets/rec_syscalls.png b/ui/src/assets/rec_syscalls.png
index 734854a..90b4ad8 100644
--- a/ui/src/assets/rec_syscalls.png
+++ b/ui/src/assets/rec_syscalls.png
Binary files differ
diff --git a/ui/src/assets/rec_vmstat.png b/ui/src/assets/rec_vmstat.png
index 58a4e71..46a1006 100644
--- a/ui/src/assets/rec_vmstat.png
+++ b/ui/src/assets/rec_vmstat.png
Binary files differ
diff --git a/ui/src/assets/scheduling_latency.png b/ui/src/assets/scheduling_latency.png
index 36bcfb8..8b5a2e1 100644
--- a/ui/src/assets/scheduling_latency.png
+++ b/ui/src/assets/scheduling_latency.png
Binary files differ
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index 41a201f..d3c5f4b 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -19,3 +19,8 @@
 export function exists<T>(value: T): value is NonNullable<T> {
   return value !== undefined && value !== null;
 }
+
+// Generic result type - similar to Rust's Result<T, E>
+export type Result<T, E = {}> =
+  | {success: true; result: T}
+  | {success: false; error: E};
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4cebecb..15d8f6c 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -253,15 +253,15 @@
     // the reducer.
     args: {
       name: string;
-      id: string;
+      key: string;
       summaryTrackKey?: string;
       collapsed: boolean;
       fixedOrdering?: boolean;
     },
   ): void {
-    state.trackGroups[args.id] = {
+    state.trackGroups[args.key] = {
       name: args.name,
-      id: args.id,
+      key: args.key,
       collapsed: args.collapsed,
       tracks: [],
       summaryTrack: args.summaryTrackKey,
@@ -394,12 +394,8 @@
     }
   },
 
-  toggleTrackGroupCollapsed(
-    state: StateDraft,
-    args: {trackGroupId: string},
-  ): void {
-    const id = args.trackGroupId;
-    const trackGroup = assertExists(state.trackGroups[id]);
+  toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
+    const trackGroup = assertExists(state.trackGroups[args.groupKey]);
     trackGroup.collapsed = !trackGroup.collapsed;
   },
 
@@ -448,31 +444,6 @@
     }
   },
 
-  createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void {
-    state.permalink = {
-      requestId: generateNextId(state),
-      hash: undefined,
-      isRecordingConfig: args.isRecordingConfig,
-    };
-  },
-
-  setPermalink(
-    state: StateDraft,
-    args: {requestId: string; hash: string},
-  ): void {
-    // Drop any links for old requests.
-    if (state.permalink.requestId !== args.requestId) return;
-    state.permalink = args;
-  },
-
-  loadPermalink(state: StateDraft, args: {hash: string}): void {
-    state.permalink = {requestId: generateNextId(state), hash: args.hash};
-  },
-
-  clearPermalink(state: StateDraft, _: {}): void {
-    state.permalink = {};
-  },
-
   updateStatus(state: StateDraft, args: Status): void {
     if (statusTraceEvent) {
       traceEventEnd(statusTraceEvent);
@@ -920,7 +891,7 @@
 
   toggleTrackSelection(
     state: StateDraft,
-    args: {id: string; isTrackGroup: boolean},
+    args: {key: string; isTrackGroup: boolean},
   ) {
     const selection = state.selection;
     if (
@@ -930,12 +901,12 @@
       return;
     }
     const areaId = selection.legacySelection.areaId;
-    const index = state.areas[areaId].tracks.indexOf(args.id);
+    const index = state.areas[areaId].tracks.indexOf(args.key);
     if (index > -1) {
       state.areas[areaId].tracks.splice(index, 1);
       if (args.isTrackGroup) {
         // Also remove all child tracks.
-        for (const childTrack of state.trackGroups[args.id].tracks) {
+        for (const childTrack of state.trackGroups[args.key].tracks) {
           const childIndex = state.areas[areaId].tracks.indexOf(childTrack);
           if (childIndex > -1) {
             state.areas[areaId].tracks.splice(childIndex, 1);
@@ -943,10 +914,10 @@
         }
       }
     } else {
-      state.areas[areaId].tracks.push(args.id);
+      state.areas[areaId].tracks.push(args.key);
       if (args.isTrackGroup) {
         // Also add all child tracks.
-        for (const childTrack of state.trackGroups[args.id].tracks) {
+        for (const childTrack of state.trackGroups[args.key].tracks) {
           if (!state.areas[areaId].tracks.includes(childTrack)) {
             state.areas[areaId].tracks.push(childTrack);
           }
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index de497d7..734ac91 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -60,14 +60,14 @@
 
 function fakeTrackGroup(
   state: State,
-  args: {id: string; summaryTrackId: string},
+  args: {key: string; summaryTrackKey: string},
 ): State {
   return produce(state, (draft) => {
     StateActions.addTrackGroup(draft, {
       name: 'A group',
-      id: args.id,
+      key: args.key,
       collapsed: false,
-      summaryTrackKey: args.summaryTrackId,
+      summaryTrackKey: args.summaryTrackKey,
     });
   });
 }
@@ -117,7 +117,7 @@
   const afterGroup = produce(state, (draft) => {
     StateActions.addTrackGroup(draft, {
       name: 'A track group',
-      id: '123-123-123',
+      key: '123-123-123',
       summaryTrackKey: 's',
       collapsed: false,
     });
@@ -151,19 +151,19 @@
     });
   });
 
-  const firstTrackId = once.scrollingTracks[0];
-  const secondTrackId = once.scrollingTracks[1];
+  const firstTrackKey = once.scrollingTracks[0];
+  const secondTrackKey = once.scrollingTracks[1];
 
   const twice = produce(once, (draft) => {
     StateActions.moveTrack(draft, {
-      srcId: `${firstTrackId}`,
+      srcId: `${firstTrackKey}`,
       op: 'after',
-      dstId: `${secondTrackId}`,
+      dstId: `${secondTrackKey}`,
     });
   });
 
-  expect(twice.scrollingTracks[0]).toBe(secondTrackId);
-  expect(twice.scrollingTracks[1]).toBe(firstTrackId);
+  expect(twice.scrollingTracks[0]).toBe(secondTrackKey);
+  expect(twice.scrollingTracks[1]).toBe(firstTrackKey);
 });
 
 test('reorder pinned to scrolling', () => {
@@ -326,7 +326,7 @@
 
 test('sortTracksByPriority', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
   state = fakeTrack(state, {
     key: 'b',
     uri: HEAP_PROFILE_TRACK_KIND,
@@ -351,7 +351,7 @@
 
 test('sortTracksByPriorityAndKindAndName', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
   state = fakeTrack(state, {
     key: 'a',
     uri: PROCESS_SCHEDULING_TRACK_KIND,
@@ -416,7 +416,7 @@
 
 test('sortTracksByTidThenName', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'a'});
   state = fakeTrack(state, {
     key: 'a',
     uri: SLICE_TRACK_KIND,
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index e3ed2b7..c513d73 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -95,7 +95,6 @@
     scrollingTracks: [],
     areas: {},
     queries: {},
-    permalink: {},
     notes: {},
 
     recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
diff --git a/ui/src/common/metatracing.ts b/ui/src/common/metatracing.ts
index 756d68d..cdd43b4 100644
--- a/ui/src/common/metatracing.ts
+++ b/ui/src/common/metatracing.ts
@@ -13,12 +13,8 @@
 // limitations under the License.
 
 import {featureFlags} from '../core/feature_flags';
-import {
-  MetatraceCategories,
-  PerfettoMetatrace,
-  Trace,
-  TracePacket,
-} from '../protos';
+import {MetatraceCategories, PerfettoMetatrace} from '../protos';
+import protobuf from 'protobufjs/minimal';
 
 const METATRACING_BUFFER_SIZE = 100000;
 
@@ -83,7 +79,7 @@
 const traceEvents: TraceEvent[] = [];
 
 function readMetatrace(): Uint8Array {
-  const eventToPacket = (e: TraceEvent): TracePacket => {
+  const eventToPacket = (e: TraceEvent): Uint8Array => {
     const metatraceEvent = PerfettoMetatrace.create({
       eventName: e.eventName,
       threadId: e.track,
@@ -97,20 +93,37 @@
         }),
       );
     }
-    return TracePacket.create({
-      timestamp: e.startNs,
-      timestampClockId: 1,
-      perfettoMetatrace: metatraceEvent,
-    });
+    const PROTO_VARINT_TYPE = 0;
+    const PROTO_LEN_DELIMITED_WIRE_TYPE = 2;
+    const TRACE_PACKET_PROTO_TAG = (1 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
+    const TRACE_PACKET_TIMESTAMP_TAG = (8 << 3) | PROTO_VARINT_TYPE;
+    const TRACE_PACKET_CLOCK_ID_TAG = (58 << 3) | PROTO_VARINT_TYPE;
+    const TRACE_PACKET_METATRACE_TAG =
+      (49 << 3) | PROTO_LEN_DELIMITED_WIRE_TYPE;
+
+    const wri = protobuf.Writer.create();
+    wri.uint32(TRACE_PACKET_PROTO_TAG);
+    wri.fork(); // Start of Trace Packet.
+    wri.uint32(TRACE_PACKET_TIMESTAMP_TAG).int64(e.startNs);
+    wri.uint32(TRACE_PACKET_CLOCK_ID_TAG).int32(1);
+    wri
+      .uint32(TRACE_PACKET_METATRACE_TAG)
+      .bytes(PerfettoMetatrace.encode(metatraceEvent).finish());
+    wri.ldelim();
+    return wri.finish();
   };
-  const packets: TracePacket[] = [];
+  const packets: Uint8Array[] = [];
   for (const event of traceEvents) {
     packets.push(eventToPacket(event));
   }
-  const trace = Trace.create({
-    packet: packets,
-  });
-  return Trace.encode(trace).finish();
+  const totalLength = packets.reduce((acc, arr) => acc + arr.length, 0);
+  const trace = new Uint8Array(totalLength);
+  let offset = 0;
+  for (const packet of packets) {
+    trace.set(packet, offset);
+    offset += packet.length;
+  }
+  return trace;
 }
 
 interface TraceEventParams {
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 3a5899e..f729597 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -17,7 +17,7 @@
 import {Disposable, Trash} from '../base/disposable';
 import {Registry} from '../base/registry';
 import {Span, duration, time} from '../base/time';
-import {globals} from '../frontend/globals';
+import {TraceContext, globals} from '../frontend/globals';
 import {
   Command,
   DetailsPanel,
@@ -45,6 +45,7 @@
 import {raf} from '../core/raf_scheduler';
 import {defaultPlugins} from '../core/default_plugins';
 import {HighPrecisionTimeSpan} from './high_precision_time';
+import {PromptOption} from '../frontend/omnibox_manager';
 
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
@@ -275,10 +276,10 @@
           };
           return predicate(ref);
         })
-        .map((group) => group.id);
+        .map((group) => group.key);
 
-      for (const trackGroupId of groupsToExpand) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+      for (const groupKey of groupsToExpand) {
+        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
       }
     },
 
@@ -293,10 +294,10 @@
           };
           return predicate(ref);
         })
-        .map((group) => group.id);
+        .map((group) => group.key);
 
-      for (const trackGroupId of groupsToCollapse) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+      for (const groupKey of groupsToCollapse) {
+        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
       }
     },
 
@@ -342,11 +343,16 @@
     return globals.store.createSubStore(['plugins', this.pluginId], migrate);
   }
 
-  readonly trace = {
-    get span(): Span<time, duration> {
-      return globals.stateTraceTimeTP();
-    },
-  };
+  get trace(): TraceContext {
+    return globals.traceContext;
+  }
+
+  async prompt(
+    text: string,
+    options?: PromptOption[] | undefined,
+  ): Promise<string> {
+    return globals.omnibox.prompt(text, options);
+  }
 }
 
 function isPinned(trackId: string): boolean {
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index a6c461b..227b9cd 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -40,48 +40,59 @@
   params?: QueryRunParams,
 ): Promise<QueryResponse> {
   const startMs = performance.now();
-  const queryRes = engine.execute(sqlQuery);
 
   // TODO(primiano): once the controller thread is gone we should pass down
   // the result objects directly to the frontend, iterate over the result
   // and deal with pagination there. For now we keep the old behavior and
   // truncate to 10k rows.
 
-  try {
-    await queryRes.waitAllRows();
-  } catch {
+  const maybeResult = await engine.tryQuery(sqlQuery);
+
+  if (maybeResult.success) {
+    const queryRes = maybeResult.result;
+    const convertNullsToString = params?.convertNullsToString ?? true;
+
+    const durationMs = performance.now() - startMs;
+    const rows: Row[] = [];
+    const columns = queryRes.columns();
+    let numRows = 0;
+    for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
+      const row: Row = {};
+      for (const colName of columns) {
+        const value = iter.get(colName);
+        row[colName] = value === null && convertNullsToString ? 'NULL' : value;
+      }
+      rows.push(row);
+      if (++numRows >= MAX_DISPLAY_ROWS) break;
+    }
+
+    const result: QueryResponse = {
+      query: sqlQuery,
+      durationMs,
+      error: queryRes.error(),
+      totalRowCount: queryRes.numRows(),
+      columns,
+      rows,
+      statementCount: queryRes.statementCount(),
+      statementWithOutputCount: queryRes.statementWithOutputCount(),
+      lastStatementSql: queryRes.lastStatementSql(),
+    };
+    return result;
+  } else {
     // In the case of a query error we don't want the exception to bubble up
     // as a crash. The |queryRes| object will be populated anyways.
     // queryRes.error() is used to tell if the query errored or not. If it
     // errored, the frontend will show a graceful message instead.
+    return {
+      query: sqlQuery,
+      durationMs: performance.now() - startMs,
+      error: maybeResult.error.message,
+      totalRowCount: 0,
+      columns: [],
+      rows: [],
+      statementCount: 0,
+      statementWithOutputCount: 0,
+      lastStatementSql: '',
+    };
   }
-
-  const convertNullsToString = params?.convertNullsToString ?? true;
-
-  const durationMs = performance.now() - startMs;
-  const rows: Row[] = [];
-  const columns = queryRes.columns();
-  let numRows = 0;
-  for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
-    const row: Row = {};
-    for (const colName of columns) {
-      const value = iter.get(colName);
-      row[colName] = value === null && convertNullsToString ? 'NULL' : value;
-    }
-    rows.push(row);
-    if (++numRows >= MAX_DISPLAY_ROWS) break;
-  }
-
-  const result: QueryResponse = {
-    query: sqlQuery,
-    durationMs,
-    error: queryRes.error(),
-    totalRowCount: queryRes.numRows(),
-    columns,
-    rows,
-    statementCount: queryRes.statementCount(),
-    statementWithOutputCount: queryRes.statementWithOutputCount(),
-    lastStatementSql: queryRes.lastStatementSql(),
-  };
-  return result;
 }
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index fc3018e..94eb429 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -151,7 +151,8 @@
 // 52. Update track group state - don't make the summary track the first track.
 // 53. Remove android log state.
 // 54. Remove traceTime.
-export const STATE_VERSION = 54;
+// 55. Rename TrackGroupState.id -> TrackGroupState.key.
+export const STATE_VERSION = 55;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -288,7 +289,7 @@
 }
 
 export interface TrackGroupState {
-  id: string;
+  key: string;
   name: string;
   collapsed: boolean;
   tracks: string[]; // Child track ids.
@@ -310,12 +311,6 @@
   query: string;
 }
 
-export interface PermalinkConfig {
-  requestId?: string; // Set by the frontend to request a new permalink.
-  hash?: string; // Set by the controller when the link has been created.
-  isRecordingConfig?: boolean; // this permalink request is for a recording config only
-}
-
 export interface FrontendLocalState {
   visibleState: VisibleState;
 }
@@ -476,7 +471,7 @@
   newEngineMode: NewEngineMode;
   engine?: EngineConfig;
   traceUuid?: string;
-  trackGroups: ObjectById<TrackGroupState>;
+  trackGroups: ObjectByKey<TrackGroupState>;
   tracks: ObjectByKey<TrackState>;
   utidToThreadSortKey: UtidToTrackSortKey;
   areas: ObjectById<AreaById>;
@@ -486,7 +481,6 @@
   debugTrackId?: string;
   lastTrackReloadRequest?: number;
   queries: ObjectById<QueryConfig>;
-  permalink: PermalinkConfig;
   notes: ObjectById<Note | AreaNote>;
   status: Status;
   selection: Selection;
@@ -915,20 +909,19 @@
   ];
 }
 
-export function getContainingTrackId(
+export function getContainingGroupKey(
   state: State,
   trackKey: string,
 ): null | string {
   const track = state.tracks[trackKey];
-  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-  if (!track) {
+  if (track === undefined) {
     return null;
   }
-  const parentId = track.trackGroup;
-  if (!parentId) {
+  const parentGroupKey = track.trackGroup;
+  if (!parentGroupKey) {
     return null;
   }
-  return parentId;
+  return parentGroupKey;
 }
 
 export function getLegacySelection(state: State): LegacySelection | null {
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index be66340..ff111de 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -15,7 +15,7 @@
 import {PrimaryTrackSortKey} from '../public';
 
 import {createEmptyState} from './empty_state';
-import {getContainingTrackId, State} from './state';
+import {getContainingGroupKey, State} from './state';
 import {deserializeStateObject, serializeStateObject} from './upload_utils';
 
 test('createEmptyState', () => {
@@ -40,9 +40,9 @@
     trackGroup: 'containsB',
   };
 
-  expect(getContainingTrackId(state, 'z')).toEqual(null);
-  expect(getContainingTrackId(state, 'a')).toEqual(null);
-  expect(getContainingTrackId(state, 'b')).toEqual('containsB');
+  expect(getContainingGroupKey(state, 'z')).toEqual(null);
+  expect(getContainingGroupKey(state, 'a')).toEqual(null);
+  expect(getContainingGroupKey(state, 'b')).toEqual('containsB');
 });
 
 test('state is serializable', () => {
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
index 4c8ad85..7d4d7cd 100644
--- a/ui/src/controller/app_controller.ts
+++ b/ui/src/controller/app_controller.ts
@@ -16,7 +16,6 @@
 import {globals} from '../frontend/globals';
 
 import {Child, Controller, ControllerInitializerAny} from './controller';
-import {PermalinkController} from './permalink_controller';
 import {RecordController} from './record_controller';
 import {TraceController} from './trace_controller';
 
@@ -40,9 +39,7 @@
   // - An internal promise of a nested controller being resolved and manually
   //   re-triggering the controllers.
   run() {
-    const childControllers: ControllerInitializerAny[] = [
-      Child('permalink', PermalinkController, {}),
-    ];
+    const childControllers: ControllerInitializerAny[] = [];
     if (!RECORDING_V2_FLAG.get()) {
       childControllers.push(
         Child('record', RecordController, {extensionPort: this.extensionPort}),
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
deleted file mode 100644
index 1380ebf..0000000
--- a/ui/src/controller/permalink_controller.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {produce} from 'immer';
-
-import {assertExists} from '../base/logging';
-import {runValidator} from '../base/validators';
-import {Actions} from '../common/actions';
-import {ConversionJobStatus} from '../common/conversion_jobs';
-import {
-  createEmptyNonSerializableState,
-  createEmptyState,
-} from '../common/empty_state';
-import {EngineConfig, ObjectById, State, STATE_VERSION} from '../common/state';
-import {
-  BUCKET_NAME,
-  buggyToSha256,
-  deserializeStateObject,
-  saveState,
-  toSha256,
-  TraceGcsUploader,
-} from '../common/upload_utils';
-import {globals} from '../frontend/globals';
-import {publishConversionJobStatusUpdate} from '../frontend/publish';
-import {Router} from '../frontend/router';
-
-import {Controller} from './controller';
-import {RecordConfig, recordConfigValidator} from './record_config_types';
-import {showModal} from '../widgets/modal';
-
-interface MultiEngineState {
-  currentEngineId?: string;
-  engines: ObjectById<EngineConfig>;
-}
-
-function isMultiEngineState(
-  state: State | MultiEngineState,
-): state is MultiEngineState {
-  if ((state as MultiEngineState).engines !== undefined) {
-    return true;
-  }
-  return false;
-}
-
-export class PermalinkController extends Controller<'main'> {
-  private lastRequestId?: string;
-  constructor() {
-    super('main');
-  }
-
-  run() {
-    if (
-      globals.state.permalink.requestId === undefined ||
-      globals.state.permalink.requestId === this.lastRequestId
-    ) {
-      return;
-    }
-    const requestId = assertExists(globals.state.permalink.requestId);
-    this.lastRequestId = requestId;
-
-    // if the |hash| is not set, this is a request to create a permalink.
-    if (globals.state.permalink.hash === undefined) {
-      const isRecordingConfig = assertExists(
-        globals.state.permalink.isRecordingConfig,
-      );
-
-      const jobName = 'create_permalink';
-      publishConversionJobStatusUpdate({
-        jobName,
-        jobStatus: ConversionJobStatus.InProgress,
-      });
-
-      PermalinkController.createPermalink(isRecordingConfig)
-        .then((hash) => {
-          globals.dispatch(Actions.setPermalink({requestId, hash}));
-        })
-        .finally(() => {
-          publishConversionJobStatusUpdate({
-            jobName,
-            jobStatus: ConversionJobStatus.NotRunning,
-          });
-        });
-      return;
-    }
-
-    // Otherwise, this is a request to load the permalink.
-    PermalinkController.loadState(globals.state.permalink.hash).then(
-      (stateOrConfig) => {
-        if (PermalinkController.isRecordConfig(stateOrConfig)) {
-          // This permalink state only contains a RecordConfig. Show the
-          // recording page with the config, but keep other state as-is.
-          const validConfig = runValidator(
-            recordConfigValidator,
-            stateOrConfig as unknown,
-          ).result;
-          globals.dispatch(Actions.setRecordConfig({config: validConfig}));
-          Router.navigate('#!/record');
-          return;
-        }
-        globals.dispatch(Actions.setState({newState: stateOrConfig}));
-        this.lastRequestId = stateOrConfig.permalink.requestId;
-      },
-    );
-  }
-
-  private static upgradeState(state: State): State {
-    if (state.engine !== undefined && state.engine.source.type !== 'URL') {
-      // All permalink traces should be modified to have a source.type=URL
-      // pointing to the uploaded trace. Due to a bug in some older version
-      // of the UI (b/327049372), an upload failure can end up with a state that
-      // has type=FILE but a null file object. If this happens, invalidate the
-      // trace and show a message.
-      showModal({
-        title: 'Cannot load trace permalink',
-        content: m(
-          'div',
-          'The permalink stored on the server is corrupted ' +
-            'and cannot be loaded.',
-        ),
-      });
-      return createEmptyState();
-    }
-
-    if (state.version !== STATE_VERSION) {
-      const newState = createEmptyState();
-      // Old permalinks from state versions prior to version 24
-      // have multiple engines of which only one is identified as the
-      // current engine via currentEngineId. Handle this case:
-      if (isMultiEngineState(state)) {
-        const engineId = state.currentEngineId;
-        if (engineId !== undefined) {
-          newState.engine = state.engines[engineId];
-        }
-      } else {
-        newState.engine = state.engine;
-      }
-
-      if (newState.engine !== undefined) {
-        newState.engine.ready = false;
-      }
-      const message =
-        `Unable to parse old state version. Discarding state ` +
-        `and loading trace.`;
-      console.warn(message);
-      PermalinkController.updateStatus(message);
-      return newState;
-    } else {
-      // Loaded state is presumed to be compatible with the State type
-      // definition in the app. However, a non-serializable part has to be
-      // recreated.
-      state.nonSerializableState = createEmptyNonSerializableState();
-    }
-    return state;
-  }
-
-  private static isRecordConfig(
-    stateOrConfig: State | RecordConfig,
-  ): stateOrConfig is RecordConfig {
-    const mode = (stateOrConfig as {mode?: string}).mode;
-    return (
-      mode !== undefined &&
-      ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
-    );
-  }
-
-  private static async createPermalink(
-    isRecordingConfig: boolean,
-  ): Promise<string> {
-    let uploadState: State | RecordConfig = globals.state;
-
-    if (isRecordingConfig) {
-      uploadState = globals.state.recordConfig;
-    } else {
-      const engine = assertExists(globals.getCurrentEngine());
-      let dataToUpload: File | ArrayBuffer | undefined = undefined;
-      let traceName = `trace ${engine.id}`;
-      if (engine.source.type === 'FILE') {
-        dataToUpload = engine.source.file;
-        traceName = dataToUpload.name;
-      } else if (engine.source.type === 'ARRAY_BUFFER') {
-        dataToUpload = engine.source.buffer;
-      } else if (engine.source.type !== 'URL') {
-        throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
-      }
-
-      if (dataToUpload !== undefined) {
-        PermalinkController.updateStatus(`Uploading ${traceName}`);
-        const uploader = new TraceGcsUploader(dataToUpload, () => {
-          switch (uploader.state) {
-            case 'UPLOADING':
-              const statusTxt = `Uploading ${uploader.getEtaString()}`;
-              PermalinkController.updateStatus(statusTxt);
-              break;
-            case 'UPLOADED':
-              // Convert state to use URLs and remove permalink.
-              const url = uploader.uploadedUrl;
-              uploadState = produce(globals.state, (draft) => {
-                assertExists(draft.engine).source = {type: 'URL', url};
-                draft.permalink = {};
-              });
-              break;
-            case 'ERROR':
-              PermalinkController.updateStatus(
-                `Upload failed ${uploader.error}`,
-              );
-              break;
-          } // switch (state)
-        }); // onProgress
-        await uploader.waitForCompletion();
-      }
-    }
-
-    // Upload state.
-    PermalinkController.updateStatus(`Creating permalink...`);
-    const hash = await saveState(uploadState);
-    PermalinkController.updateStatus(`Permalink ready`);
-    return hash;
-  }
-
-  private static async loadState(id: string): Promise<State | RecordConfig> {
-    const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
-    const response = await fetch(url);
-    if (!response.ok) {
-      throw new Error(
-        `Could not fetch permalink.\n` +
-          `Are you sure the id (${id}) is correct?\n` +
-          `URL: ${url}`,
-      );
-    }
-    const text = await response.text();
-    const stateHash = await toSha256(text);
-    const state = deserializeStateObject<State>(text);
-    if (stateHash !== id) {
-      // Old permalinks incorrectly dropped some digits from the
-      // hexdigest of the SHA256. We don't want to invalidate those
-      // links so we also compute the old string and try that here
-      // also.
-      const buggyStateHash = await buggyToSha256(text);
-      if (buggyStateHash !== id) {
-        throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
-      }
-    }
-    if (!this.isRecordConfig(state)) {
-      return this.upgradeState(state);
-    }
-    return state;
-  }
-
-  private static updateStatus(msg: string): void {
-    // TODO(hjd): Unify loading updates.
-    globals.dispatch(
-      Actions.updateStatus({
-        msg,
-        timestamp: Date.now() / 1000,
-      }),
-    );
-  }
-}
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index ba2d354..bd6a3b1 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -163,7 +163,7 @@
       utids.push(it.utid);
     }
 
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
     const maxCpu = Math.max(...cpus, -1);
 
     const res = await this.query(`
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 1b6dd0f..0a58c43 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -441,7 +441,7 @@
           IFNULL(value, 0) as value
         FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
     const previousValue = previous.firstRow({value: NUM}).value;
-    const endTs = rightTs !== -1n ? rightTs : globals.traceTime.end;
+    const endTs = rightTs !== -1n ? rightTs : globals.traceContext.end;
     const delta = value - previousValue;
     const duration = endTs - ts;
     const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 445bd4a..a3e6a59 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -29,11 +29,11 @@
 import {EngineMode, PendingDeeplinkState, ProfileType} from '../common/state';
 import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
 import {
-  defaultTraceTime,
+  defaultTraceContext,
   globals,
   QuantizedLoad,
   ThreadDesc,
-  TraceTime,
+  TraceContext,
 } from '../frontend/globals';
 import {
   clearOverviewData,
@@ -41,7 +41,7 @@
   publishMetricError,
   publishOverviewData,
   publishThreads,
-  publishTraceDetails,
+  publishTraceContext,
 } from '../frontend/publish';
 import {addQueryResultsTab} from '../frontend/query_result_tab';
 import {Router} from '../frontend/router';
@@ -452,7 +452,7 @@
     const traceUuid = await this.cacheCurrentTrace();
 
     const traceDetails = await getTraceTimeDetails(this.engine);
-    publishTraceDetails(traceDetails);
+    publishTraceContext(traceDetails);
 
     const shownJsonWarning =
       window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
@@ -1100,8 +1100,8 @@
   // if we have non-default visible state, update the visible time to it
   const previousVisibleState = globals.stateVisibleTime();
   const defaultTraceSpan = new TimeSpan(
-    defaultTraceTime.start,
-    defaultTraceTime.end,
+    defaultTraceContext.start,
+    defaultTraceContext.end,
   );
   if (
     !(
@@ -1122,7 +1122,7 @@
   let visibleEnd = traceEnd;
 
   // compare start and end with metadata computed by the trace processor
-  const mdTime = await engine.getTracingMetadataTimeBounds();
+  const mdTime = await getTracingMetadataTimeBounds(engine);
   // make sure the bounds hold
   if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
     visibleStart = Time.max(visibleStart, mdTime.start);
@@ -1145,8 +1145,8 @@
   return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd);
 }
 
-async function getTraceTimeDetails(engine: EngineBase): Promise<TraceTime> {
-  const traceTime = await engine.getTraceTimeBounds();
+async function getTraceTimeDetails(engine: Engine): Promise<TraceContext> {
+  const traceTime = await getTraceTimeBounds(engine);
 
   // Find the first REALTIME or REALTIME_COARSE clock snapshot.
   // Prioritize REALTIME over REALTIME_COARSE.
@@ -1216,5 +1216,67 @@
     realtimeOffset,
     utcOffset,
     traceTzOffset,
+    cpus: await getCpus(engine),
+    gpuCount: await getNumberOfGpus(engine),
   };
 }
+
+async function getTraceTimeBounds(
+  engine: Engine,
+): Promise<Span<time, duration>> {
+  const result = await engine.query(
+    `select start_ts as startTs, end_ts as endTs from trace_bounds`,
+  );
+  const bounds = result.firstRow({
+    startTs: LONG,
+    endTs: LONG,
+  });
+  return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
+}
+
+// TODO(hjd): When streaming must invalidate this somehow.
+async function getCpus(engine: Engine): Promise<number[]> {
+  const cpus = [];
+  const queryRes = await engine.query(
+    'select distinct(cpu) as cpu from sched order by cpu;',
+  );
+  for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+    cpus.push(it.cpu);
+  }
+  return cpus;
+}
+
+async function getNumberOfGpus(engine: Engine): Promise<number> {
+  const result = await engine.query(`
+    select count(distinct(gpu_id)) as gpuCount
+    from gpu_counter_track
+    where name = 'gpufreq';
+  `);
+  return result.firstRow({gpuCount: NUM}).gpuCount;
+}
+
+async function getTracingMetadataTimeBounds(
+  engine: Engine,
+): Promise<Span<time, duration>> {
+  const queryRes = await engine.query(`select
+       name,
+       int_value as intValue
+       from metadata
+       where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
+       or name = 'all_data_source_started_ns'`);
+  let startBound = Time.MIN;
+  let endBound = Time.MAX;
+  const it = queryRes.iter({name: STR, intValue: LONG_NULL});
+  for (; it.valid(); it.next()) {
+    const columnName = it.name;
+    const timestamp = it.intValue;
+    if (timestamp === null) continue;
+    if (columnName === 'tracing_disabled_ns') {
+      endBound = Time.min(endBound, Time.fromRaw(timestamp));
+    } else {
+      startBound = Time.max(startBound, Time.fromRaw(timestamp));
+    }
+  }
+
+  return new TimeSpan(startBound, endBound);
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index e1b907c..a9b186d 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -118,7 +118,7 @@
   }
 
   async addCpuSchedulingTracks(): Promise<void> {
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
     const cpuToSize = await this.guessCpuSizes();
 
     for (const cpu of cpus) {
@@ -134,7 +134,7 @@
   }
 
   async addCpuFreqTracks(engine: Engine): Promise<void> {
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
 
     for (const cpu of cpus) {
       // Only add a cpu freq track if we have
@@ -189,38 +189,38 @@
       parentName: STR_NULL,
     });
 
-    const parentIdToGroupId = new Map<number, string>();
+    const parentIdToGroupKey = new Map<number, string>();
     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 parentTrackId = it.parentId;
-      let trackGroup = SCROLLING_TRACK_GROUP;
+      let groupKey = SCROLLING_TRACK_GROUP;
 
       if (parentTrackId !== null) {
-        const groupId = parentIdToGroupId.get(parentTrackId);
-        if (groupId === undefined) {
-          trackGroup = uuidv4();
-          parentIdToGroupId.set(parentTrackId, trackGroup);
+        const maybeGroupKey = parentIdToGroupKey.get(parentTrackId);
+        if (maybeGroupKey === undefined) {
+          groupKey = uuidv4();
+          parentIdToGroupKey.set(parentTrackId, groupKey);
 
           const parentName = getTrackName({name: rawParentName, kind});
           this.addTrackGroupActions.push(
             Actions.addTrackGroup({
               name: parentName,
-              id: trackGroup,
+              key: groupKey,
               collapsed: true,
             }),
           );
         } else {
-          trackGroup = groupId;
+          groupKey = maybeGroupKey;
         }
       }
 
       const track: AddTrackArgs = {
         uri: `perfetto.AsyncSlices#${rawName}.${it.parentId}`,
         trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-        trackGroup,
+        trackGroup: groupKey,
         name,
       };
 
@@ -229,7 +229,7 @@
   }
 
   async addGpuFreqTracks(engine: Engine): Promise<void> {
-    const numGpus = await this.engine.getNumberOfGpus();
+    const numGpus = globals.traceContext.gpuCount;
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
       // gpu freq data.
@@ -315,7 +315,7 @@
       return;
     }
 
-    const id = uuidv4();
+    const groupUuid = uuidv4();
     const summaryTrackKey = uuidv4();
     let foundSummary = false;
 
@@ -328,14 +328,14 @@
         track.key = summaryTrackKey;
         track.trackGroup = undefined;
       } else {
-        track.trackGroup = id;
+        track.trackGroup = groupUuid;
       }
     }
 
     const addGroup = Actions.addTrackGroup({
       summaryTrackKey,
       name: MEM_DMA_COUNTER_NAME,
-      id,
+      key: groupUuid,
       collapsed: true,
     });
     this.addTrackGroupActions.push(addGroup);
@@ -369,7 +369,7 @@
       const groupName = group + key;
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: value,
+        key: value,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -408,7 +408,7 @@
       const groupName = key;
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: value,
+        key: value,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -441,7 +441,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -483,7 +483,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -511,7 +511,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -537,7 +537,7 @@
       summaryTrackKey: string;
     }
 
-    const groupNameToIds = new Map<string, GroupIds>();
+    const groupNameToKeys = new Map<string, GroupIds>();
 
     for (; sliceIt.valid(); sliceIt.next()) {
       const id = sliceIt.id;
@@ -553,13 +553,13 @@
         // If this is the first track encountered for a certain group,
         // create an id for the group and use this track as the group's
         // summary track.
-        const groupIds = groupNameToIds.get(groupName);
-        if (groupIds) {
-          trackGroupId = groupIds.id;
+        const groupKeys = groupNameToKeys.get(groupName);
+        if (groupKeys) {
+          trackGroupId = groupKeys.id;
         } else {
           trackGroupId = uuidv4();
           summaryTrackKey = uuidv4();
-          groupNameToIds.set(groupName, {
+          groupNameToKeys.set(groupName, {
             id: trackGroupId,
             summaryTrackKey,
           });
@@ -575,11 +575,11 @@
       });
     }
 
-    for (const [groupName, groupIds] of groupNameToIds) {
+    for (const [groupName, groupKeys] of groupNameToKeys) {
       const addGroup = Actions.addTrackGroup({
-        summaryTrackKey: groupIds.summaryTrackKey,
+        summaryTrackKey: groupKeys.summaryTrackKey,
         name: groupName,
-        id: groupIds.id,
+        key: groupKeys.id,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -843,7 +843,7 @@
     for (const [name, groupUuid] of groupMap) {
       const addGroup = Actions.addTrackGroup({
         name: name,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -1153,7 +1153,7 @@
     const addTrackGroup = Actions.addTrackGroup({
       summaryTrackKey,
       name: `Kernel threads`,
-      id: kthreadGroupUuid,
+      key: kthreadGroupUuid,
       collapsed: true,
     });
     this.addTrackGroupActions.push(addTrackGroup);
@@ -1320,7 +1320,7 @@
       const addTrackGroup = Actions.addTrackGroup({
         summaryTrackKey,
         name,
-        id: this.getOrCreateUuid(utid, upid),
+        key: this.getOrCreateUuid(utid, upid),
         // Perf profiling tracks remain collapsed, otherwise we would have too
         // many expanded process tracks for some perf traces, leading to
         // jankyness.
@@ -1373,7 +1373,7 @@
           groupUuid = uuidv4();
           const addGroup = Actions.addTrackGroup({
             name: groupName,
-            id: groupUuid,
+            key: groupUuid,
             collapsed: true,
             fixedOrdering: true,
           });
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 15e5d2c..4f5460e 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -44,7 +44,6 @@
   'perfetto.CpuProfile',
   'perfetto.CpuSlices',
   'perfetto.CriticalUserInteraction',
-  'perfetto.CustomSqlTrack',
   'perfetto.DebugSlices',
   'perfetto.Flows',
   'perfetto.Frames',
@@ -58,4 +57,5 @@
   'perfetto.ThreadState',
   'perfetto.VisualisedArgs',
   'org.kernel.LinuxKernelDevices',
+  'perfetto.TrackUtils',
 ];
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index bda66f3..43ccb9d 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -18,7 +18,8 @@
   SLICE_TRACK_KIND,
 } from '../chrome_slices/chrome_slice_track';
 import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
-import {COUNTER_TRACK_KIND, TraceProcessorCounterTrack} from '../counter';
+import {COUNTER_TRACK_KIND} from '../counter';
+import {TraceProcessorCounterTrack} from '../counter/trace_processor_counter_track';
 
 class AnnotationPlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts b/ui/src/core_plugins/async_slices/async_slice_track.ts
similarity index 95%
rename from ui/src/core_plugins/async_slices/async_slice_track_v2.ts
rename to ui/src/core_plugins/async_slices/async_slice_track.ts
index 659a89a..275c418 100644
--- a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts
+++ b/ui/src/core_plugins/async_slices/async_slice_track.ts
@@ -17,7 +17,7 @@
 import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public';
 
-export class AsyncSliceTrackV2 extends NamedSliceTrack {
+export class AsyncSliceTrack extends NamedSliceTrack {
   constructor(
     args: NewTrackArgs,
     maxDepth: number,
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index 644d22f..b01cb38 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -16,7 +16,7 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
-import {AsyncSliceTrackV2} from './async_slice_track_v2';
+import {AsyncSliceTrack} from './async_slice_track';
 
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
 
@@ -71,7 +71,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
         },
       });
     }
@@ -123,7 +123,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2(
+          return new AsyncSliceTrack(
             {engine: ctx.engine, trackKey},
             maxDepth,
             trackIds,
@@ -190,7 +190,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
         },
       });
     }
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
new file mode 100644
index 0000000..9268750
--- /dev/null
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -0,0 +1,180 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
+import {globals} from '../../frontend/globals';
+import {
+  NAMED_ROW,
+  NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NUM, Slice, STR} from '../../public';
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlImportConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+
+import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
+import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+
+export const CRITICAL_USER_INTERACTIONS_KIND =
+  'org.chromium.CriticalUserInteraction.track';
+
+export const CRITICAL_USER_INTERACTIONS_ROW = {
+  ...NAMED_ROW,
+  scopedId: NUM,
+  type: STR,
+};
+export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
+
+export interface CriticalUserInteractionSlice extends Slice {
+  scopedId: number;
+  type: string;
+}
+
+export interface CriticalUserInteractionSliceTrackTypes
+  extends NamedSliceTrackTypes {
+  slice: CriticalUserInteractionSlice;
+  row: CriticalUserInteractionRow;
+}
+
+enum CriticalUserInteractionType {
+  UNKNOWN = 'Unknown',
+  PAGE_LOAD = 'chrome_page_loads',
+  STARTUP = 'chrome_startups',
+  WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
+}
+
+function convertToCriticalUserInteractionType(
+  cujType: string,
+): CriticalUserInteractionType {
+  switch (cujType) {
+    case CriticalUserInteractionType.PAGE_LOAD:
+      return CriticalUserInteractionType.PAGE_LOAD;
+    case CriticalUserInteractionType.STARTUP:
+      return CriticalUserInteractionType.STARTUP;
+    case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+      return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
+    default:
+      return CriticalUserInteractionType.UNKNOWN;
+  }
+}
+
+export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
+  static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      columns: [
+        // The scoped_id is not a unique identifier within the table; generate
+        // a unique id from type and scoped_id on the fly to use for slice
+        // selection.
+        'hash(type, scoped_id) AS id',
+        'scoped_id AS scopedId',
+        'name',
+        'ts',
+        'dur',
+        'type',
+      ],
+      sqlTableName: 'chrome_interactions',
+    };
+  }
+
+  getDetailsPanel(
+    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+  ): CustomSqlDetailsPanelConfig {
+    let detailsPanel = {
+      kind: GenericSliceDetailsTab.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Chrome Interaction',
+      },
+    };
+
+    switch (convertToCriticalUserInteractionType(args.slice.type)) {
+      case CriticalUserInteractionType.PAGE_LOAD:
+        detailsPanel = {
+          kind: PageLoadDetailsPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Page Load',
+          },
+        };
+        break;
+      case CriticalUserInteractionType.STARTUP:
+        detailsPanel = {
+          kind: StartupDetailsPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Startup',
+          },
+        };
+        break;
+      case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+        detailsPanel = {
+          kind: WebContentInteractionPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Web Content Interaction',
+          },
+        };
+        break;
+      default:
+        break;
+    }
+    return detailsPanel;
+  }
+
+  onSliceClick(
+    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+  ) {
+    const detailsPanelConfig = this.getDetailsPanel(args);
+    globals.makeSelection(
+      Actions.selectGenericSlice({
+        id: args.slice.scopedId,
+        sqlTableName: this.tableName,
+        start: args.slice.ts,
+        duration: args.slice.dur,
+        trackKey: this.trackKey,
+        detailsPanelConfig: {
+          kind: detailsPanelConfig.kind,
+          config: detailsPanelConfig.config,
+        },
+      }),
+    );
+  }
+
+  getSqlImports(): CustomSqlImportConfig {
+    return {
+      modules: ['chrome.interactions'],
+    };
+  }
+
+  getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
+    return CRITICAL_USER_INTERACTIONS_ROW;
+  }
+
+  rowToSlice(
+    row: CriticalUserInteractionSliceTrackTypes['row'],
+  ): CriticalUserInteractionSliceTrackTypes['slice'] {
+    const baseSlice = super.rowToSlice(row);
+    const scopedId = row.scopedId;
+    const type = row.type;
+    return {...baseSlice, scopedId, type};
+  }
+}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index faa863b..b8d7050 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -16,186 +16,23 @@
 
 import {Actions} from '../../common/actions';
 import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {
-  GenericSliceDetailsTab,
-  GenericSliceDetailsTabConfig,
-} from '../../frontend/generic_slice_details_tab';
+import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {globals} from '../../frontend/globals';
 import {
-  NAMED_ROW,
-  NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {
   BottomTabToSCSAdapter,
-  NUM,
   Plugin,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
-  Slice,
-  STR,
 } from '../../public';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlImportConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
 
 import {PageLoadDetailsPanel} from './page_load_details_panel';
 import {StartupDetailsPanel} from './startup_details_panel';
 import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
 
-export const CRITICAL_USER_INTERACTIONS_KIND =
-  'org.chromium.CriticalUserInteraction.track';
-
-export const CRITICAL_USER_INTERACTIONS_ROW = {
-  ...NAMED_ROW,
-  scopedId: NUM,
-  type: STR,
-};
-export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
-
-export interface CriticalUserInteractionSlice extends Slice {
-  scopedId: number;
-  type: string;
-}
-
-export interface CriticalUserInteractionSliceTrackTypes
-  extends NamedSliceTrackTypes {
-  slice: CriticalUserInteractionSlice;
-  row: CriticalUserInteractionRow;
-}
-
-enum CriticalUserInteractionType {
-  UNKNOWN = 'Unknown',
-  PAGE_LOAD = 'chrome_page_loads',
-  STARTUP = 'chrome_startups',
-  WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
-}
-
-function convertToCriticalUserInteractionType(
-  cujType: string,
-): CriticalUserInteractionType {
-  switch (cujType) {
-    case CriticalUserInteractionType.PAGE_LOAD:
-      return CriticalUserInteractionType.PAGE_LOAD;
-    case CriticalUserInteractionType.STARTUP:
-      return CriticalUserInteractionType.STARTUP;
-    case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-      return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
-    default:
-      return CriticalUserInteractionType.UNKNOWN;
-  }
-}
-
-export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
-  static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      columns: [
-        // The scoped_id is not a unique identifier within the table; generate
-        // a unique id from type and scoped_id on the fly to use for slice
-        // selection.
-        'hash(type, scoped_id) AS id',
-        'scoped_id AS scopedId',
-        'name',
-        'ts',
-        'dur',
-        'type',
-      ],
-      sqlTableName: 'chrome_interactions',
-    };
-  }
-
-  getDetailsPanel(
-    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
-  ): CustomSqlDetailsPanelConfig {
-    let detailsPanel = {
-      kind: GenericSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Interaction',
-      },
-    };
-
-    switch (convertToCriticalUserInteractionType(args.slice.type)) {
-      case CriticalUserInteractionType.PAGE_LOAD:
-        detailsPanel = {
-          kind: PageLoadDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Page Load',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.STARTUP:
-        detailsPanel = {
-          kind: StartupDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Startup',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-        detailsPanel = {
-          kind: WebContentInteractionPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Web Content Interaction',
-          },
-        };
-        break;
-      default:
-        break;
-    }
-    return detailsPanel;
-  }
-
-  onSliceClick(
-    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
-  ) {
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    globals.makeSelection(
-      Actions.selectGenericSlice({
-        id: args.slice.scopedId,
-        sqlTableName: this.tableName,
-        start: args.slice.ts,
-        duration: args.slice.dur,
-        trackKey: this.trackKey,
-        detailsPanelConfig: {
-          kind: detailsPanelConfig.kind,
-          config: detailsPanelConfig.config,
-        },
-      }),
-    );
-  }
-
-  getSqlImports(): CustomSqlImportConfig {
-    return {
-      modules: ['chrome.interactions'],
-    };
-  }
-
-  getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
-    return CRITICAL_USER_INTERACTIONS_ROW;
-  }
-
-  rowToSlice(
-    row: CriticalUserInteractionSliceTrackTypes['row'],
-  ): CriticalUserInteractionSliceTrackTypes['slice'] {
-    const baseSlice = super.rowToSlice(row);
-    const scopedId = row.scopedId;
-    const type = row.type;
-    return {...baseSlice, scopedId, type};
-  }
-}
-
-export function addCriticalUserInteractionTrack() {
+function addCriticalUserInteractionTrack() {
   const trackKey = uuidv4();
   globals.dispatchMultiple([
     Actions.addTrack({
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
index 8c20cc4..523880d 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/common.ts
@@ -15,7 +15,7 @@
 import {AddTrackArgs} from '../../common/actions';
 import {ObjectByKey} from '../../common/state';
 import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
+import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
 
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 6d89daa..98f3dd3 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -20,7 +20,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 4267f4c..aae51c4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -84,7 +84,7 @@
 
   const addTrackGroup = Actions.addTrackGroup({
     name: 'Chrome Scroll Jank',
-    id: SCROLL_JANK_GROUP_ID,
+    key: SCROLL_JANK_GROUP_ID,
     collapsed: false,
     fixedOrdering: true,
   });
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index f775740..11d6f1f 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -20,7 +20,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {EventLatencyTrackTypes} from './event_latency_track';
 import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 123d8f9..2d69139 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -19,7 +19,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 import {
   DecideTracksResult,
   SCROLL_JANK_GROUP_ID,
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 55773cd..7ac31d0 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -14,14 +14,10 @@
 
 import m from 'mithril';
 
-import {Time} from '../../base/time';
-import {Actions} from '../../common/actions';
 import {CounterDetailsPanel} from '../../frontend/counter_panel';
-import {globals} from '../../frontend/globals';
 import {
   NUM_NULL,
   STR_NULL,
-  LONG,
   LONG_NULL,
   NUM,
   Plugin,
@@ -31,11 +27,8 @@
   STR,
 } from '../../public';
 import {getTrackName} from '../../public/utils';
-import {
-  BaseCounterTrack,
-  BaseCounterTrackArgs,
-  CounterOptions,
-} from '../../frontend/base_counter_track';
+import {CounterOptions} from '../../frontend/base_counter_track';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
 
 export const COUNTER_TRACK_KIND = 'CounterTrack';
 
@@ -112,82 +105,6 @@
   return options;
 }
 
-interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
-  trackId: number;
-  rootTable?: string;
-}
-
-export class TraceProcessorCounterTrack extends BaseCounterTrack {
-  private trackId: number;
-  private rootTable: string;
-
-  constructor(args: TraceProcessorCounterTrackArgs) {
-    super(args);
-    this.trackId = args.trackId;
-    this.rootTable = args.rootTable ?? 'counter';
-  }
-
-  getSqlSource() {
-    return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
-  }
-
-  onMouseClick({x}: {x: number}): boolean {
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
-
-    const query = `
-      select
-        id,
-        ts as leftTs,
-        (
-          select ts
-          from ${this.rootTable}
-          where
-            track_id = ${this.trackId}
-            and ts >= ${time}
-          order by ts
-          limit 1
-        ) as rightTs
-      from ${this.rootTable}
-      where
-        track_id = ${this.trackId}
-        and ts < ${time}
-      order by ts DESC
-      limit 1
-    `;
-
-    this.engine.query(query).then((result) => {
-      const it = result.iter({
-        id: NUM,
-        leftTs: LONG,
-        rightTs: LONG_NULL,
-      });
-      if (!it.valid()) {
-        return;
-      }
-      const trackKey = this.trackKey;
-      const id = it.id;
-      const leftTs = Time.fromRaw(it.leftTs);
-
-      // TODO(stevegolton): Don't try to guess times and durations here, make it
-      // obvious to the user that this counter sample has no duration as it's
-      // the last one in the series
-      const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
-
-      globals.makeSelection(
-        Actions.selectCounter({
-          leftTs,
-          rightTs,
-          id,
-          trackKey,
-        }),
-      );
-    });
-
-    return true;
-  }
-}
-
 class CounterPlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addCounterTracks(ctx);
@@ -421,7 +338,7 @@
 
   private async addGpuFrequencyTracks(ctx: PluginContextTrace) {
     const engine = ctx.engine;
-    const numGpus = await engine.getNumberOfGpus();
+    const numGpus = ctx.trace.gpuCount;
 
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
new file mode 100644
index 0000000..a80ac4b
--- /dev/null
+++ b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
@@ -0,0 +1,98 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {LONG, LONG_NULL, NUM} from '../../public';
+import {
+  BaseCounterTrack,
+  BaseCounterTrackArgs,
+} from '../../frontend/base_counter_track';
+
+interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
+  trackId: number;
+  rootTable?: string;
+}
+
+export class TraceProcessorCounterTrack extends BaseCounterTrack {
+  private trackId: number;
+  private rootTable: string;
+
+  constructor(args: TraceProcessorCounterTrackArgs) {
+    super(args);
+    this.trackId = args.trackId;
+    this.rootTable = args.rootTable ?? 'counter';
+  }
+
+  getSqlSource() {
+    return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
+  }
+
+  onMouseClick({x}: {x: number}): boolean {
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
+
+    const query = `
+      select
+        id,
+        ts as leftTs,
+        (
+          select ts
+          from ${this.rootTable}
+          where
+            track_id = ${this.trackId}
+            and ts >= ${time}
+          order by ts
+          limit 1
+        ) as rightTs
+      from ${this.rootTable}
+      where
+        track_id = ${this.trackId}
+        and ts < ${time}
+      order by ts DESC
+      limit 1
+    `;
+
+    this.engine.query(query).then((result) => {
+      const it = result.iter({
+        id: NUM,
+        leftTs: LONG,
+        rightTs: LONG_NULL,
+      });
+      if (!it.valid()) {
+        return;
+      }
+      const trackKey = this.trackKey;
+      const id = it.id;
+      const leftTs = Time.fromRaw(it.leftTs);
+
+      // TODO(stevegolton): Don't try to guess times and durations here, make it
+      // obvious to the user that this counter sample has no duration as it's
+      // the last one in the series
+      const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
+
+      globals.makeSelection(
+        Actions.selectCounter({
+          leftTs,
+          rightTs,
+          id,
+          trackKey,
+        }),
+      );
+    });
+
+    return true;
+  }
+}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index 07b9b1d..a1d3947 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -73,14 +73,14 @@
 
   async onCreate() {
     if (this.config.idleTrackId === undefined) {
-      await this.engine.execute(`
+      await this.engine.query(`
         create view raw_freq_idle_${this.trackUuid} as
         select ts, dur, value as freqValue, -1 as idleValue
         from experimental_counter_dur c
         where track_id = ${this.config.freqTrackId}
       `);
     } else {
-      await this.engine.execute(`
+      await this.engine.query(`
         create view raw_freq_${this.trackUuid} as
         select ts, dur, value as freqValue
         from experimental_counter_dur c
@@ -99,7 +99,7 @@
       `);
     }
 
-    await this.engine.execute(`
+    await this.engine.query(`
       create virtual table cpu_freq_${this.trackUuid}
       using __intrinsic_counter_mipmap((
         select ts, freqValue as value
@@ -119,13 +119,15 @@
   }
 
   async onDestroy(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table cpu_freq_${this.trackUuid}`);
-      await this.engine.query(`drop table cpu_idle_${this.trackUuid}`);
-      await this.engine.query(`drop table raw_freq_idle_${this.trackUuid}`);
-      await this.engine.query(`drop view if exists raw_freq_${this.trackUuid}`);
-      await this.engine.query(`drop view if exists raw_idle_${this.trackUuid}`);
-    }
+    await this.engine.tryQuery(`drop table cpu_freq_${this.trackUuid}`);
+    await this.engine.tryQuery(`drop table cpu_idle_${this.trackUuid}`);
+    await this.engine.tryQuery(`drop table raw_freq_idle_${this.trackUuid}`);
+    await this.engine.tryQuery(
+      `drop view if exists raw_freq_${this.trackUuid}`,
+    );
+    await this.engine.tryQuery(
+      `drop view if exists raw_idle_${this.trackUuid}`,
+    );
   }
 
   async onBoundsChange(
@@ -403,7 +405,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
 
-    const cpus = await engine.getCpus();
+    const cpus = ctx.trace.cpus;
 
     const maxCpuFreqResult = await engine.query(`
       select ifnull(max(value), 0) as freq
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
new file mode 100644
index 0000000..802e341
--- /dev/null
+++ b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
@@ -0,0 +1,248 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {getLegacySelection} from '../../common/state';
+import {Actions} from '../../common/actions';
+import {colorForSample} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+
+const BAR_HEIGHT = 3;
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+interface Data extends TrackData {
+  ids: Float64Array;
+  tsStarts: BigInt64Array;
+  callsiteId: Uint32Array;
+}
+
+export class CpuProfileTrack implements Track {
+  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
+  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
+  private hoveredTs: time | undefined = undefined;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+  private engine: Engine;
+  private utid: number;
+
+  constructor(engine: Engine, utid: number) {
+    this.engine = engine;
+    this.utid = utid;
+  }
+
+  async onUpdate(): Promise<void> {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    const query = `select
+        id,
+        ts,
+        callsite_id as callsiteId
+      from cpu_profile_stack_sample
+      where utid = ${this.utid}
+      order by ts`;
+
+    const result = await this.engine.query(query);
+    const numRows = result.numRows();
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      ids: new Float64Array(numRows),
+      tsStarts: new BigInt64Array(numRows),
+      callsiteId: new Uint32Array(numRows),
+    };
+
+    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      data.ids[row] = it.id;
+      data.tsStarts[row] = it.ts;
+      data.callsiteId[row] = it.callsiteId;
+    }
+
+    return data;
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher.dispose();
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT - 1;
+  }
+
+  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+    const {visibleTimeScale: timeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return;
+
+    for (let i = 0; i < data.tsStarts.length; i++) {
+      const centerX = Time.fromRaw(data.tsStarts[i]);
+      const selection = getLegacySelection(globals.state);
+      const isHovered = this.hoveredTs === centerX;
+      const isSelected =
+        selection !== null &&
+        selection.kind === 'CPU_PROFILE_SAMPLE' &&
+        selection.ts === centerX;
+      const strokeWidth = isSelected ? 3 : 0;
+      this.drawMarker(
+        ctx,
+        timeScale.timeToPx(centerX),
+        this.centerY,
+        isHovered,
+        strokeWidth,
+        data.callsiteId[i],
+      );
+    }
+
+    // Group together identical identical CPU profile samples by connecting them
+    // with an horizontal bar.
+    let clusterStartIndex = 0;
+    while (clusterStartIndex < data.tsStarts.length) {
+      const callsiteId = data.callsiteId[clusterStartIndex];
+
+      // Find the end of the cluster by searching for the next different CPU
+      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
+      // inclusive and within array bounds.
+      let clusterEndIndex = clusterStartIndex;
+      while (
+        clusterEndIndex + 1 < data.tsStarts.length &&
+        data.callsiteId[clusterEndIndex + 1] === callsiteId
+      ) {
+        clusterEndIndex++;
+      }
+
+      // If there are multiple CPU samples in the cluster, draw a line.
+      if (clusterStartIndex !== clusterEndIndex) {
+        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
+        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
+        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
+        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
+        const width = rightPx - leftPx;
+        ctx.fillStyle = colorForSample(callsiteId, false);
+        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
+      }
+
+      // Move to the next cluster.
+      clusterStartIndex = clusterEndIndex + 1;
+    }
+  }
+
+  drawMarker(
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    isHovered: boolean,
+    strokeWidth: number,
+    callsiteId: number,
+  ): void {
+    ctx.beginPath();
+    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
+    ctx.lineTo(x, y + this.markerWidth);
+    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
+    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
+    ctx.closePath();
+    ctx.fillStyle = colorForSample(callsiteId, isHovered);
+    ctx.fill();
+    if (strokeWidth > 0) {
+      ctx.strokeStyle = colorForSample(callsiteId, false);
+      ctx.lineWidth = strokeWidth;
+      ctx.stroke();
+    }
+  }
+
+  onMouseMove({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    const {visibleTimeScale: timeScale} = globals.timeline;
+    const time = timeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+    this.hoveredTs =
+      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+  }
+
+  onMouseOut() {
+    this.hoveredTs = undefined;
+  }
+
+  onMouseClick({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale: timeScale} = globals.timeline;
+
+    const time = timeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+
+    if (index !== -1) {
+      const id = data.ids[index];
+      const ts = Time.fromRaw(data.tsStarts[index]);
+
+      globals.makeSelection(
+        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
+      );
+      return true;
+    }
+    return false;
+  }
+
+  // If the markers overlap the rightmost one will be selected.
+  findTimestampIndex(
+    left: number,
+    timeScale: TimeScale,
+    data: Data,
+    x: number,
+    y: number,
+    right: number,
+  ): number {
+    let index = -1;
+    if (left !== -1) {
+      const start = Time.fromRaw(data.tsStarts[left]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = left;
+      }
+    }
+    if (right !== -1) {
+      const start = Time.fromRaw(data.tsStarts[right]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = right;
+      }
+    }
+    return index;
+  }
+
+  isInMarker(x: number, y: number, centerX: number) {
+    return (
+      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+    );
+  }
+}
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 364225d..9e8fe84 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -12,255 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {getLegacySelection} from '../../common/state';
-import {Actions} from '../../common/actions';
-import {colorForSample} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
 import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Track,
-} from '../../public';
-import {
-  LONG,
-  NUM,
-  NUM_NULL,
-  STR_NULL,
-} from '../../trace_processor/query_result';
-
-const BAR_HEIGHT = 3;
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {CpuProfileTrack} from './cpu_profile_track';
 
 export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
 
-interface Data extends TrackData {
-  ids: Float64Array;
-  tsStarts: BigInt64Array;
-  callsiteId: Uint32Array;
-}
-
-class CpuProfileTrack implements Track {
-  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
-  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private utid: number;
-
-  constructor(engine: Engine, utid: number) {
-    this.engine = engine;
-    this.utid = utid;
-  }
-
-  async onUpdate(): Promise<void> {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const query = `select
-        id,
-        ts,
-        callsite_id as callsiteId
-      from cpu_profile_stack_sample
-      where utid = ${this.utid}
-      order by ts`;
-
-    const result = await this.engine.query(query);
-    const numRows = result.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      ids: new Float64Array(numRows),
-      tsStarts: new BigInt64Array(numRows),
-      callsiteId: new Uint32Array(numRows),
-    };
-
-    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
-    for (let row = 0; it.valid(); it.next(), ++row) {
-      data.ids[row] = it.id;
-      data.tsStarts[row] = it.ts;
-      data.callsiteId[row] = it.callsiteId;
-    }
-
-    return data;
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher.dispose();
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
-    const {visibleTimeScale: timeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'CPU_PROFILE_SAMPLE' &&
-        selection.ts === centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        timeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-        data.callsiteId[i],
-      );
-    }
-
-    // Group together identical identical CPU profile samples by connecting them
-    // with an horizontal bar.
-    let clusterStartIndex = 0;
-    while (clusterStartIndex < data.tsStarts.length) {
-      const callsiteId = data.callsiteId[clusterStartIndex];
-
-      // Find the end of the cluster by searching for the next different CPU
-      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
-      // inclusive and within array bounds.
-      let clusterEndIndex = clusterStartIndex;
-      while (
-        clusterEndIndex + 1 < data.tsStarts.length &&
-        data.callsiteId[clusterEndIndex + 1] === callsiteId
-      ) {
-        clusterEndIndex++;
-      }
-
-      // If there are multiple CPU samples in the cluster, draw a line.
-      if (clusterStartIndex !== clusterEndIndex) {
-        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
-        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
-        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
-        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
-        const width = rightPx - leftPx;
-        ctx.fillStyle = colorForSample(callsiteId, false);
-        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
-      }
-
-      // Move to the next cluster.
-      clusterStartIndex = clusterEndIndex + 1;
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-    callsiteId: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = colorForSample(callsiteId, isHovered);
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = colorForSample(callsiteId, false);
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const {visibleTimeScale: timeScale} = globals.timeline;
-    const time = timeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale: timeScale} = globals.timeline;
-
-    const time = timeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
-
-    if (index !== -1) {
-      const id = data.ids[index];
-      const ts = Time.fromRaw(data.tsStarts[index]);
-
-      globals.makeSelection(
-        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
-    );
-  }
-}
-
 class CpuProfile implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
new file mode 100644
index 0000000..70acb78
--- /dev/null
+++ b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
@@ -0,0 +1,474 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {search, searchEq, searchSegment} from '../../base/binary_search';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Duration, duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {getLegacySelection} from '../../common/state';
+import {
+  cropText,
+  drawDoubleHeadedArrow,
+  drawIncompleteSlice,
+  drawTrackHoverTooltip,
+} from '../../common/canvas_utils';
+import {Color} from '../../core/color';
+import {colorForThread} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+
+export interface Data extends TrackData {
+  // Slices are stored in a columnar fashion. All fields have the same length.
+  ids: Float64Array;
+  startQs: BigInt64Array;
+  endQs: BigInt64Array;
+  utids: Uint32Array;
+  flags: Uint8Array;
+  lastRowId: number;
+}
+
+const MARGIN_TOP = 3;
+const RECT_HEIGHT = 24;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
+const CPU_SLICE_FLAGS_INCOMPLETE = 1;
+const CPU_SLICE_FLAGS_REALTIME = 2;
+
+export class CpuSliceTrack implements Track {
+  private mousePos?: {x: number; y: number};
+  private utidHoveredInThisTrack = -1;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private lastRowId = -1;
+  private engine: Engine;
+  private cpu: number;
+  private trackKey: string;
+  private trackUuid = uuidv4Sql();
+
+  constructor(engine: Engine, trackKey: string, cpu: number) {
+    this.engine = engine;
+    this.trackKey = trackKey;
+    this.cpu = cpu;
+  }
+
+  async onCreate() {
+    await this.engine.query(`
+      create virtual table cpu_slice_${this.trackUuid}
+      using __intrinsic_slice_mipmap((
+        select
+          id,
+          ts,
+          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
+          0 as depth
+        from sched
+        where cpu = ${this.cpu} and utid != 0
+      ));
+    `);
+    const it = await this.engine.query(`
+      select coalesce(max(id), -1) as lastRowId
+      from sched
+      where cpu = ${this.cpu} and utid != 0
+    `);
+    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
+  }
+
+  async onUpdate() {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const queryRes = await this.engine.query(`
+      select
+        (z.ts / ${resolution}) * ${resolution} as tsQ,
+        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
+        s.utid,
+        s.id,
+        s.dur = -1 as isIncomplete,
+        ifnull(s.priority < 100, 0) as isRealtime
+      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
+      cross join sched s using (id)
+    `);
+
+    const numRows = queryRes.numRows();
+    const slices: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      lastRowId: this.lastRowId,
+      ids: new Float64Array(numRows),
+      startQs: new BigInt64Array(numRows),
+      endQs: new BigInt64Array(numRows),
+      utids: new Uint32Array(numRows),
+      flags: new Uint8Array(numRows),
+    };
+
+    const it = queryRes.iter({
+      tsQ: LONG,
+      tsEndQ: LONG,
+      utid: NUM,
+      id: NUM,
+      isIncomplete: NUM,
+      isRealtime: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      slices.startQs[row] = it.tsQ;
+      slices.endQs[row] = it.tsEndQ;
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
+
+      slices.flags[row] = 0;
+      if (it.isIncomplete) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
+      }
+      if (it.isRealtime) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
+      }
+    }
+    return slices;
+  }
+
+  async onDestroy() {
+    await this.engine.tryQuery(
+      `drop table if exists cpu_slice_${this.trackUuid}`,
+    );
+    this.fetcher.dispose();
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const {visibleTimeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      visibleTimeScale.timeToPx(data.start),
+      visibleTimeScale.timeToPx(data.end),
+    );
+
+    this.renderSlices(ctx, data);
+  }
+
+  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
+    const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
+      globals.timeline;
+    assertTrue(data.startQs.length === data.endQs.length);
+    assertTrue(data.startQs.length === data.utids.length);
+
+    const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
+
+    ctx.textAlign = 'center';
+    ctx.font = '12px Roboto Condensed';
+    const charWidth = ctx.measureText('dbpqaouk').width / 8;
+
+    const startTime = visibleTimeSpan.start;
+    const endTime = visibleTimeSpan.end;
+
+    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
+    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
+
+    for (let i = startIdx; i < endIdx; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      let tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+
+      // If the last slice is incomplete, it should end with the end of the
+      // window, else it might spill over the window and the end would not be
+      // visible as a zigzag line.
+      if (
+        data.ids[i] === data.lastRowId &&
+        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
+      ) {
+        tEnd = endTime;
+      }
+      const rectStart = visibleTimeScale.timeToPx(tStart);
+      const rectEnd = visibleTimeScale.timeToPx(tEnd);
+      const rectWidth = Math.max(1, rectEnd - rectStart);
+
+      const threadInfo = globals.threads.get(utid);
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
+
+      const isHovering = globals.state.hoveredUtid !== -1;
+      const isThreadHovered = globals.state.hoveredUtid === utid;
+      const isProcessHovered = globals.state.hoveredPid === pid;
+      const colorScheme = colorForThread(threadInfo);
+      let color: Color;
+      let textColor: Color;
+      if (isHovering && !isThreadHovered) {
+        if (!isProcessHovered) {
+          color = colorScheme.disabled;
+          textColor = colorScheme.textDisabled;
+        } else {
+          color = colorScheme.variant;
+          textColor = colorScheme.textVariant;
+        }
+      } else {
+        color = colorScheme.base;
+        textColor = colorScheme.textBase;
+      }
+      ctx.fillStyle = color.cssString;
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
+        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      } else {
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // Don't render text when we have less than 5px to play with.
+      if (rectWidth < 5) continue;
+
+      // Stylize real-time threads. We don't do it when zoomed out as the
+      // fillRect is expensive.
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        ctx.fillStyle = getHatchedPattern(ctx);
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // TODO: consider de-duplicating this code with the copied one from
+      // chrome_slices/frontend.ts.
+      let title = `[utid:${utid}]`;
+      let subTitle = '';
+      if (threadInfo) {
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
+        if (threadInfo.pid) {
+          /* eslint-enable */
+          let procName = threadInfo.procName || '';
+          if (procName.startsWith('/')) {
+            // Remove folder paths from name
+            procName = procName.substring(procName.lastIndexOf('/') + 1);
+          }
+          title = `${procName} [${threadInfo.pid}]`;
+          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        } else {
+          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        }
+      }
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        subTitle = subTitle + ' (RT)';
+      }
+
+      const right = Math.min(visWindowEndPx, rectEnd);
+      const left = Math.max(rectStart, 0);
+      const visibleWidth = Math.max(right - left, 1);
+      title = cropText(title, charWidth, visibleWidth);
+      subTitle = cropText(subTitle, charWidth, visibleWidth);
+      const rectXCenter = left + visibleWidth / 2;
+      ctx.fillStyle = textColor.cssString;
+      ctx.font = '12px Roboto Condensed';
+      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
+      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
+      ctx.font = '10px Roboto Condensed';
+      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
+    }
+
+    const selection = getLegacySelection(globals.state);
+    const details = globals.sliceDetails;
+    if (selection !== null && selection.kind === 'SLICE') {
+      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
+      if (startIndex !== endIndex) {
+        const tStart = Time.fromRaw(data.startQs[startIndex]);
+        const tEnd = Time.fromRaw(data.endQs[startIndex]);
+        const utid = data.utids[startIndex];
+        const color = colorForThread(globals.threads.get(utid));
+        const rectStart = visibleTimeScale.timeToPx(tStart);
+        const rectEnd = visibleTimeScale.timeToPx(tEnd);
+        const rectWidth = Math.max(1, rectEnd - rectStart);
+
+        // Draw a rectangle around the slice that is currently selected.
+        ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
+        ctx.beginPath();
+        ctx.lineWidth = 3;
+        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
+        ctx.closePath();
+        // Draw arrow from wakeup time of current slice.
+        if (details.wakeupTs) {
+          const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
+          const latencyWidth = rectStart - wakeupPos;
+          drawDoubleHeadedArrow(
+            ctx,
+            wakeupPos,
+            MARGIN_TOP + RECT_HEIGHT,
+            latencyWidth,
+            latencyWidth >= 20,
+          );
+          // Latency time with a white semi-transparent background.
+          const latency = tStart - details.wakeupTs;
+          const displayText = Duration.humanise(latency);
+          const measured = ctx.measureText(displayText);
+          if (latencyWidth >= measured.width + 2) {
+            ctx.fillStyle = 'rgba(255,255,255,0.7)';
+            ctx.fillRect(
+              wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
+              MARGIN_TOP + RECT_HEIGHT - 12,
+              measured.width + 2,
+              11,
+            );
+            ctx.textBaseline = 'bottom';
+            ctx.fillStyle = 'black';
+            ctx.fillText(
+              displayText,
+              wakeupPos + latencyWidth / 2,
+              MARGIN_TOP + RECT_HEIGHT - 1,
+            );
+          }
+        }
+      }
+
+      // Draw diamond if the track being drawn is the cpu of the waker.
+      if (this.cpu === details.wakerCpu && details.wakeupTs) {
+        const wakeupPos = Math.floor(
+          visibleTimeScale.timeToPx(details.wakeupTs),
+        );
+        ctx.beginPath();
+        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
+        ctx.fillStyle = 'black';
+        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
+        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.fill();
+        ctx.closePath();
+      }
+    }
+
+    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
+    const maxHeight = this.getHeight();
+    if (hoveredThread !== undefined && this.mousePos !== undefined) {
+      const tidText = `T: ${hoveredThread.threadName}
+      [${hoveredThread.tid}]`;
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (hoveredThread.pid) {
+        const pidText = `P: ${hoveredThread.procName}
+        [${hoveredThread.pid}]`;
+        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
+      } else {
+        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
+      }
+    }
+  }
+
+  onMouseMove(pos: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    this.mousePos = pos;
+    if (data === undefined) return;
+    const {visibleTimeScale} = globals.timeline;
+    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
+      this.utidHoveredInThisTrack = -1;
+      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+      return;
+    }
+    const t = visibleTimeScale.pxToHpTime(pos.x);
+    let hoveredUtid = -1;
+
+    for (let i = 0; i < data.startQs.length; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      const tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+      if (t.gte(tStart) && t.lt(tEnd)) {
+        hoveredUtid = utid;
+        break;
+      }
+    }
+    this.utidHoveredInThisTrack = hoveredUtid;
+    const threadInfo = globals.threads.get(hoveredUtid);
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
+    globals.dispatch(
+      Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
+    );
+  }
+
+  onMouseOut() {
+    this.utidHoveredInThisTrack = -1;
+    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+    this.mousePos = undefined;
+  }
+
+  onMouseClick({x}: {x: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x);
+    const index = search(data.startQs, time.toTime());
+    const id = index === -1 ? undefined : data.ids[index];
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    if (!id || this.utidHoveredInThisTrack === -1) return false;
+
+    globals.setLegacySelection(
+      {
+        kind: 'SLICE',
+        id,
+        trackKey: this.trackKey,
+      },
+      {
+        clearSearch: true,
+        pendingScrollId: undefined,
+        switchToCurrentSelectionTab: true,
+      },
+    );
+
+    return true;
+  }
+}
+
+// Creates a diagonal hatched pattern to be used for distinguishing slices with
+// real-time priorities. The pattern is created once as an offscreen canvas and
+// is kept cached inside the Context2D of the main canvas, without making
+// assumptions on the lifetime of the main canvas.
+function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
+  const mctx = mainCtx as CanvasRenderingContext2D & {
+    sliceHatchedPattern?: CanvasPattern;
+  };
+  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
+  const canvas = document.createElement('canvas');
+  const SIZE = 8;
+  canvas.width = canvas.height = SIZE;
+  const ctx = assertExists(canvas.getContext('2d'));
+  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
+  ctx.beginPath();
+  ctx.lineWidth = 1;
+  ctx.moveTo(0, SIZE);
+  ctx.lineTo(SIZE, 0);
+  ctx.stroke();
+  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
+  return mctx.sliceHatchedPattern;
+}
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index 1fc1e67..3f456a3 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -12,458 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {search, searchEq, searchSegment} from '../../base/binary_search';
-import {assertExists, assertTrue} from '../../base/logging';
-import {Duration, duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {getLegacySelection} from '../../common/state';
-import {
-  cropText,
-  drawDoubleHeadedArrow,
-  drawIncompleteSlice,
-  drawTrackHoverTooltip,
-} from '../../common/canvas_utils';
-import {Color} from '../../core/color';
-import {colorForThread} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
 import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
 import {
   Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
-  Track,
 } from '../../public';
-import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
+import {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {CpuSliceTrack} from './cpu_slice_track';
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
 
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  ids: Float64Array;
-  startQs: BigInt64Array;
-  endQs: BigInt64Array;
-  utids: Uint32Array;
-  flags: Uint8Array;
-  lastRowId: number;
-}
-
-const MARGIN_TOP = 3;
-const RECT_HEIGHT = 24;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-const CPU_SLICE_FLAGS_INCOMPLETE = 1;
-const CPU_SLICE_FLAGS_REALTIME = 2;
-
-class CpuSliceTrack implements Track {
-  private mousePos?: {x: number; y: number};
-  private utidHoveredInThisTrack = -1;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private lastRowId = -1;
-  private engine: Engine;
-  private cpu: number;
-  private trackKey: string;
-  private trackUuid = uuidv4Sql();
-
-  constructor(engine: Engine, trackKey: string, cpu: number) {
-    this.engine = engine;
-    this.trackKey = trackKey;
-    this.cpu = cpu;
-  }
-
-  async onCreate() {
-    await this.engine.query(`
-      create virtual table cpu_slice_${this.trackUuid}
-      using __intrinsic_slice_mipmap((
-        select
-          id,
-          ts,
-          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
-          0 as depth
-        from sched
-        where cpu = ${this.cpu} and utid != 0
-      ));
-    `);
-    const it = await this.engine.query(`
-      select coalesce(max(id), -1) as lastRowId
-      from sched
-      where cpu = ${this.cpu} and utid != 0
-    `);
-    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
-  }
-
-  async onUpdate() {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const queryRes = await this.engine.query(`
-      select
-        (z.ts / ${resolution}) * ${resolution} as tsQ,
-        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
-        s.utid,
-        s.id,
-        s.dur = -1 as isIncomplete,
-        ifnull(s.priority < 100, 0) as isRealtime
-      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
-      cross join sched s using (id)
-    `);
-
-    const numRows = queryRes.numRows();
-    const slices: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      lastRowId: this.lastRowId,
-      ids: new Float64Array(numRows),
-      startQs: new BigInt64Array(numRows),
-      endQs: new BigInt64Array(numRows),
-      utids: new Uint32Array(numRows),
-      flags: new Uint8Array(numRows),
-    };
-
-    const it = queryRes.iter({
-      tsQ: LONG,
-      tsEndQ: LONG,
-      utid: NUM,
-      id: NUM,
-      isIncomplete: NUM,
-      isRealtime: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), row++) {
-      slices.startQs[row] = it.tsQ;
-      slices.endQs[row] = it.tsEndQ;
-      slices.utids[row] = it.utid;
-      slices.ids[row] = it.id;
-
-      slices.flags[row] = 0;
-      if (it.isIncomplete) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
-      }
-      if (it.isRealtime) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
-      }
-    }
-    return slices;
-  }
-
-  async onDestroy() {
-    if (this.engine.isAlive) {
-      await this.engine.query(
-        `drop table if exists cpu_slice_${this.trackUuid}`,
-      );
-    }
-    this.fetcher.dispose();
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const {visibleTimeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      visibleTimeScale.timeToPx(data.start),
-      visibleTimeScale.timeToPx(data.end),
-    );
-
-    this.renderSlices(ctx, data);
-  }
-
-  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
-    const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
-      globals.timeline;
-    assertTrue(data.startQs.length === data.endQs.length);
-    assertTrue(data.startQs.length === data.utids.length);
-
-    const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
-
-    ctx.textAlign = 'center';
-    ctx.font = '12px Roboto Condensed';
-    const charWidth = ctx.measureText('dbpqaouk').width / 8;
-
-    const startTime = visibleTimeSpan.start;
-    const endTime = visibleTimeSpan.end;
-
-    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
-    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
-
-    for (let i = startIdx; i < endIdx; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      let tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-
-      // If the last slice is incomplete, it should end with the end of the
-      // window, else it might spill over the window and the end would not be
-      // visible as a zigzag line.
-      if (
-        data.ids[i] === data.lastRowId &&
-        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
-      ) {
-        tEnd = endTime;
-      }
-      const rectStart = visibleTimeScale.timeToPx(tStart);
-      const rectEnd = visibleTimeScale.timeToPx(tEnd);
-      const rectWidth = Math.max(1, rectEnd - rectStart);
-
-      const threadInfo = globals.threads.get(utid);
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
-
-      const isHovering = globals.state.hoveredUtid !== -1;
-      const isThreadHovered = globals.state.hoveredUtid === utid;
-      const isProcessHovered = globals.state.hoveredPid === pid;
-      const colorScheme = colorForThread(threadInfo);
-      let color: Color;
-      let textColor: Color;
-      if (isHovering && !isThreadHovered) {
-        if (!isProcessHovered) {
-          color = colorScheme.disabled;
-          textColor = colorScheme.textDisabled;
-        } else {
-          color = colorScheme.variant;
-          textColor = colorScheme.textVariant;
-        }
-      } else {
-        color = colorScheme.base;
-        textColor = colorScheme.textBase;
-      }
-      ctx.fillStyle = color.cssString;
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
-        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      } else {
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // Don't render text when we have less than 5px to play with.
-      if (rectWidth < 5) continue;
-
-      // Stylize real-time threads. We don't do it when zoomed out as the
-      // fillRect is expensive.
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        ctx.fillStyle = getHatchedPattern(ctx);
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // TODO: consider de-duplicating this code with the copied one from
-      // chrome_slices/frontend.ts.
-      let title = `[utid:${utid}]`;
-      let subTitle = '';
-      if (threadInfo) {
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        if (threadInfo.pid) {
-          /* eslint-enable */
-          let procName = threadInfo.procName || '';
-          if (procName.startsWith('/')) {
-            // Remove folder paths from name
-            procName = procName.substring(procName.lastIndexOf('/') + 1);
-          }
-          title = `${procName} [${threadInfo.pid}]`;
-          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        } else {
-          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        }
-      }
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        subTitle = subTitle + ' (RT)';
-      }
-
-      const right = Math.min(visWindowEndPx, rectEnd);
-      const left = Math.max(rectStart, 0);
-      const visibleWidth = Math.max(right - left, 1);
-      title = cropText(title, charWidth, visibleWidth);
-      subTitle = cropText(subTitle, charWidth, visibleWidth);
-      const rectXCenter = left + visibleWidth / 2;
-      ctx.fillStyle = textColor.cssString;
-      ctx.font = '12px Roboto Condensed';
-      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
-      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
-      ctx.font = '10px Roboto Condensed';
-      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
-    }
-
-    const selection = getLegacySelection(globals.state);
-    const details = globals.sliceDetails;
-    if (selection !== null && selection.kind === 'SLICE') {
-      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
-      if (startIndex !== endIndex) {
-        const tStart = Time.fromRaw(data.startQs[startIndex]);
-        const tEnd = Time.fromRaw(data.endQs[startIndex]);
-        const utid = data.utids[startIndex];
-        const color = colorForThread(globals.threads.get(utid));
-        const rectStart = visibleTimeScale.timeToPx(tStart);
-        const rectEnd = visibleTimeScale.timeToPx(tEnd);
-        const rectWidth = Math.max(1, rectEnd - rectStart);
-
-        // Draw a rectangle around the slice that is currently selected.
-        ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
-        ctx.beginPath();
-        ctx.lineWidth = 3;
-        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
-        ctx.closePath();
-        // Draw arrow from wakeup time of current slice.
-        if (details.wakeupTs) {
-          const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
-          const latencyWidth = rectStart - wakeupPos;
-          drawDoubleHeadedArrow(
-            ctx,
-            wakeupPos,
-            MARGIN_TOP + RECT_HEIGHT,
-            latencyWidth,
-            latencyWidth >= 20,
-          );
-          // Latency time with a white semi-transparent background.
-          const latency = tStart - details.wakeupTs;
-          const displayText = Duration.humanise(latency);
-          const measured = ctx.measureText(displayText);
-          if (latencyWidth >= measured.width + 2) {
-            ctx.fillStyle = 'rgba(255,255,255,0.7)';
-            ctx.fillRect(
-              wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
-              MARGIN_TOP + RECT_HEIGHT - 12,
-              measured.width + 2,
-              11,
-            );
-            ctx.textBaseline = 'bottom';
-            ctx.fillStyle = 'black';
-            ctx.fillText(
-              displayText,
-              wakeupPos + latencyWidth / 2,
-              MARGIN_TOP + RECT_HEIGHT - 1,
-            );
-          }
-        }
-      }
-
-      // Draw diamond if the track being drawn is the cpu of the waker.
-      if (this.cpu === details.wakerCpu && details.wakeupTs) {
-        const wakeupPos = Math.floor(
-          visibleTimeScale.timeToPx(details.wakeupTs),
-        );
-        ctx.beginPath();
-        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
-        ctx.fillStyle = 'black';
-        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
-        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.fill();
-        ctx.closePath();
-      }
-    }
-
-    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    const maxHeight = this.getHeight();
-    if (hoveredThread !== undefined && this.mousePos !== undefined) {
-      const tidText = `T: ${hoveredThread.threadName}
-      [${hoveredThread.tid}]`;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (hoveredThread.pid) {
-        const pidText = `P: ${hoveredThread.procName}
-        [${hoveredThread.pid}]`;
-        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
-      } else {
-        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
-      }
-    }
-  }
-
-  onMouseMove(pos: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    this.mousePos = pos;
-    if (data === undefined) return;
-    const {visibleTimeScale} = globals.timeline;
-    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
-      this.utidHoveredInThisTrack = -1;
-      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-      return;
-    }
-    const t = visibleTimeScale.pxToHpTime(pos.x);
-    let hoveredUtid = -1;
-
-    for (let i = 0; i < data.startQs.length; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      const tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-      if (t.gte(tStart) && t.lt(tEnd)) {
-        hoveredUtid = utid;
-        break;
-      }
-    }
-    this.utidHoveredInThisTrack = hoveredUtid;
-    const threadInfo = globals.threads.get(hoveredUtid);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
-    globals.dispatch(
-      Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
-    );
-  }
-
-  onMouseOut() {
-    this.utidHoveredInThisTrack = -1;
-    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mousePos = undefined;
-  }
-
-  onMouseClick({x}: {x: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x);
-    const index = search(data.startQs, time.toTime());
-    const id = index === -1 ? undefined : data.ids[index];
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!id || this.utidHoveredInThisTrack === -1) return false;
-
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id,
-        trackKey: this.trackKey,
-      },
-      {
-        clearSearch: true,
-        pendingScrollId: undefined,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-
-    return true;
-  }
-}
-
 class CpuSlices implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const cpus = await ctx.engine.getCpus();
+    const cpus = ctx.trace.cpus;
     const cpuToSize = await this.guessCpuSizes(ctx.engine);
 
     for (const cpu of cpus) {
@@ -516,29 +79,6 @@
   }
 }
 
-// Creates a diagonal hatched pattern to be used for distinguishing slices with
-// real-time priorities. The pattern is created once as an offscreen canvas and
-// is kept cached inside the Context2D of the main canvas, without making
-// assumptions on the lifetime of the main canvas.
-function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
-  const mctx = mainCtx as CanvasRenderingContext2D & {
-    sliceHatchedPattern?: CanvasPattern;
-  };
-  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
-  const canvas = document.createElement('canvas');
-  const SIZE = 8;
-  canvas.width = canvas.height = SIZE;
-  const ctx = assertExists(canvas.getContext('2d'));
-  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
-  ctx.beginPath();
-  ctx.lineWidth = 1;
-  ctx.moveTo(0, SIZE);
-  ctx.lineTo(SIZE, 0);
-  ctx.stroke();
-  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
-  return mctx.sliceHatchedPattern;
-}
-
 export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CpuSlices',
   plugin: CpuSlices,
diff --git a/ui/src/core_plugins/debug/counter_track.ts b/ui/src/core_plugins/debug/counter_track.ts
index 56274d3..5c0022d 100644
--- a/ui/src/core_plugins/debug/counter_track.ts
+++ b/ui/src/core_plugins/debug/counter_track.ts
@@ -67,8 +67,6 @@
   }
 
   private async dropTrackTable(): Promise<void> {
-    if (this.engine.isAlive) {
-      this.engine.query(`drop table if exists ${this.sqlTableName}`);
-    }
+    this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/core_plugins/debug/slice_track.ts b/ui/src/core_plugins/debug/slice_track.ts
index 49e5142..a0734d9 100644
--- a/ui/src/core_plugins/debug/slice_track.ts
+++ b/ui/src/core_plugins/debug/slice_track.ts
@@ -19,7 +19,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {DebugSliceDetailsTab} from './details_tab';
 import {
@@ -111,8 +111,6 @@
   }
 
   private async destroyTrackTable() {
-    if (this.engine.isAlive) {
-      await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/core_plugins/frames/actual_frames_track_v2.ts b/ui/src/core_plugins/frames/actual_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/actual_frames_track_v2.ts
rename to ui/src/core_plugins/frames/actual_frames_track.ts
diff --git a/ui/src/core_plugins/frames/expected_frames_track_v2.ts b/ui/src/core_plugins/frames/expected_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/expected_frames_track_v2.ts
rename to ui/src/core_plugins/frames/expected_frames_track.ts
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 2cc877f..8e67de9 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -16,8 +16,8 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
-import {ActualFramesTrack as ActualFramesTrackV2} from './actual_frames_track_v2';
-import {ExpectedFramesTrack as ExpectedFramesTrackV2} from './expected_frames_track_v2';
+import {ActualFramesTrack} from './actual_frames_track';
+import {ExpectedFramesTrack} from './expected_frames_track';
 
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
@@ -75,12 +75,7 @@
         trackIds,
         kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new ExpectedFramesTrackV2(
-            engine,
-            maxDepth,
-            trackKey,
-            trackIds,
-          );
+          return new ExpectedFramesTrack(engine, maxDepth, trackKey, trackIds);
         },
       });
     }
@@ -138,7 +133,7 @@
         trackIds,
         kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new ActualFramesTrackV2(engine, maxDepth, trackKey, trackIds);
+          return new ActualFramesTrack(engine, maxDepth, trackKey, trackIds);
         },
       });
     }
diff --git a/ui/src/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
new file mode 100644
index 0000000..9ecd947
--- /dev/null
+++ b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
@@ -0,0 +1,108 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {ProfileType, LegacySelection} from '../../common/state';
+import {profileType} from '../../controller/flamegraph_controller';
+import {
+  BASE_ROW,
+  BaseSliceTrack,
+  BaseSliceTrackTypes,
+  OnSliceClickArgs,
+  OnSliceOverArgs,
+} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {NewTrackArgs} from '../../frontend/track';
+import {Slice} from '../../public';
+import {STR} from '../../trace_processor/query_result';
+
+export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
+
+const HEAP_PROFILE_ROW = {
+  ...BASE_ROW,
+  type: STR,
+};
+type HeapProfileRow = typeof HEAP_PROFILE_ROW;
+interface HeapProfileSlice extends Slice {
+  type: ProfileType;
+}
+
+interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
+  row: HeapProfileRow;
+  slice: HeapProfileSlice;
+}
+
+export class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
+  private upid: number;
+
+  constructor(args: NewTrackArgs, upid: number) {
+    super(args);
+    this.upid = upid;
+  }
+
+  getSqlSource(): string {
+    return `select
+      *,
+      0 AS dur,
+      0 AS depth
+      from (
+        select distinct
+          id,
+          ts,
+          'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
+        from heap_profile_allocation
+        where upid = ${this.upid}
+        union
+        select distinct
+          id,
+          graph_sample_ts AS ts,
+          'graph' AS type
+        from heap_graph_object
+        where upid = ${this.upid}
+      )`;
+  }
+
+  getRowSpec(): HeapProfileRow {
+    return HEAP_PROFILE_ROW;
+  }
+
+  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
+    const slice = super.rowToSlice(row);
+    let type = row.type;
+    if (type === 'heap_profile:libc.malloc,com.android.art') {
+      type = 'heap_profile:com.android.art,libc.malloc';
+    }
+    slice.type = profileType(type);
+    return slice;
+  }
+
+  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
+    args.tooltip = [args.slice.type];
+  }
+
+  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
+    globals.makeSelection(
+      Actions.selectHeapProfile({
+        id: args.slice.id,
+        upid: this.upid,
+        ts: args.slice.ts,
+        type: args.slice.type,
+      }),
+    );
+  }
+
+  protected isSelectionHandled(selection: LegacySelection): boolean {
+    return selection.kind === 'HEAP_PROFILE';
+  }
+}
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 512af1a..c3576f2 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -12,107 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../../common/actions';
-import {ProfileType, LegacySelection} from '../../common/state';
-import {profileType} from '../../controller/flamegraph_controller';
-import {
-  BASE_ROW,
-  BaseSliceTrack,
-  BaseSliceTrackTypes,
-  OnSliceClickArgs,
-  OnSliceOverArgs,
-} from '../../frontend/base_slice_track';
 import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs} from '../../frontend/track';
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Slice,
-} from '../../public';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {HeapProfileTrack} from './heap_profile_track';
 
 export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
 
-const HEAP_PROFILE_ROW = {
-  ...BASE_ROW,
-  type: STR,
-};
-type HeapProfileRow = typeof HEAP_PROFILE_ROW;
-interface HeapProfileSlice extends Slice {
-  type: ProfileType;
-}
-
-interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
-  row: HeapProfileRow;
-  slice: HeapProfileSlice;
-}
-
-class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
-  private upid: number;
-
-  constructor(args: NewTrackArgs, upid: number) {
-    super(args);
-    this.upid = upid;
-  }
-
-  getSqlSource(): string {
-    return `select
-      *,
-      0 AS dur,
-      0 AS depth
-      from (
-        select distinct
-          id,
-          ts,
-          'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
-        from heap_profile_allocation
-        where upid = ${this.upid}
-        union
-        select distinct
-          id,
-          graph_sample_ts AS ts,
-          'graph' AS type
-        from heap_graph_object
-        where upid = ${this.upid}
-      )`;
-  }
-
-  getRowSpec(): HeapProfileRow {
-    return HEAP_PROFILE_ROW;
-  }
-
-  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
-    const slice = super.rowToSlice(row);
-    let type = row.type;
-    if (type === 'heap_profile:libc.malloc,com.android.art') {
-      type = 'heap_profile:com.android.art,libc.malloc';
-    }
-    slice.type = profileType(type);
-    return slice;
-  }
-
-  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
-    args.tooltip = [args.slice.type];
-  }
-
-  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
-    globals.makeSelection(
-      Actions.selectHeapProfile({
-        id: args.slice.id,
-        upid: this.upid,
-        ts: args.slice.ts,
-        type: args.slice.type,
-      }),
-    );
-  }
-
-  protected isSelectionHandled(selection: LegacySelection): boolean {
-    return selection.kind === 'HEAP_PROFILE';
-  }
-}
-
 class HeapProfilePlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 319fb25..df9b7c0 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -12,25 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {ProfileType, getLegacySelection} from '../../common/state';
 import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Track,
-} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {PerfSamplesProfileTrack} from './perf_samples_profile_track';
 
 export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
 
@@ -38,211 +24,6 @@
   tsStarts: BigInt64Array;
 }
 
-const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
-
-class PerfSamplesProfileTrack implements Track {
-  private centerY = this.getHeight() / 2;
-  private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private upid: number;
-  private engine: Engine;
-
-  constructor(engine: Engine, upid: number) {
-    this.upid = upid;
-    this.engine = engine;
-  }
-
-  async onUpdate(): Promise<void> {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher.dispose();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    if (this.upid === undefined) {
-      return {
-        start,
-        end,
-        resolution,
-        length: 0,
-        tsStarts: new BigInt64Array(),
-      };
-    }
-    const queryRes = await this.engine.query(`
-      select ts, upid from perf_sample
-      join thread using (utid)
-      where upid = ${this.upid}
-      and callsite_id is not null
-      order by ts`);
-    const numRows = queryRes.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      tsStarts: new BigInt64Array(numRows),
-    };
-
-    const it = queryRes.iter({ts: LONG});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      data.tsStarts[row] = it.ts;
-    }
-    return data;
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
-    const {visibleTimeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'PERF_SAMPLES' &&
-        selection.leftTs <= centerX &&
-        selection.rightTs >= centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        visibleTimeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-      );
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y);
-    ctx.lineTo(x, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(
-      left,
-      visibleTimeScale,
-      data,
-      x,
-      y,
-      right,
-    );
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale} = globals.timeline;
-
-    const time = visibleTimeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(
-      left,
-      visibleTimeScale,
-      data,
-      x,
-      y,
-      right,
-    );
-
-    if (index !== -1) {
-      const ts = Time.fromRaw(data.tsStarts[index]);
-      globals.makeSelection(
-        Actions.selectPerfSamples({
-          id: index,
-          upid: this.upid,
-          leftTs: ts,
-          rightTs: ts,
-          type: ProfileType.PERF_SAMPLE,
-        }),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
-    );
-  }
-}
-
 class PerfSamplesProfilePlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
new file mode 100644
index 0000000..aca33de
--- /dev/null
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -0,0 +1,237 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {ProfileType, getLegacySelection} from '../../common/state';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG} from '../../trace_processor/query_result';
+
+export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
+
+export interface Data extends TrackData {
+  tsStarts: BigInt64Array;
+}
+
+const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+export class PerfSamplesProfileTrack implements Track {
+  private centerY = this.getHeight() / 2;
+  private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
+  private hoveredTs: time | undefined = undefined;
+  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
+  private upid: number;
+  private engine: Engine;
+
+  constructor(engine: Engine, upid: number) {
+    this.upid = upid;
+    this.engine = engine;
+  }
+
+  async onUpdate(): Promise<void> {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher.dispose();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    if (this.upid === undefined) {
+      return {
+        start,
+        end,
+        resolution,
+        length: 0,
+        tsStarts: new BigInt64Array(),
+      };
+    }
+    const queryRes = await this.engine.query(`
+      select ts, upid from perf_sample
+      join thread using (utid)
+      where upid = ${this.upid}
+      and callsite_id is not null
+      order by ts`);
+    const numRows = queryRes.numRows();
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      tsStarts: new BigInt64Array(numRows),
+    };
+
+    const it = queryRes.iter({ts: LONG});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.tsStarts[row] = it.ts;
+    }
+    return data;
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT - 1;
+  }
+
+  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+    const {visibleTimeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return;
+
+    for (let i = 0; i < data.tsStarts.length; i++) {
+      const centerX = Time.fromRaw(data.tsStarts[i]);
+      const selection = getLegacySelection(globals.state);
+      const isHovered = this.hoveredTs === centerX;
+      const isSelected =
+        selection !== null &&
+        selection.kind === 'PERF_SAMPLES' &&
+        selection.leftTs <= centerX &&
+        selection.rightTs >= centerX;
+      const strokeWidth = isSelected ? 3 : 0;
+      this.drawMarker(
+        ctx,
+        visibleTimeScale.timeToPx(centerX),
+        this.centerY,
+        isHovered,
+        strokeWidth,
+      );
+    }
+  }
+
+  drawMarker(
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    isHovered: boolean,
+    strokeWidth: number,
+  ): void {
+    ctx.beginPath();
+    ctx.moveTo(x, y - this.markerWidth);
+    ctx.lineTo(x - this.markerWidth, y);
+    ctx.lineTo(x, y + this.markerWidth);
+    ctx.lineTo(x + this.markerWidth, y);
+    ctx.lineTo(x, y - this.markerWidth);
+    ctx.closePath();
+    ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
+    ctx.fill();
+    if (strokeWidth > 0) {
+      ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
+      ctx.lineWidth = strokeWidth;
+      ctx.stroke();
+    }
+  }
+
+  onMouseMove({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+    const index = this.findTimestampIndex(
+      left,
+      visibleTimeScale,
+      data,
+      x,
+      y,
+      right,
+    );
+    this.hoveredTs =
+      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+  }
+
+  onMouseOut() {
+    this.hoveredTs = undefined;
+  }
+
+  onMouseClick({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale} = globals.timeline;
+
+    const time = visibleTimeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+    const index = this.findTimestampIndex(
+      left,
+      visibleTimeScale,
+      data,
+      x,
+      y,
+      right,
+    );
+
+    if (index !== -1) {
+      const ts = Time.fromRaw(data.tsStarts[index]);
+      globals.makeSelection(
+        Actions.selectPerfSamples({
+          id: index,
+          upid: this.upid,
+          leftTs: ts,
+          rightTs: ts,
+          type: ProfileType.PERF_SAMPLE,
+        }),
+      );
+      return true;
+    }
+    return false;
+  }
+
+  // If the markers overlap the rightmost one will be selected.
+  findTimestampIndex(
+    left: number,
+    timeScale: TimeScale,
+    data: Data,
+    x: number,
+    y: number,
+    right: number,
+  ): number {
+    let index = -1;
+    if (left !== -1) {
+      const start = Time.fromRaw(data.tsStarts[left]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = left;
+      }
+    }
+    if (right !== -1) {
+      const start = Time.fromRaw(data.tsStarts[right]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = right;
+      }
+    }
+    return index;
+  }
+
+  isInMarker(x: number, y: number, centerX: number) {
+    return (
+      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+    );
+  }
+}
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
index f2a7475..da51bb0 100644
--- a/ui/src/core_plugins/process_summary/index.ts
+++ b/ui/src/core_plugins/process_summary/index.ts
@@ -34,6 +34,8 @@
   }
 
   private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
+    const cpuCount = Math.max(...ctx.trace.cpus, -1) + 1;
+
     const result = await ctx.engine.query(`
       INCLUDE PERFETTO MODULE android.process_metadata;
 
@@ -106,7 +108,7 @@
             isDebuggable,
           },
           trackFactory: () => {
-            return new ProcessSchedulingTrack(ctx.engine, config);
+            return new ProcessSchedulingTrack(ctx.engine, config, cpuCount);
           },
         });
       } else {
diff --git a/ui/src/core_plugins/process_summary/process_scheduling_track.ts b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
index 28b536f..4b26432 100644
--- a/ui/src/core_plugins/process_summary/process_scheduling_track.ts
+++ b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
@@ -56,23 +56,18 @@
   private mousePos?: {x: number; y: number};
   private utidHoveredInThisTrack = -1;
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private maxCpu = 0;
+  private cpuCount: number;
   private engine: Engine;
   private trackUuid = uuidv4Sql();
   private config: Config;
 
-  constructor(engine: Engine, config: Config) {
+  constructor(engine: Engine, config: Config, cpuCount: number) {
     this.engine = engine;
     this.config = config;
+    this.cpuCount = cpuCount;
   }
 
   async onCreate(): Promise<void> {
-    const cpus = await this.engine.getCpus();
-
-    // A process scheduling track should only exist in a trace that has cpus.
-    assertTrue(cpus.length > 0);
-    this.maxCpu = Math.max(...cpus) + 1;
-
     if (this.config.upid !== null) {
       await this.engine.query(`
         create virtual table process_scheduling_${this.trackUuid}
@@ -119,11 +114,9 @@
 
   async onDestroy(): Promise<void> {
     this.fetcher.dispose();
-    if (this.engine.isAlive) {
-      await this.engine.query(`
-        drop table process_scheduling_${this.trackUuid}
-      `);
-    }
+    await this.engine.tryQuery(`
+      drop table process_scheduling_${this.trackUuid}
+    `);
   }
 
   async onBoundsChange(
@@ -142,7 +135,7 @@
       end,
       resolution,
       length: numRows,
-      maxCpu: this.maxCpu,
+      maxCpu: this.cpuCount,
       starts: new BigInt64Array(numRows),
       ends: new BigInt64Array(numRows),
       cpus: new Uint32Array(numRows),
diff --git a/ui/src/core_plugins/process_summary/process_summary_track.ts b/ui/src/core_plugins/process_summary/process_summary_track.ts
index 5fa31de..57abd75 100644
--- a/ui/src/core_plugins/process_summary/process_summary_track.ts
+++ b/ui/src/core_plugins/process_summary/process_summary_track.ts
@@ -171,13 +171,11 @@
   }
 
   async onDestroy(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(
-        `drop table if exists ${this.tableName(
-          'window',
-        )}; drop table if exists ${this.tableName('span')}`,
-      );
-    }
+    await this.engine.tryQuery(
+      `drop table if exists ${this.tableName(
+        'window',
+      )}; drop table if exists ${this.tableName('span')}`,
+    );
     this.fetcher.dispose();
   }
 
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index 029f818..f6eaebb 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -15,7 +15,6 @@
 import {uuidv4} from '../../base/uuid';
 import {AddTrackArgs} from '../../common/actions';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {
   BottomTabToSCSAdapter,
   NUM,
@@ -25,34 +24,9 @@
   PrimaryTrackSortKey,
 } from '../../public';
 import {Engine} from '../../trace_processor/engine';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
 
 import {ScreenshotTab} from './screenshot_panel';
-
-class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
-  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      sqlTableName: 'android_screenshots',
-      columns: ['*'],
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScreenshotTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Screenshots',
-      },
-    };
-  }
-}
+import {ScreenshotsTrack} from './screenshots_track';
 
 export type DecideTracksResult = {
   tracksToAdd: AddTrackArgs[];
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
new file mode 100644
index 0000000..a880b33
--- /dev/null
+++ b/ui/src/core_plugins/screenshots/screenshots_track.ts
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
+import {ScreenshotTab} from './screenshot_panel';
+
+export class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      sqlTableName: 'android_screenshots',
+      columns: ['*'],
+    };
+  }
+
+  getDetailsPanel(): CustomSqlDetailsPanelConfig {
+    return {
+      kind: ScreenshotTab.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Screenshots',
+      },
+    };
+  }
+}
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index 73e2faf..ab0bcc5 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -24,7 +24,7 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
 
-import {ThreadStateTrack as ThreadStateTrackV2} from './thread_state_v2';
+import {ThreadStateTrack} from './thread_state_track';
 
 export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
 
@@ -64,7 +64,7 @@
         kind: THREAD_STATE_TRACK_KIND,
         utid,
         trackFactory: ({trackKey}) => {
-          return new ThreadStateTrackV2(
+          return new ThreadStateTrack(
             {
               engine: ctx.engine,
               trackKey,
diff --git a/ui/src/core_plugins/thread_state/thread_state_v2.ts b/ui/src/core_plugins/thread_state/thread_state_track.ts
similarity index 100%
rename from ui/src/core_plugins/thread_state/thread_state_v2.ts
rename to ui/src/core_plugins/thread_state/thread_state_track.ts
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
new file mode 100644
index 0000000..937ba70
--- /dev/null
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -0,0 +1,100 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {
+  getTimeSpanOfSelectionOrVisibleWindow,
+  globals,
+} from '../../frontend/globals';
+import {OmniboxMode} from '../../frontend/omnibox_manager';
+import {verticalScrollToTrack} from '../../frontend/scroll_helper';
+import {
+  Plugin,
+  PluginContextTrace,
+  PluginDescriptor,
+  PromptOption,
+} from '../../public';
+
+class TrackUtilsPlugin implements Plugin {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    ctx.registerCommand({
+      id: 'perfetto.RunQueryInSelectedTimeWindow',
+      name: `Run query in selected time window`,
+      callback: () => {
+        const window = getTimeSpanOfSelectionOrVisibleWindow();
+        globals.omnibox.setMode(OmniboxMode.Query);
+        globals.omnibox.setText(
+          `select  where ts >= ${window.start} and ts < ${window.end}`,
+        );
+        globals.omnibox.focusOmnibox(7);
+      },
+    });
+
+    ctx.registerCommand({
+      // Selects & reveals the first track on the timeline with a given URI.
+      id: 'perfetto.FindTrack',
+      name: 'Find track by URI',
+      callback: async () => {
+        const tracks = globals.trackManager.getAllTracks();
+        const options = tracks.map(({uri}): PromptOption => {
+          return {key: uri, displayName: uri};
+        });
+
+        // Sort tracks in a natural sort order
+        const collator = new Intl.Collator('en', {
+          numeric: true,
+          sensitivity: 'base',
+        });
+        const sortedOptions = options.sort((a, b) => {
+          return collator.compare(a.displayName, b.displayName);
+        });
+
+        try {
+          const selectedUri = await ctx.prompt(
+            'Choose a track...',
+            sortedOptions,
+          );
+
+          // Find the first track with this URI
+          const firstTrack = Object.values(globals.state.tracks).find(
+            ({uri}) => uri === selectedUri,
+          );
+          if (firstTrack) {
+            console.log(firstTrack);
+            verticalScrollToTrack(firstTrack.key, true);
+            const traceTime = globals.stateTraceTimeTP();
+            globals.makeSelection(
+              Actions.selectArea({
+                area: {
+                  start: traceTime.start,
+                  end: traceTime.end,
+                  tracks: [firstTrack.key],
+                },
+              }),
+            );
+          } else {
+            alert(`No tracks with uri ${selectedUri} on the timeline`);
+          }
+        } catch {
+          // Prompt was probably cancelled - do nothing.
+        }
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.TrackUtils',
+  plugin: TrackUtilsPlugin,
+};
diff --git a/ui/src/core_plugins/visualised_args/visualized_args_track.ts b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
index 8de85d3..55cf49d 100644
--- a/ui/src/core_plugins/visualised_args/visualized_args_track.ts
+++ b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
@@ -68,9 +68,7 @@
     `);
 
     return new DisposableCallback(() => {
-      if (this.engine.isAlive) {
-        this.engine.query(`drop view ${this.viewName}`);
-      }
+      this.engine.tryQuery(`drop view ${this.viewName}`);
     });
   }
 
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 09b1036..6c90216 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -18,12 +18,10 @@
 import {Trash} from '../base/disposable';
 import {findRef} from '../base/dom_utils';
 import {FuzzyFinder} from '../base/fuzzy';
-import {assertExists} from '../base/logging';
+import {assertExists, assertUnreachable} from '../base/logging';
 import {undoCommonChatAppReplacements} from '../base/string_utils';
-import {duration, Span, time, TimeSpan} from '../base/time';
 import {Actions} from '../common/actions';
 import {getLegacySelection} from '../common/state';
-import {runQuery} from '../common/queries';
 import {
   DurationPrecision,
   setDurationPrecision,
@@ -31,28 +29,22 @@
   TimestampFormat,
 } from '../core/timestamp_format';
 import {raf} from '../core/raf_scheduler';
-import {Command} from '../public';
-import {Engine} from '../trace_processor/engine';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
+import {Command, Engine, addDebugSliceTrack} from '../public';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
 import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
 
 import {onClickCopy} from './clipboard';
 import {CookieConsent} from './cookie_consent';
-import {globals} from './globals';
+import {getTimeSpanOfSelectionOrVisibleWindow, globals} from './globals';
 import {toggleHelp} from './help_modal';
 import {Notes} from './notes';
 import {Omnibox, OmniboxOption} from './omnibox';
 import {addQueryResultsTab} from './query_result_tab';
-import {verticalScrollToTrack} from './scroll_helper';
 import {executeSearch} from './search_handler';
 import {Sidebar} from './sidebar';
-import {Utid} from './sql_types';
-import {getThreadInfo} from './thread_and_process_info';
 import {Topbar} from './topbar';
 import {shareTrace} from './trace_attrs';
-import {addDebugSliceTrack} from './debug_tracks';
 import {AggregationsTabs} from './aggregation_tab';
 import {addSqlTableTab} from './sql_table/tab';
 import {SqlTables} from './sql_table/well_known_tables';
@@ -62,12 +54,16 @@
   lockSliceSpan,
   moveByFocusedFlow,
 } from './keyboard_event_handler';
-import {exists} from '../base/utils';
+import {publishPermalinkHash} from './publish';
+import {OmniboxMode, PromptOption} from './omnibox_manager';
+import {Utid} from './sql_types';
+import {getThreadInfo} from './thread_and_process_info';
+import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
 
 function renderPermalink(): m.Children {
-  const permalink = globals.state.permalink;
-  if (!permalink.requestId || !permalink.hash) return null;
-  const url = `${self.location.origin}/#!/?s=${permalink.hash}`;
+  const hash = globals.permalinkHash;
+  if (!hash) return null;
+  const url = `${self.location.origin}/#!/?s=${hash}`;
   const linkProps = {title: 'Click to copy the URL', onclick: onClickCopy(url)};
 
   return m('.alert-permalink', [
@@ -75,7 +71,7 @@
     m(
       'button',
       {
-        onclick: () => globals.dispatch(Actions.clearPermalink({})),
+        onclick: () => publishPermalinkHash(undefined),
       },
       m('i.material-icons.disallow-selection', 'close'),
     ),
@@ -88,25 +84,6 @@
   }
 }
 
-interface PromptOption {
-  key: string;
-  displayName: string;
-}
-
-interface Prompt {
-  text: string;
-  options?: PromptOption[];
-  resolve(result: string): void;
-  reject(): void;
-}
-
-enum OmniboxMode {
-  Search,
-  Query,
-  Command,
-  Prompt,
-}
-
 const criticalPathSliceColumns = {
   ts: 'ts',
   dur: 'dur',
@@ -138,14 +115,6 @@
 
 export class App implements m.ClassComponent {
   private trash = new Trash();
-
-  private omniboxMode: OmniboxMode = OmniboxMode.Search;
-  private omniboxText = '';
-  private queryText = '';
-  private omniboxSelectionIndex = 0;
-  private focusOmniboxNextRender = false;
-  private pendingCursorPlacement = -1;
-  private pendingPrompt?: Prompt;
   static readonly OMNIBOX_INPUT_REF = 'omnibox';
   private omniboxInputEl?: HTMLInputElement;
   private recentCommands: string[] = [];
@@ -164,80 +133,6 @@
     return engine;
   }
 
-  private enterCommandMode(): void {
-    this.omniboxMode = OmniboxMode.Command;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-    this.focusOmniboxNextRender = true;
-
-    raf.scheduleFullRedraw();
-  }
-
-  private enterQueryMode(): void {
-    this.omniboxMode = OmniboxMode.Query;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-    this.focusOmniboxNextRender = true;
-
-    raf.scheduleFullRedraw();
-  }
-
-  private enterSearchMode(focusOmnibox: boolean): void {
-    this.omniboxMode = OmniboxMode.Search;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-
-    if (focusOmnibox) {
-      this.focusOmniboxNextRender = true;
-    }
-
-    globals.dispatch(Actions.setOmniboxMode({mode: 'SEARCH'}));
-
-    raf.scheduleFullRedraw();
-  }
-
-  // Start a prompt. If options are supplied, the user must pick one from the
-  // list, otherwise the input is free-form text.
-  private prompt(text: string, options?: PromptOption[]): Promise<string> {
-    this.omniboxMode = OmniboxMode.Prompt;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-
-    const promise = new Promise<string>((resolve, reject) => {
-      this.pendingPrompt = {
-        text,
-        options,
-        resolve,
-        reject,
-      };
-    });
-
-    this.focusOmniboxNextRender = true;
-    raf.scheduleFullRedraw();
-
-    return promise;
-  }
-
-  // Resolve the pending prompt with a value to return to the prompter.
-  private resolvePrompt(value: string): void {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.resolve(value);
-      this.pendingPrompt = undefined;
-    }
-    this.enterSearchMode(false);
-  }
-
-  // Reject the prompt outright. Doing this will force the owner of the prompt
-  // promise to catch, so only do this when things go seriously wrong.
-  // Use |resolvePrompt(null)| to indicate cancellation.
-  private rejectPrompt(): void {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.reject();
-      this.pendingPrompt = undefined;
-    }
-    this.enterSearchMode(false);
-  }
-
   private getFirstUtidOfSelectionOrVisibleWindow(): number {
     const selection = getLegacySelection(globals.state);
     if (selection && selection.kind === 'AREA') {
@@ -283,7 +178,7 @@
         const promptText = 'Select format...';
 
         try {
-          const result = await this.prompt(promptText, options);
+          const result = await globals.omnibox.prompt(promptText, options);
           setTimestampFormat(result as TimestampFormat);
           raf.scheduleFullRedraw();
         } catch {
@@ -305,7 +200,7 @@
         const promptText = 'Select duration precision mode...';
 
         try {
-          const result = await this.prompt(promptText, options);
+          const result = await globals.omnibox.prompt(promptText, options);
           setDurationPrecision(result as DurationPrecision);
           raf.scheduleFullRedraw();
         } catch {
@@ -322,9 +217,8 @@
         const engine = this.getEngine();
 
         if (engine !== undefined && trackUtid != 0) {
-          await runQuery(
+          await engine.query(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -365,9 +259,8 @@
         const engine = this.getEngine();
 
         if (engine !== undefined && trackUtid != 0) {
-          await runQuery(
+          await engine.query(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -454,19 +347,19 @@
     {
       id: 'perfetto.OpenCommandPalette',
       name: 'Open command palette',
-      callback: () => this.enterCommandMode(),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Command),
       defaultHotkey: '!Mod+Shift+P',
     },
     {
       id: 'perfetto.RunQuery',
       name: 'Run query',
-      callback: () => this.enterQueryMode(),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Query),
       defaultHotkey: '!Mod+O',
     },
     {
       id: 'perfetto.Search',
       name: 'Search',
-      callback: () => this.enterSearchMode(true),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Search),
       defaultHotkey: '/',
     },
     {
@@ -476,16 +369,6 @@
       defaultHotkey: '?',
     },
     {
-      id: 'perfetto.RunQueryInSelectedTimeWindow',
-      name: `Run query in selected time window`,
-      callback: () => {
-        const window = getTimeSpanOfSelectionOrVisibleWindow();
-        this.enterQueryMode();
-        this.queryText = `select  where ts >= ${window.start} and ts < ${window.end}`;
-        this.pendingCursorPlacement = 7;
-      },
-    },
-    {
       id: 'perfetto.CopyTimeWindow',
       name: `Copy selected time window to clipboard`,
       callback: () => {
@@ -495,56 +378,6 @@
       },
     },
     {
-      // Selects & reveals the first track on the timeline with a given URI.
-      id: 'perfetto.FindTrack',
-      name: 'Find track by URI',
-      callback: async () => {
-        const tracks = globals.trackManager.getAllTracks();
-        const options = tracks.map(({uri}): PromptOption => {
-          return {key: uri, displayName: uri};
-        });
-
-        // Sort tracks in a natural sort order
-        const collator = new Intl.Collator('en', {
-          numeric: true,
-          sensitivity: 'base',
-        });
-        const sortedOptions = options.sort((a, b) => {
-          return collator.compare(a.displayName, b.displayName);
-        });
-
-        try {
-          const selectedUri = await this.prompt(
-            'Choose a track...',
-            sortedOptions,
-          );
-
-          // Find the first track with this URI
-          const firstTrack = Object.values(globals.state.tracks).find(
-            ({uri}) => uri === selectedUri,
-          );
-          if (firstTrack) {
-            console.log(firstTrack);
-            verticalScrollToTrack(firstTrack.key, true);
-            const traceTime = globals.stateTraceTimeTP();
-            globals.makeSelection(
-              Actions.selectArea({
-                area: {
-                  start: traceTime.start,
-                  end: traceTime.end,
-                  tracks: [firstTrack.key],
-                },
-              }),
-            );
-          } else {
-            alert(`No tracks with uri ${selectedUri} on the timeline`);
-          }
-        } catch {
-          // Prompt was probably cancelled - do nothing.
-        }
-      },
-    },
-    {
       id: 'perfetto.FocusSelection',
       name: 'Focus current selection',
       callback: () => findCurrentSelection(),
@@ -620,8 +453,8 @@
         if (selection !== null && selection.kind === 'AREA') {
           const area = globals.state.areas[selection.areaId];
           const coversEntireTimeRange =
-            globals.traceTime.start === area.start &&
-            globals.traceTime.end === area.end;
+            globals.traceContext.start === area.start &&
+            globals.traceContext.end === area.end;
           if (!coversEntireTimeRange) {
             // If the current selection is an area which does not cover the
             // entire time range, preserve the list of selected tracks and
@@ -636,7 +469,7 @@
           // If the current selection is not an area, select all.
           tracksToSelect = Object.keys(globals.state.tracks);
         }
-        const {start, end} = globals.traceTime;
+        const {start, end} = globals.traceContext;
         globals.dispatch(
           Actions.selectArea({
             area: {
@@ -655,18 +488,6 @@
     return this.cmds;
   }
 
-  private rejectPendingPrompt() {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.reject();
-      this.pendingPrompt = undefined;
-    }
-  }
-
-  private resetOmnibox() {
-    this.omniboxText = '';
-    this.omniboxSelectionIndex = 0;
-  }
-
   private renderOmnibox(): m.Children {
     const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3;
     const engineIsBusy =
@@ -683,22 +504,23 @@
       );
     }
 
-    if (this.omniboxMode === OmniboxMode.Command) {
+    const omniboxMode = globals.omnibox.omniboxMode;
+
+    if (omniboxMode === OmniboxMode.Command) {
       return this.renderCommandOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Prompt) {
+    } else if (omniboxMode === OmniboxMode.Prompt) {
       return this.renderPromptOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Query) {
+    } else if (omniboxMode === OmniboxMode.Query) {
       return this.renderQueryOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Search) {
+    } else if (omniboxMode === OmniboxMode.Search) {
       return this.renderSearchOmnibox();
     } else {
-      const x: never = this.omniboxMode;
-      throw new Error(`Unhandled omnibox mode ${x}`);
+      assertUnreachable(omniboxMode);
     }
   }
 
   renderPromptOmnibox(): m.Children {
-    const prompt = assertExists(this.pendingPrompt);
+    const prompt = assertExists(globals.omnibox.pendingPrompt);
 
     let options: OmniboxOption[] | undefined = undefined;
 
@@ -707,7 +529,7 @@
         prompt.options,
         ({displayName}) => displayName,
       );
-      const result = fuzzy.find(this.omniboxText);
+      const result = fuzzy.find(globals.omnibox.text);
       options = result.map((result) => {
         return {
           key: result.item.key,
@@ -717,27 +539,27 @@
     }
 
     return m(Omnibox, {
-      value: this.omniboxText,
+      value: globals.omnibox.text,
       placeholder: prompt.text,
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'prompt-mode',
       closeOnOutsideClick: true,
       options,
-      selectedOptionIndex: this.omniboxSelectionIndex,
+      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
       onSelectedOptionChanged: (index) => {
-        this.omniboxSelectionIndex = index;
+        globals.omnibox.setOmniboxSelectionIndex(index);
         raf.scheduleFullRedraw();
       },
       onInput: (value) => {
-        this.omniboxText = value;
-        this.omniboxSelectionIndex = 0;
+        globals.omnibox.setText(value);
+        globals.omnibox.setOmniboxSelectionIndex(0);
         raf.scheduleFullRedraw();
       },
       onSubmit: (value, _alt) => {
-        this.resolvePrompt(value);
+        globals.omnibox.resolvePrompt(value);
       },
       onClose: () => {
-        this.rejectPrompt();
+        globals.omnibox.rejectPrompt();
       },
     });
   }
@@ -746,7 +568,7 @@
     const cmdMgr = globals.commandManager;
 
     // Fuzzy-filter commands by the filter string.
-    const filteredCmds = cmdMgr.fuzzyFilterCommands(this.omniboxText);
+    const filteredCmds = cmdMgr.fuzzyFilterCommands(globals.omnibox.text);
 
     // Create an array of commands with attached heuristics from the recent
     // command register.
@@ -777,36 +599,35 @@
     });
 
     return m(Omnibox, {
-      value: this.omniboxText,
+      value: globals.omnibox.text,
       placeholder: 'Filter commands...',
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'command-mode',
       options,
       closeOnSubmit: true,
       closeOnOutsideClick: true,
-      selectedOptionIndex: this.omniboxSelectionIndex,
+      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
       onSelectedOptionChanged: (index) => {
-        this.omniboxSelectionIndex = index;
+        globals.omnibox.setOmniboxSelectionIndex(index);
         raf.scheduleFullRedraw();
       },
       onInput: (value) => {
-        this.omniboxText = value;
-        this.omniboxSelectionIndex = 0;
+        globals.omnibox.setText(value);
+        globals.omnibox.setOmniboxSelectionIndex(0);
         raf.scheduleFullRedraw();
       },
       onClose: () => {
         if (this.omniboxInputEl) {
           this.omniboxInputEl.blur();
         }
-        this.enterSearchMode(false);
-        raf.scheduleFullRedraw();
+        globals.omnibox.reset();
       },
       onSubmit: (key: string) => {
         this.addRecentCommand(key);
         cmdMgr.runCommand(key);
       },
       onGoBack: () => {
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
       },
     });
   }
@@ -822,13 +643,13 @@
   renderQueryOmnibox(): m.Children {
     const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
     return m(Omnibox, {
-      value: this.queryText,
+      value: globals.omnibox.text,
       placeholder: ph,
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'query-mode',
 
       onInput: (value) => {
-        this.queryText = value;
+        globals.omnibox.setText(value);
         raf.scheduleFullRedraw();
       },
       onSubmit: (query, alt) => {
@@ -840,15 +661,15 @@
         addQueryResultsTab(config, tag);
       },
       onClose: () => {
-        this.queryText = '';
+        globals.omnibox.setText('');
         if (this.omniboxInputEl) {
           this.omniboxInputEl.blur();
         }
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
         raf.scheduleFullRedraw();
       },
       onGoBack: () => {
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
       },
     });
   }
@@ -865,10 +686,10 @@
       onInput: (value, prev) => {
         if (prev === '') {
           if (value === '>') {
-            this.enterCommandMode();
+            globals.omnibox.setMode(OmniboxMode.Command);
             return;
           } else if (value === ':') {
-            this.enterQueryMode();
+            globals.omnibox.setMode(OmniboxMode.Query);
             return;
           }
         }
@@ -987,32 +808,20 @@
   }
 
   private maybeFocusOmnibar() {
-    if (this.focusOmniboxNextRender) {
+    if (globals.omnibox.focusOmniboxNextRender) {
       const omniboxEl = this.omniboxInputEl;
       if (omniboxEl) {
         omniboxEl.focus();
-        if (this.pendingCursorPlacement === -1) {
+        if (globals.omnibox.pendingCursorPlacement === undefined) {
           omniboxEl.select();
         } else {
           omniboxEl.setSelectionRange(
-            this.pendingCursorPlacement,
-            this.pendingCursorPlacement,
+            globals.omnibox.pendingCursorPlacement,
+            globals.omnibox.pendingCursorPlacement,
           );
-          this.pendingCursorPlacement = -1;
         }
       }
-      this.focusOmniboxNextRender = false;
+      globals.omnibox.clearOmniboxFocusFlag();
     }
   }
 }
-
-// Returns the time span of the current selection, or the visible window if
-// there is no current selection.
-function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
-  const range = globals.findTimeRangeOfSelection();
-  if (exists(range)) {
-    return new TimeSpan(range.start, range.end);
-  } else {
-    return globals.stateVisibleTime();
-  }
-}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 9a37a6a..d010f95 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -18,26 +18,18 @@
 import {Disposable, NullDisposable} from '../base/disposable';
 import {assertTrue, assertUnreachable} from '../base/logging';
 import {Time, time} from '../base/time';
+import {uuidv4Sql} from '../base/uuid';
 import {drawTrackHoverTooltip} from '../common/canvas_utils';
 import {raf} from '../core/raf_scheduler';
+import {CacheKey} from '../core/timeline_cache';
 import {Engine, LONG, NUM, Track} from '../public';
 import {Button} from '../widgets/button';
-import {MenuItem, MenuDivider, PopupMenu2} from '../widgets/menu';
+import {MenuDivider, MenuItem, PopupMenu2} from '../widgets/menu';
 
 import {checkerboardExcept} from './checkerboard';
 import {globals} from './globals';
 import {PanelSize} from './panel';
 import {NewTrackArgs} from './track';
-import {CacheKey} from '../core/timeline_cache';
-import {featureFlags} from '../core/feature_flags';
-import {uuidv4Sql} from '../base/uuid';
-
-export const COUNTER_DEBUG_MENU_ITEMS = featureFlags.register({
-  id: 'counterDebugMenuItems',
-  name: 'Counter debug menu items',
-  description: 'Extra counter menu items for debugging purposes.',
-  defaultValue: false,
-});
 
 function roundAway(n: number): number {
   const exp = Math.ceil(Math.log10(Math.max(Math.abs(n), 1)));
@@ -355,65 +347,63 @@
           },
         }),
 
-      COUNTER_DEBUG_MENU_ITEMS.get() && [
-        m(MenuDivider),
-        m(
-          MenuItem,
-          {
-            label: `Mode (currently: ${options.yMode})`,
-          },
+      m(MenuDivider),
+      m(
+        MenuItem,
+        {
+          label: `Mode (currently: ${options.yMode})`,
+        },
 
-          m(MenuItem, {
-            label: 'Value',
-            icon:
-              options.yMode === 'value'
-                ? 'radio_button_checked'
-                : 'radio_button_unchecked',
-            onclick: () => {
-              options.yMode = 'value';
-              this.invalidate();
-            },
-          }),
-
-          m(MenuItem, {
-            label: 'Delta',
-            icon:
-              options.yMode === 'delta'
-                ? 'radio_button_checked'
-                : 'radio_button_unchecked',
-            onclick: () => {
-              options.yMode = 'delta';
-              this.invalidate();
-            },
-          }),
-
-          m(MenuItem, {
-            label: 'Rate',
-            icon:
-              options.yMode === 'rate'
-                ? 'radio_button_checked'
-                : 'radio_button_unchecked',
-            onclick: () => {
-              options.yMode = 'rate';
-              this.invalidate();
-            },
-          }),
-        ),
         m(MenuItem, {
-          label: 'Round y-axis scale',
+          label: 'Value',
           icon:
-            options.yRangeRounding === 'human_readable'
-              ? 'check_box'
-              : 'check_box_outline_blank',
+            options.yMode === 'value'
+              ? 'radio_button_checked'
+              : 'radio_button_unchecked',
           onclick: () => {
-            options.yRangeRounding =
-              options.yRangeRounding === 'human_readable'
-                ? 'strict'
-                : 'human_readable';
+            options.yMode = 'value';
             this.invalidate();
           },
         }),
-      ],
+
+        m(MenuItem, {
+          label: 'Delta',
+          icon:
+            options.yMode === 'delta'
+              ? 'radio_button_checked'
+              : 'radio_button_unchecked',
+          onclick: () => {
+            options.yMode = 'delta';
+            this.invalidate();
+          },
+        }),
+
+        m(MenuItem, {
+          label: 'Rate',
+          icon:
+            options.yMode === 'rate'
+              ? 'radio_button_checked'
+              : 'radio_button_unchecked',
+          onclick: () => {
+            options.yMode = 'rate';
+            this.invalidate();
+          },
+        }),
+      ),
+      m(MenuItem, {
+        label: 'Round y-axis scale',
+        icon:
+          options.yRangeRounding === 'human_readable'
+            ? 'check_box'
+            : 'check_box_outline_blank',
+        onclick: () => {
+          options.yRangeRounding =
+            options.yRangeRounding === 'human_readable'
+              ? 'strict'
+              : 'human_readable';
+          this.invalidate();
+        },
+      }),
     ];
   }
 
@@ -698,9 +688,7 @@
       this.initState.dispose();
       this.initState = undefined;
     }
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table if exists ${this.getTableName()}`);
-    }
+    await this.engine.tryQuery(`drop table if exists ${this.getTableName()}`);
   }
 
   // Compute the range of values to display and range label.
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 720ace2..253ed41 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -666,9 +666,7 @@
       this.initState.dispose();
       this.initState = undefined;
     }
-    if (this.engine.isAlive) {
-      await this.engine.execute(`drop table ${this.getTableName()}`);
-    }
+    await this.engine.tryQuery(`drop table ${this.getTableName()}`);
   }
 
   // This method figures out if the visible window is outside the bounds of
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 91cf54f..dd13a8d 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -17,7 +17,6 @@
 import {Icons} from '../base/semantic_icons';
 import {duration, Time, TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
-import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
@@ -104,17 +103,18 @@
     run: (slice: SliceDetails) => {
       const engine = getEngine();
       if (engine === undefined) return;
-      runQuery(
-        `
+      engine
+        .query(
+          `
         INCLUDE PERFETTO MODULE android.binder;
         INCLUDE PERFETTO MODULE android.monitor_contention;
       `,
-        engine,
-      ).then(() =>
-        addDebugSliceTrack(
-          engine,
-          {
-            sqlSource: `
+        )
+        .then(() =>
+          addDebugSliceTrack(
+            engine,
+            {
+              sqlSource: `
                                 WITH merged AS (
                                   SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
                                   FROM android_binder_txns tx
@@ -151,14 +151,14 @@
                                         AND short_blocked_method IS NOT NULL
                                   ORDER BY depth
                                 ) SELECT ts, dur, name FROM merged`,
-          },
-          `Binder names (${getProcessNameFromSlice(
-            slice,
-          )}:${getThreadNameFromSlice(slice)})`,
-          {ts: 'ts', dur: 'dur', name: 'name'},
-          [],
-        ),
-      );
+            },
+            `Binder names (${getProcessNameFromSlice(
+              slice,
+            )}:${getThreadNameFromSlice(slice)})`,
+            {ts: 'ts', dur: 'dur', name: 'name'},
+            [],
+          ),
+        );
     },
   },
 ];
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4ffa6cf..69097ec 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -71,8 +71,8 @@
       for (const trackId of getTrackIds(track)) {
         this.trackIdToTrackPanel.set(trackId, {panel, yStart});
       }
-    } else if (exists(panel.trackGroupId)) {
-      this.groupIdToTrackGroupPanel.set(panel.trackGroupId, {
+    } else if (exists(panel.groupKey)) {
+      this.groupIdToTrackGroupPanel.set(panel.groupKey, {
         panel,
         yStart,
         height,
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 04cdc17..46d6cbe 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -56,6 +56,7 @@
 import {PxSpan, TimeScale} from './time_scale';
 import {SelectionManager, LegacySelection} from '../core/selection_manager';
 import {exists} from '../base/utils';
+import {OmniboxManager} from './omnibox_manager';
 
 const INSTANT_FOCUS_DURATION = 1n;
 const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -221,7 +222,7 @@
   pendingScrollId: number | undefined;
 }
 
-export interface TraceTime {
+export interface TraceContext {
   readonly start: time;
   readonly end: time;
 
@@ -237,14 +238,22 @@
   // Trace TZ is like UTC but keeps into account also the timezone_off_mins
   // recorded into the trace, to show timestamps in the device local time.
   readonly traceTzOffset: time;
+
+  // The list of CPUs in the trace
+  readonly cpus: number[];
+
+  // The number of gpus in the trace
+  readonly gpuCount: number;
 }
 
-export const defaultTraceTime: TraceTime = {
+export const defaultTraceContext: TraceContext = {
   start: Time.ZERO,
   end: Time.fromSeconds(10),
   realtimeOffset: Time.ZERO,
   utcOffset: Time.ZERO,
   traceTzOffset: Time.ZERO,
+  cpus: [],
+  gpuCount: 0,
 };
 
 /**
@@ -291,12 +300,15 @@
   private _selectionManager = new SelectionManager(this._store);
   private _hasFtrace: boolean = false;
 
+  omnibox = new OmniboxManager();
+
   scrollToTrackKey?: string | number;
   httpRpcState: HttpRpcState = {connected: false};
   newVersionAvailable = false;
   showPanningHint = false;
+  permalinkHash?: string;
 
-  traceTime = defaultTraceTime;
+  traceContext = defaultTraceContext;
 
   // TODO(hjd): Remove once we no longer need to update UUID on redraw.
   private _publishRedraw?: () => void = undefined;
@@ -716,19 +728,19 @@
 
   // Get a timescale that covers the entire trace
   getTraceTimeScale(pxSpan: PxSpan): TimeScale {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     const traceTime = HighPrecisionTimeSpan.fromTime(start, end);
     return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
   }
 
   // Get the trace time bounds
   stateTraceTime(): Span<HighPrecisionTime> {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     return HighPrecisionTimeSpan.fromTime(start, end);
   }
 
   stateTraceTimeTP(): Span<time, duration> {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     return new TimeSpan(start, end);
   }
 
@@ -762,14 +774,14 @@
     switch (fmt) {
       case TimestampFormat.Timecode:
       case TimestampFormat.Seconds:
-        return this.traceTime.start;
+        return this.traceContext.start;
       case TimestampFormat.Raw:
       case TimestampFormat.RawLocale:
         return Time.ZERO;
       case TimestampFormat.UTC:
-        return this.traceTime.utcOffset;
+        return this.traceContext.utcOffset;
       case TimestampFormat.TraceTz:
-        return this.traceTime.traceTzOffset;
+        return this.traceContext.traceTzOffset;
       default:
         const x: never = fmt;
         throw new Error(`Unsupported format ${x}`);
@@ -859,4 +871,15 @@
   }
 }
 
+// Returns the time span of the current selection, or the visible window if
+// there is no current selection.
+export function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
+  const range = globals.findTimeRangeOfSelection();
+  if (exists(range)) {
+    return new TimeSpan(range.start, range.end);
+  } else {
+    return globals.stateVisibleTime();
+  }
+}
+
 export const globals = new Globals();
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index c583e2d..0dc71bd 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -57,12 +57,9 @@
 export class NotesPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
 
   hoveredX: null | number = null;
 
-  constructor(readonly key: string) {}
-
   render(): m.Children {
     const allCollapsed = Object.values(globals.state.trackGroups).every(
       (group) => group.collapsed,
diff --git a/ui/src/frontend/omnibox_manager.ts b/ui/src/frontend/omnibox_manager.ts
new file mode 100644
index 0000000..bb09810
--- /dev/null
+++ b/ui/src/frontend/omnibox_manager.ts
@@ -0,0 +1,155 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {raf} from '../core/raf_scheduler';
+
+export enum OmniboxMode {
+  Search,
+  Query,
+  Command,
+  Prompt,
+}
+
+export interface PromptOption {
+  key: string;
+  displayName: string;
+}
+
+interface Prompt {
+  text: string;
+  options?: PromptOption[];
+  resolve(result: string): void;
+  reject(): void;
+}
+
+const defaultMode = OmniboxMode.Search;
+
+export class OmniboxManager {
+  private _omniboxMode = defaultMode;
+  private _focusOmniboxNextRender = false;
+  private _pendingCursorPlacement?: number;
+  private _pendingPrompt?: Prompt;
+  private _text = '';
+  private _omniboxSelectionIndex = 0;
+
+  get omniboxMode(): OmniboxMode {
+    return this._omniboxMode;
+  }
+
+  get pendingPrompt(): Prompt | undefined {
+    return this._pendingPrompt;
+  }
+
+  get text(): string {
+    return this._text;
+  }
+
+  get omniboxSelectionIndex(): number {
+    return this._omniboxSelectionIndex;
+  }
+
+  get focusOmniboxNextRender(): boolean {
+    return this._focusOmniboxNextRender;
+  }
+
+  get pendingCursorPlacement(): number | undefined {
+    return this._pendingCursorPlacement;
+  }
+
+  setText(value: string): void {
+    this._text = value;
+  }
+
+  setOmniboxSelectionIndex(index: number): void {
+    this._omniboxSelectionIndex = index;
+  }
+
+  focusOmnibox(cursorPlacement?: number): void {
+    this._focusOmniboxNextRender = true;
+    this._pendingCursorPlacement = cursorPlacement;
+    raf.scheduleFullRedraw();
+  }
+
+  clearOmniboxFocusFlag(): void {
+    this._focusOmniboxNextRender = false;
+    this._pendingCursorPlacement = undefined;
+  }
+
+  setMode(mode: OmniboxMode): void {
+    this._omniboxMode = mode;
+    this.resetOmniboxText();
+    this.rejectPendingPrompt();
+    raf.scheduleFullRedraw();
+  }
+
+  // Start a prompt. If options are supplied, the user must pick one from the
+  // list, otherwise the input is free-form text.
+  prompt(text: string, options?: PromptOption[]): Promise<string> {
+    this._omniboxMode = OmniboxMode.Prompt;
+    this.resetOmniboxText();
+    this.rejectPendingPrompt();
+
+    const promise = new Promise<string>((resolve, reject) => {
+      this._pendingPrompt = {
+        text,
+        options,
+        resolve,
+        reject,
+      };
+    });
+
+    this._focusOmniboxNextRender = true;
+    raf.scheduleFullRedraw();
+
+    return promise;
+  }
+
+  // Resolve the pending prompt with a value to return to the prompter.
+  resolvePrompt(value: string): void {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.resolve(value);
+      this._pendingPrompt = undefined;
+    }
+    this.setMode(OmniboxMode.Search);
+  }
+
+  // Reject the prompt outright. Doing this will force the owner of the prompt
+  // promise to catch, so only do this when things go seriously wrong.
+  // Use |resolvePrompt(null)| to indicate cancellation.
+  rejectPrompt(): void {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.reject();
+      this._pendingPrompt = undefined;
+    }
+    this.setMode(OmniboxMode.Search);
+  }
+
+  reset(): void {
+    this.setMode(defaultMode);
+    this.resetOmniboxText();
+    raf.scheduleFullRedraw();
+  }
+
+  private rejectPendingPrompt() {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.reject();
+      this._pendingPrompt = undefined;
+    }
+  }
+
+  private resetOmniboxText() {
+    this._text = '';
+    this._omniboxSelectionIndex = 0;
+  }
+}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index a47434a..4ff95bb 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -42,7 +42,6 @@
   private static HANDLE_SIZE_PX = 5;
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
 
   private width = 0;
   private gesture?: DragGestureHandler;
@@ -51,8 +50,6 @@
   private dragStrategy?: DragStrategy;
   private readonly boundOnMouseMove = this.onMouseMove.bind(this);
 
-  constructor(readonly key: string) {}
-
   // Must explicitly type now; arguments types are no longer auto-inferred.
   // https://github.com/Microsoft/TypeScript/issues/1373
   onupdate({dom}: m.CVnodeDOM) {
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9214430..5611a19 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -47,22 +47,20 @@
 const CANVAS_OVERDRAW_PX = 100;
 
 export interface Panel {
-  kind: 'panel';
+  readonly kind: 'panel';
   render(): m.Children;
-  selectable: boolean;
-  key: string;
-  trackKey?: string;
-  trackGroupId?: string;
+  readonly selectable: boolean;
+  readonly trackKey?: string; // Defined if this panel represents are track
+  readonly groupKey?: string; // Defined if this panel represents a group - i.e. a group summary track
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void;
   getSliceRect?(tStart: time, tDur: time, depth: number): SliceRect | undefined;
 }
 
 export interface PanelGroup {
-  kind: 'group';
-  collapsed: boolean;
-  header: Panel;
-  childPanels: Panel[];
-  trackGroupId: string;
+  readonly kind: 'group';
+  readonly collapsed: boolean;
+  readonly header: Panel;
+  readonly childPanels: Panel[];
 }
 
 export type PanelOrGroup = Panel | PanelGroup;
@@ -74,7 +72,7 @@
 }
 
 interface PanelInfo {
-  id: string; // Can be == '' for singleton panels.
+  trackOrGroupKey: string; // Can be == '' for singleton panels.
   panel: Panel;
   height: number;
   width: number;
@@ -91,7 +89,7 @@
   private panelContainerHeight = 0;
 
   // Updated every render cycle in the view() hook
-  private panelByKey = new Map<string, Panel>();
+  private panelById = new Map<string, Panel>();
 
   // Updated every render cycle in the oncreate/onupdate hook
   private panelInfos: PanelInfo[] = [];
@@ -179,11 +177,11 @@
         tracks.push(panel.trackKey);
         continue;
       }
-      if (panel.trackGroupId !== undefined) {
-        const trackGroup = globals.state.trackGroups[panel.trackGroupId];
+      if (panel.groupKey !== undefined) {
+        const trackGroup = globals.state.trackGroups[panel.groupKey];
         // Only select a track group and all child tracks if it is closed.
         if (trackGroup.collapsed) {
-          tracks.push(panel.trackGroupId);
+          tracks.push(panel.groupKey);
           for (const track of trackGroup.tracks) {
             tracks.push(track);
           }
@@ -258,37 +256,40 @@
     this.trash.dispose();
   }
 
-  renderPanel(node: Panel, key: string, extraClass = ''): m.Vnode {
-    assertFalse(this.panelByKey.has(key));
-    this.panelByKey.set(key, node);
-    return m(`.pf-panel${extraClass}`, {key, 'data-key': key}, node.render());
+  renderPanel(node: Panel, panelId: string, extraClass = ''): m.Vnode {
+    assertFalse(this.panelById.has(panelId));
+    this.panelById.set(panelId, node);
+    return m(
+      `.pf-panel${extraClass}`,
+      {'data-panel-id': panelId},
+      node.render(),
+    );
   }
 
   // Render a tree of panels into one vnode. Argument `path` is used to build
   // `key` attribute for intermediate tree vnodes: otherwise Mithril internals
   // will complain about keyed and non-keyed vnodes mixed together.
-  renderTree(node: PanelOrGroup, path: string): m.Vnode {
+  renderTree(node: PanelOrGroup, panelId: string): m.Vnode {
     if (node.kind === 'group') {
       return m(
         'div.pf-panel-group',
-        {key: path},
         this.renderPanel(
           node.header,
-          `${path}-header`,
+          `${panelId}-header`,
           node.collapsed ? '' : '.pf-sticky',
         ),
         ...node.childPanels.map((child, index) =>
-          this.renderTree(child, `${path}-${index}`),
+          this.renderTree(child, `${panelId}-${index}`),
         ),
       );
     }
-    return this.renderPanel(node, assertExists(node.key));
+    return this.renderPanel(node, panelId);
   }
 
   view({attrs}: m.CVnode<PanelContainerAttrs>) {
-    this.panelByKey.clear();
+    this.panelById.clear();
     const children = attrs.panels.map((panel, index) =>
-      this.renderTree(panel, `track-tree-${index}`),
+      this.renderTree(panel, `${index}`),
     );
 
     return m(
@@ -316,14 +317,15 @@
     this.panelContainerHeight = domRect.height;
 
     dom.querySelectorAll('.pf-panel').forEach((panelElement) => {
-      const key = assertExists(panelElement.getAttribute('data-key'));
-      const panel = assertExists(this.panelByKey.get(key));
+      const panelHTMLElement = toHTMLElement(panelElement);
+      const panelId = assertExists(panelHTMLElement.dataset.panelId);
+      const panel = assertExists(this.panelById.get(panelId));
 
       // NOTE: the id can be undefined for singletons like overview timeline.
-      const id = panel.trackKey || panel.trackGroupId || '';
+      const key = panel.trackKey || panel.groupKey || '';
       const rect = panelElement.getBoundingClientRect();
       this.panelInfos.push({
-        id,
+        trackOrGroupKey: key,
         height: rect.height,
         width: rect.width,
         clientX: rect.x,
@@ -440,7 +442,7 @@
     let selectedTracksMaxY = this.panelContainerTop;
     let trackFromCurrentContainerSelected = false;
     for (let i = 0; i < this.panelInfos.length; i++) {
-      if (area.tracks.includes(this.panelInfos[i].id)) {
+      if (area.tracks.includes(this.panelInfos[i].trackOrGroupKey)) {
         trackFromCurrentContainerSelected = true;
         selectedTracksMinY = Math.min(
           selectedTracksMinY,
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
new file mode 100644
index 0000000..888fe55
--- /dev/null
+++ b/ui/src/frontend/permalink.ts
@@ -0,0 +1,250 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {produce} from 'immer';
+import {assertExists} from '../base/logging';
+import {runValidator} from '../base/validators';
+import {Actions} from '../common/actions';
+import {ConversionJobStatus} from '../common/conversion_jobs';
+import {
+  createEmptyNonSerializableState,
+  createEmptyState,
+} from '../common/empty_state';
+import {EngineConfig, ObjectById, STATE_VERSION, State} from '../common/state';
+import {
+  BUCKET_NAME,
+  TraceGcsUploader,
+  buggyToSha256,
+  deserializeStateObject,
+  saveState,
+  toSha256,
+} from '../common/upload_utils';
+import {
+  RecordConfig,
+  recordConfigValidator,
+} from '../controller/record_config_types';
+import {globals} from './globals';
+import {
+  publishConversionJobStatusUpdate,
+  publishPermalinkHash,
+} from './publish';
+import {Router} from './router';
+import {showModal} from '../widgets/modal';
+
+export interface PermalinkOptions {
+  isRecordingConfig?: boolean;
+}
+
+export async function createPermalink(
+  options: PermalinkOptions = {},
+): Promise<void> {
+  const {isRecordingConfig = false} = options;
+  const jobName = 'create_permalink';
+  publishConversionJobStatusUpdate({
+    jobName,
+    jobStatus: ConversionJobStatus.InProgress,
+  });
+
+  try {
+    const hash = await createPermalinkInternal(isRecordingConfig);
+    publishPermalinkHash(hash);
+  } finally {
+    publishConversionJobStatusUpdate({
+      jobName,
+      jobStatus: ConversionJobStatus.NotRunning,
+    });
+  }
+}
+
+async function createPermalinkInternal(
+  isRecordingConfig: boolean,
+): Promise<string> {
+  let uploadState: State | RecordConfig = globals.state;
+
+  if (isRecordingConfig) {
+    uploadState = globals.state.recordConfig;
+  } else {
+    const engine = assertExists(globals.getCurrentEngine());
+    let dataToUpload: File | ArrayBuffer | undefined = undefined;
+    let traceName = `trace ${engine.id}`;
+    if (engine.source.type === 'FILE') {
+      dataToUpload = engine.source.file;
+      traceName = dataToUpload.name;
+    } else if (engine.source.type === 'ARRAY_BUFFER') {
+      dataToUpload = engine.source.buffer;
+    } else if (engine.source.type !== 'URL') {
+      throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
+    }
+
+    if (dataToUpload !== undefined) {
+      updateStatus(`Uploading ${traceName}`);
+      const uploader = new TraceGcsUploader(dataToUpload, () => {
+        switch (uploader.state) {
+          case 'UPLOADING':
+            const statusTxt = `Uploading ${uploader.getEtaString()}`;
+            updateStatus(statusTxt);
+            break;
+          case 'UPLOADED':
+            // Convert state to use URLs and remove permalink.
+            const url = uploader.uploadedUrl;
+            uploadState = produce(globals.state, (draft) => {
+              assertExists(draft.engine).source = {type: 'URL', url};
+            });
+            break;
+          case 'ERROR':
+            updateStatus(`Upload failed ${uploader.error}`);
+            break;
+        } // switch (state)
+      }); // onProgress
+      await uploader.waitForCompletion();
+    }
+  }
+
+  // Upload state.
+  updateStatus(`Creating permalink...`);
+  const hash = await saveState(uploadState);
+  updateStatus(`Permalink ready`);
+  return hash;
+}
+
+function updateStatus(msg: string): void {
+  // TODO(hjd): Unify loading updates.
+  globals.dispatch(
+    Actions.updateStatus({
+      msg,
+      timestamp: Date.now() / 1000,
+    }),
+  );
+}
+
+export async function loadPermalink(hash: string): Promise<void> {
+  // Otherwise, this is a request to load the permalink.
+  const stateOrConfig = await loadState(hash);
+
+  if (isRecordConfig(stateOrConfig)) {
+    // This permalink state only contains a RecordConfig. Show the
+    // recording page with the config, but keep other state as-is.
+    const validConfig = runValidator(
+      recordConfigValidator,
+      stateOrConfig as unknown,
+    ).result;
+    globals.dispatch(Actions.setRecordConfig({config: validConfig}));
+    Router.navigate('#!/record');
+    return;
+  }
+  globals.dispatch(Actions.setState({newState: stateOrConfig}));
+}
+
+async function loadState(id: string): Promise<State | RecordConfig> {
+  const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
+  const response = await fetch(url);
+  if (!response.ok) {
+    throw new Error(
+      `Could not fetch permalink.\n` +
+        `Are you sure the id (${id}) is correct?\n` +
+        `URL: ${url}`,
+    );
+  }
+  const text = await response.text();
+  const stateHash = await toSha256(text);
+  const state = deserializeStateObject<State>(text);
+  if (stateHash !== id) {
+    // Old permalinks incorrectly dropped some digits from the
+    // hexdigest of the SHA256. We don't want to invalidate those
+    // links so we also compute the old string and try that here
+    // also.
+    const buggyStateHash = await buggyToSha256(text);
+    if (buggyStateHash !== id) {
+      throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
+    }
+  }
+  if (!isRecordConfig(state)) {
+    return upgradeState(state);
+  }
+  return state;
+}
+
+function isRecordConfig(
+  stateOrConfig: State | RecordConfig,
+): stateOrConfig is RecordConfig {
+  const mode = (stateOrConfig as {mode?: string}).mode;
+  return (
+    mode !== undefined &&
+    ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
+  );
+}
+
+function upgradeState(state: State): State {
+  if (state.engine !== undefined && state.engine.source.type !== 'URL') {
+    // All permalink traces should be modified to have a source.type=URL
+    // pointing to the uploaded trace. Due to a bug in some older version
+    // of the UI (b/327049372), an upload failure can end up with a state that
+    // has type=FILE but a null file object. If this happens, invalidate the
+    // trace and show a message.
+    showModal({
+      title: 'Cannot load trace permalink',
+      content: m(
+        'div',
+        'The permalink stored on the server is corrupted ' +
+          'and cannot be loaded.',
+      ),
+    });
+    return createEmptyState();
+  }
+
+  if (state.version !== STATE_VERSION) {
+    const newState = createEmptyState();
+    // Old permalinks from state versions prior to version 24
+    // have multiple engines of which only one is identified as the
+    // current engine via currentEngineId. Handle this case:
+    if (isMultiEngineState(state)) {
+      const engineId = state.currentEngineId;
+      if (engineId !== undefined) {
+        newState.engine = state.engines[engineId];
+      }
+    } else {
+      newState.engine = state.engine;
+    }
+
+    if (newState.engine !== undefined) {
+      newState.engine.ready = false;
+    }
+    const message =
+      `Unable to parse old state version. Discarding state ` +
+      `and loading trace.`;
+    console.warn(message);
+    updateStatus(message);
+    return newState;
+  } else {
+    // Loaded state is presumed to be compatible with the State type
+    // definition in the app. However, a non-serializable part has to be
+    // recreated.
+    state.nonSerializableState = createEmptyNonSerializableState();
+  }
+  return state;
+}
+
+interface MultiEngineState {
+  currentEngineId?: string;
+  engines: ObjectById<EngineConfig>;
+}
+
+function isMultiEngineState(
+  state: State | MultiEngineState,
+): state is MultiEngineState {
+  if ((state as MultiEngineState).engines !== undefined) {
+    return true;
+  }
+  return false;
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index bfb34ca..2fbc6df 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -31,7 +31,7 @@
   SliceDetails,
   ThreadDesc,
   ThreadStateDetails,
-  TraceTime,
+  TraceContext,
 } from './globals';
 import {findCurrentSelection} from './keyboard_event_handler';
 
@@ -96,8 +96,8 @@
   globals.publishRedraw();
 }
 
-export function publishTraceDetails(details: TraceTime): void {
-  globals.traceTime = details;
+export function publishTraceContext(details: TraceContext): void {
+  globals.traceContext = details;
   globals.publishRedraw();
 }
 
@@ -208,3 +208,8 @@
   globals.showPanningHint = true;
   globals.publishRedraw();
 }
+
+export function publishPermalinkHash(hash: string | undefined): void {
+  globals.permalinkHash = hash;
+  globals.publishRedraw();
+}
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index ad59ea8..49cc7b3 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -57,6 +57,7 @@
 import {RecordingSectionAttrs} from './recording/recording_sections';
 import {RecordingSettings} from './recording/recording_settings';
 import {EtwSettings} from './recording/etw_settings';
+import {createPermalink} from './permalink';
 
 export const PERSIST_CONFIG_FLAG = featureFlags.register({
   id: 'persistConfigsUI',
@@ -176,9 +177,7 @@
           'button.permalinkconfig',
           {
             onclick: () => {
-              globals.dispatch(
-                Actions.createPermalink({isRecordingConfig: true}),
-              );
+              createPermalink({isRecordingConfig: true});
             },
           },
           'Share recording settings',
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index db87cfa..dc1d015 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -16,7 +16,6 @@
 import {Attributes} from 'mithril';
 
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
 import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
 import {
   ChromeTargetInfo,
@@ -57,6 +56,7 @@
 import {RecordingSettings} from './recording/recording_settings';
 import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
 import {showAddNewTargetModal} from './recording/reset_target_modal';
+import {createPermalink} from './permalink';
 
 const START_RECORDING_MESSAGE = 'Start Recording';
 
@@ -186,9 +186,7 @@
           'button.permalinkconfig',
           {
             onclick: () => {
-              globals.dispatch(
-                Actions.createPermalink({isRecordingConfig: true}),
-              );
+              createPermalink({isRecordingConfig: true});
             },
           },
           'Share recording settings',
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index f643001..cd7e94d 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -18,7 +18,7 @@
   HighPrecisionTime,
   HighPrecisionTimeSpan,
 } from '../common/high_precision_time';
-import {getContainingTrackId} from '../common/state';
+import {getContainingGroupKey} from '../common/state';
 
 import {globals} from './globals';
 
@@ -127,12 +127,12 @@
   }
 
   let trackGroup = null;
-  const trackGroupId = getContainingTrackId(globals.state, trackKeyString);
-  if (trackGroupId) {
-    trackGroup = document.querySelector('#track_' + trackGroupId);
+  const groupKey = getContainingGroupKey(globals.state, trackKeyString);
+  if (groupKey) {
+    trackGroup = document.querySelector('#track_' + groupKey);
   }
 
-  if (!trackGroupId || !trackGroup) {
+  if (!groupKey || !trackGroup) {
     console.error(`Can't scroll, track (${trackKeyString}) not found.`);
     return;
   }
@@ -142,7 +142,7 @@
   if (openGroup) {
     // After the track exists in the dom, it will be scrolled to.
     globals.scrollToTrackKey = trackKey;
-    globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+    globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
     return;
   } else {
     trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 5b21ded..7bf0f0e 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -74,8 +74,6 @@
   }
 
   private async dropTrackTable(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table if exists ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index c292fe6..3eebb83 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -17,7 +17,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../core_plugins/custom_sql_table_slices';
+} from './tracks/custom_sql_table_slice_track';
 import {NamedSliceTrackTypes} from './named_slice_track';
 import {ARG_PREFIX, SliceColumns, SqlDataSource} from './debug_tracks';
 import {uuidv4Sql} from '../base/uuid';
@@ -108,8 +108,6 @@
   }
 
   private async destroyTrackTable() {
-    if (this.engine.isAlive) {
-      await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index fac6b8a..f028b15 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 
 import {Time, time} from '../base/time';
-import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Anchor} from '../widgets/anchor';
 import {Button} from '../widgets/button';
@@ -322,14 +321,13 @@
         label: 'Critical path lite',
         intent: Intent.Primary,
         onclick: () =>
-          runQuery(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-            this.engine,
-          ).then(() =>
-            addDebugSliceTrack(
-              this.engine,
-              {
-                sqlSource: `
+          this.engine
+            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+            .then(() =>
+              addDebugSliceTrack(
+                this.engine,
+                {
+                  sqlSource: `
                     SELECT
                       cr.id,
                       cr.utid,
@@ -347,26 +345,27 @@
                     JOIN thread USING(utid)
                     JOIN process USING(upid)
                   `,
-                columns: sliceLiteColumnNames,
-              },
-              `${this.state?.thread?.name}`,
-              sliceLiteColumns,
-              sliceLiteColumnNames,
+                  columns: sliceLiteColumnNames,
+                },
+                `${this.state?.thread?.name}`,
+                sliceLiteColumns,
+                sliceLiteColumnNames,
+              ),
             ),
-          ),
       }),
       m(Button, {
         label: 'Critical path',
         intent: Intent.Primary,
         onclick: () =>
-          runQuery(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            this.engine,
-          ).then(() =>
-            addDebugSliceTrack(
-              this.engine,
-              {
-                sqlSource: `
+          this.engine
+            .query(
+              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+            )
+            .then(() =>
+              addDebugSliceTrack(
+                this.engine,
+                {
+                  sqlSource: `
                     SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
                       FROM
                         _thread_executing_span_critical_path_stack(
@@ -375,13 +374,13 @@
                           trace_bounds.end_ts - trace_bounds.start_ts) cr,
                         trace_bounds WHERE name IS NOT NULL
                   `,
-                columns: sliceColumnNames,
-              },
-              `${this.state?.thread?.name}`,
-              sliceColumns,
-              sliceColumnNames,
+                  columns: sliceColumnNames,
+                },
+                `${this.state?.thread?.name}`,
+                sliceColumns,
+                sliceColumnNames,
+              ),
             ),
-          ),
       }),
     ];
   }
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 90f3e4c..632052d 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -31,9 +31,6 @@
 export class TickmarkPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.tickbar');
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index af1f3df..16c22ed 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -32,9 +32,6 @@
 export class TimeAxisPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.time-axis-panel');
@@ -57,16 +54,16 @@
         break;
       case TimestampFormat.UTC:
         const offsetDate = Time.toDate(
-          globals.traceTime.utcOffset,
-          globals.traceTime.realtimeOffset,
+          globals.traceContext.utcOffset,
+          globals.traceContext.realtimeOffset,
         );
         const dateStr = toISODateOnly(offsetDate);
         ctx.fillText(`UTC ${dateStr}`, 6, 10);
         break;
       case TimestampFormat.TraceTz:
         const offsetTzDate = Time.toDate(
-          globals.traceTime.traceTzOffset,
-          globals.traceTime.realtimeOffset,
+          globals.traceContext.traceTzOffset,
+          globals.traceContext.realtimeOffset,
         );
         const dateTzStr = toISODateOnly(offsetTzDate);
         ctx.fillText(dateTzStr, 6, 10);
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index d77b095..66e401c 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -140,9 +140,6 @@
 export class TimeSelectionPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.time-selection-panel');
diff --git a/ui/src/frontend/trace_attrs.ts b/ui/src/frontend/trace_attrs.ts
index 61f991e..ff80c85 100644
--- a/ui/src/frontend/trace_attrs.ts
+++ b/ui/src/frontend/trace_attrs.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
 import {TraceArrayBufferSource} from '../common/state';
+import {createPermalink} from './permalink';
 import {showModal} from '../widgets/modal';
 
 import {onClickCopy} from './clipboard';
@@ -74,7 +74,7 @@
   );
   if (result) {
     globals.logging.logEvent('Trace Actions', 'Create permalink');
-    globals.dispatch(Actions.createPermalink({isRecordingConfig: false}));
+    createPermalink();
   }
 }
 
diff --git a/ui/src/frontend/trace_info_page.ts b/ui/src/frontend/trace_info_page.ts
index c8d7547..87f270a 100644
--- a/ui/src/frontend/trace_info_page.ts
+++ b/ui/src/frontend/trace_info_page.ts
@@ -14,12 +14,51 @@
 
 import m from 'mithril';
 
-import {QueryResponse, runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 
 import {globals} from './globals';
 import {createPage} from './pages';
+import {QueryResult, UNKNOWN} from '../trace_processor/query_result';
+
+function getEngine(name: string): Engine | undefined {
+  const currentEngine = globals.getCurrentEngine();
+  if (currentEngine === undefined) return undefined;
+  const engineId = currentEngine.id;
+  return globals.engines.get(engineId)?.getProxy(name);
+}
+
+/**
+ * Extracts and copies fields from a source object based on the keys present in
+ * a spec object, effectively creating a new object that includes only the
+ * fields that are present in the spec object.
+ *
+ * @template S - A type representing the spec object, a subset of T.
+ * @template T - A type representing the source object, a superset of S.
+ *
+ * @param {T} source - The source object containing the full set of properties.
+ * @param {S} spec - The specification object whose keys determine which fields
+ * should be extracted from the source object.
+ *
+ * @returns {S} A new object containing only the fields from the source object
+ * that are also present in the specification object.
+ *
+ * @example
+ * const fullObject = { foo: 123, bar: '123', baz: true };
+ * const spec = { foo: 0, bar: '' };
+ * const result = pickFields(fullObject, spec);
+ * console.log(result); // Output: { foo: 123, bar: '123' }
+ */
+function pickFields<S extends Record<string, unknown>, T extends S>(
+  source: T,
+  spec: S,
+): S {
+  const result: Record<string, unknown> = {};
+  for (const key of Object.keys(spec)) {
+    result[key] = source[key];
+  }
+  return result as S;
+}
 
 interface StatsSectionAttrs {
   title: string;
@@ -29,58 +68,71 @@
   queryId: string;
 }
 
-function getEngine(name: string): Engine | undefined {
-  const currentEngine = globals.getCurrentEngine();
-  if (currentEngine === undefined) return undefined;
-  const engineId = currentEngine.id;
-  return globals.engines.get(engineId)?.getProxy(name);
-}
+const statsSpec = {
+  name: UNKNOWN,
+  value: UNKNOWN,
+  description: UNKNOWN,
+  idx: UNKNOWN,
+  severity: UNKNOWN,
+  source: UNKNOWN,
+};
+
+type StatsSectionRow = typeof statsSpec;
 
 // Generic class that generate a <section> + <table> from the stats table.
 // The caller defines the query constraint, title and styling.
 // Used for errors, data losses and debugging sections.
 class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
-  private queryResponse?: QueryResponse;
+  private data?: StatsSectionRow[];
 
   constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select name, value, cast(ifnull(idx, '') as text) as idx,
-              description, severity, source from stats
-              where ${attrs.sqlConstraints || '1=1'}
-              order by name, idx`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      select
+        name,
+        value,
+        cast(ifnull(idx, '') as text) as idx,
+        description,
+        severity,
+        source from stats
+      where ${attrs.sqlConstraints || '1=1'}
+      order by name, idx
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: StatsSectionRow[] = [];
+      const it = resp.iter(statsSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, statsSpec));
+      }
+      this.data = data;
+
       raf.scheduleFullRedraw();
     });
   }
 
   view({attrs}: m.CVnode<StatsSectionAttrs>) {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
-    if (resp.error) throw new Error(resp.error);
 
-    const tableRows = [];
-    for (const row of resp.rows) {
+    const tableRows = data.map((row) => {
       const help = [];
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (row.description) {
+      if (Boolean(row.description)) {
         help.push(m('i.material-icons.contextual-help', 'help_outline'));
       }
       const idx = row.idx !== '' ? `[${row.idx}]` : '';
-      tableRows.push(
-        m(
-          'tr',
-          m('td.name', {title: row.description}, `${row.name}${idx}`, help),
-          m('td', `${row.value}`),
-          m('td', `${row.severity} (${row.source})`),
-        ),
+      return m(
+        'tr',
+        m('td.name', {title: row.description}, `${row.name}${idx}`, help),
+        m('td', `${row.value}`),
+        m('td', `${row.severity} (${row.source})`),
       );
-    }
+    });
 
     return m(
       `section${attrs.cssClass}`,
@@ -107,49 +159,63 @@
   }
 }
 
+const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
+
+type TraceMetadataRow = typeof traceMetadataRowSpec;
+
 class TraceMetadata implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+  private data?: TraceMetadataRow[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `with 
-          metadata_with_priorities as (select
-            name, ifnull(str_value, cast(int_value as text)) as value,
-            name in (
-               "trace_size_bytes", 
-               "cr-os-arch",
-               "cr-os-name",
-               "cr-os-version",
-               "cr-physical-memory",
-               "cr-product-version",
-               "cr-hardware-class"
-            ) as priority 
-            from metadata
-          )
-          select name, value
-          from metadata_with_priorities 
-          order by priority desc, name`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      with metadata_with_priorities as (
+        select
+          name,
+          ifnull(str_value, cast(int_value as text)) as value,
+          name in (
+            "trace_size_bytes", 
+            "cr-os-arch",
+            "cr-os-name",
+            "cr-os-version",
+            "cr-physical-memory",
+            "cr-product-version",
+            "cr-hardware-class"
+          ) as priority 
+        from metadata
+      )
+      select
+        name,
+        value
+      from metadata_with_priorities 
+      order by
+        priority desc,
+        name
+    `;
+
+    engine.query(query).then((resp: QueryResult) => {
+      const tableRows: TraceMetadataRow[] = [];
+      const it = resp.iter(traceMetadataRowSpec);
+      for (; it.valid(); it.next()) {
+        tableRows.push(pickFields(it, traceMetadataRowSpec));
+      }
+      this.data = tableRows;
       raf.scheduleFullRedraw();
     });
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
 
-    const tableRows = [];
-    for (const row of resp.rows) {
-      tableRows.push(
-        m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`)),
-      );
-    }
+    const tableRows = data.map((row) => {
+      return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
+    });
 
     return m(
       'section',
@@ -163,40 +229,68 @@
   }
 }
 
+const androidGameInterventionRowSpec = {
+  package_name: UNKNOWN,
+  uid: UNKNOWN,
+  current_mode: UNKNOWN,
+  standard_mode_supported: UNKNOWN,
+  standard_mode_downscale: UNKNOWN,
+  standard_mode_use_angle: UNKNOWN,
+  standard_mode_fps: UNKNOWN,
+  perf_mode_supported: UNKNOWN,
+  perf_mode_downscale: UNKNOWN,
+  perf_mode_use_angle: UNKNOWN,
+  perf_mode_fps: UNKNOWN,
+  battery_mode_supported: UNKNOWN,
+  battery_mode_downscale: UNKNOWN,
+  battery_mode_use_angle: UNKNOWN,
+  battery_mode_fps: UNKNOWN,
+};
+
+type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
+
 class AndroidGameInterventionList implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+  private data?: AndroidGameInterventionRow[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select
-                package_name,
-                uid,
-                current_mode,
-                standard_mode_supported,
-                standard_mode_downscale,
-                standard_mode_use_angle,
-                standard_mode_fps,
-                perf_mode_supported,
-                perf_mode_downscale,
-                perf_mode_use_angle,
-                perf_mode_fps,
-                battery_mode_supported,
-                battery_mode_downscale,
-                battery_mode_use_angle,
-                battery_mode_fps
-                from android_game_intervention_list`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      select
+        package_name,
+        uid,
+        current_mode,
+        standard_mode_supported,
+        standard_mode_downscale,
+        standard_mode_use_angle,
+        standard_mode_fps,
+        perf_mode_supported,
+        perf_mode_downscale,
+        perf_mode_use_angle,
+        perf_mode_fps,
+        battery_mode_supported,
+        battery_mode_downscale,
+        battery_mode_use_angle,
+        battery_mode_fps
+      from android_game_intervention_list
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: AndroidGameInterventionRow[] = [];
+      const it = resp.iter(androidGameInterventionRowSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, androidGameInterventionRowSpec));
+      }
+      this.data = data;
       raf.scheduleFullRedraw();
     });
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
 
@@ -204,7 +298,8 @@
     let standardInterventions = '';
     let perfInterventions = '';
     let batteryInterventions = '';
-    for (const row of resp.rows) {
+
+    for (const row of data) {
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
       if (row.standard_mode_supported) {
         standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
@@ -268,46 +363,68 @@
   }
 }
 
-class PackageList implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+const packageDataSpec = {
+  packageName: UNKNOWN,
+  versionCode: UNKNOWN,
+  debuggable: UNKNOWN,
+  profileableFromShell: UNKNOWN,
+};
+
+type PackageData = typeof packageDataSpec;
+
+class PackageListSection implements m.ClassComponent {
+  private packageList?: PackageData[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select package_name, version_code, debuggable,
-                profileable_from_shell from package_list`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
-      raf.scheduleFullRedraw();
-    });
+    this.loadData(engine);
+  }
+
+  private async loadData(engine: Engine): Promise<void> {
+    const query = `
+      select
+        package_name as packageName,
+        version_code as versionCode,
+        debuggable,
+        profileable_from_shell as profileableFromShell
+      from package_list
+    `;
+
+    const packageList: PackageData[] = [];
+    const result = await engine.query(query);
+    const it = result.iter(packageDataSpec);
+    for (; it.valid(); it.next()) {
+      packageList.push(pickFields(it, packageDataSpec));
+    }
+
+    this.packageList = packageList;
+    raf.scheduleFullRedraw();
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
-      return m('');
+    const packageList = this.packageList;
+    if (packageList === undefined || packageList.length === 0) {
+      return undefined;
     }
 
-    const tableRows = [];
-    for (const row of resp.rows) {
-      tableRows.push(
+    const tableRows = packageList.map((it) => {
+      return m(
+        'tr',
+        m('td.name', `${it.packageName}`),
+        m('td', `${it.versionCode}`),
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
         m(
-          'tr',
-          m('td.name', `${row.package_name}`),
-          m('td', `${row.version_code}`),
-          /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-          m(
-            'td',
-            `${row.debuggable ? 'debuggable' : ''} ${
-              row.profileable_from_shell ? 'profileable' : ''
-            }`,
-          ),
-          /* eslint-enable */
+          'td',
+          `${it.debuggable ? 'debuggable' : ''} ${
+            it.profileableFromShell ? 'profileable' : ''
+          }`,
         ),
+        /* eslint-enable */
       );
-    }
+    });
 
     return m(
       'section',
@@ -348,7 +465,7 @@
         sqlConstraints: `severity = 'data_loss' and value > 0`,
       }),
       m(TraceMetadata),
-      m(PackageList),
+      m(PackageListSection),
       m(AndroidGameInterventionList),
       m(StatsSection, {
         queryId: 'info_all',
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index a50bed7..0e60724 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -18,6 +18,7 @@
 import {tryGetTrace} from '../common/cache_manager';
 import {showModal} from '../widgets/modal';
 
+import {loadPermalink} from './permalink';
 import {loadAndroidBugToolInfo} from './android_bug_tool';
 import {globals} from './globals';
 import {Route, Router} from './router';
@@ -34,7 +35,7 @@
 export function maybeOpenTraceFromRoute(route: Route) {
   if (route.args.s) {
     // /?s=xxxx for permalinks.
-    globals.dispatch(Actions.loadPermalink({hash: route.args.s}));
+    loadPermalink(route.args.s);
     return;
   }
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index ec9a43a..10ca865 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -16,7 +16,7 @@
 
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
-import {getContainingTrackId, getLegacySelection} from '../common/state';
+import {getContainingGroupKey, getLegacySelection} from '../common/state';
 import {TrackCacheEntry} from '../common/track_cache';
 import {TrackTags} from '../public';
 
@@ -38,8 +38,7 @@
 import {Button} from '../widgets/button';
 
 interface Attrs {
-  trackGroupId: string;
-  key: string;
+  groupKey: string;
   title: string;
   collapsed: boolean;
   trackFSM?: TrackCacheEntry;
@@ -50,16 +49,14 @@
 export class TrackGroupPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = true;
-  readonly key: string;
-  readonly trackGroupId: string;
+  readonly groupKey: string;
 
   constructor(private attrs: Attrs) {
-    this.trackGroupId = attrs.trackGroupId;
-    this.key = attrs.key;
+    this.groupKey = attrs.groupKey;
   }
 
   render(): m.Children {
-    const {trackGroupId, title, labels, tags, collapsed, trackFSM} = this.attrs;
+    const {groupKey, title, labels, tags, collapsed, trackFSM} = this.attrs;
 
     let name = title;
     if (name[0] === '/') {
@@ -72,25 +69,25 @@
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
       const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      const parentTrackId = getContainingTrackId(globals.state, trackKey);
-      if (parentTrackId === trackGroupId) {
+      const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
+      if (containingGroupKey === groupKey) {
         highlightClass = 'flash';
       }
     }
 
     const selection = getLegacySelection(globals.state);
 
-    const trackGroup = globals.state.trackGroups[trackGroupId];
+    const trackGroup = globals.state.trackGroups[groupKey];
     let checkBox = Icons.BlankCheckbox;
     if (selection !== null && selection.kind === 'AREA') {
       const selectedArea = globals.state.areas[selection.areaId];
       if (
-        selectedArea.tracks.includes(trackGroupId) &&
+        selectedArea.tracks.includes(groupKey) &&
         trackGroup.tracks.every((id) => selectedArea.tracks.includes(id))
       ) {
         checkBox = Icons.Checkbox;
       } else if (
-        selectedArea.tracks.includes(trackGroupId) ||
+        selectedArea.tracks.includes(groupKey) ||
         trackGroup.tracks.some((id) => selectedArea.tracks.includes(id))
       ) {
         checkBox = Icons.IndeterminateCheckbox;
@@ -107,7 +104,7 @@
     return m(
       `.track-group-panel[collapsed=${collapsed}]`,
       {
-        id: 'track_' + trackGroupId,
+        id: 'track_' + groupKey,
         oncreate: () => this.onupdate(),
         onupdate: () => this.onupdate(),
       },
@@ -118,7 +115,7 @@
             if (e.defaultPrevented) return;
             globals.dispatch(
               Actions.toggleTrackGroupCollapsed({
-                trackGroupId,
+                groupKey,
               }),
             ),
               e.stopPropagation();
@@ -143,7 +140,7 @@
               onclick: (e: MouseEvent) => {
                 globals.dispatch(
                   Actions.toggleTrackSelection({
-                    id: trackGroupId,
+                    key: groupKey,
                     isTrackGroup: true,
                   }),
                 );
@@ -180,7 +177,7 @@
     if (!selection || selection.kind !== 'AREA') return;
     const selectedArea = globals.state.areas[selection.areaId];
     const selectedAreaDuration = selectedArea.end - selectedArea.start;
-    if (selectedArea.tracks.includes(this.trackGroupId)) {
+    if (selectedArea.tracks.includes(this.groupKey)) {
       ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
       ctx.fillRect(
         visibleTimeScale.timeToPx(selectedArea.start) + TRACK_SHELL_WIDTH,
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 806b49c..e0aca05 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -199,7 +199,7 @@
                 onclick: (e: MouseEvent) => {
                   globals.dispatch(
                     Actions.toggleTrackSelection({
-                      id: attrs.trackKey,
+                      key: attrs.trackKey,
                       isTrackGroup: false,
                     }),
                   );
@@ -423,10 +423,6 @@
 
   constructor(private readonly attrs: TrackPanelAttrs) {}
 
-  get key(): string {
-    return this.attrs.trackKey;
-  }
-
   get trackKey(): string {
     return this.attrs.trackKey;
   }
diff --git a/ui/src/core_plugins/custom_sql_table_slices/index.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
similarity index 82%
rename from ui/src/core_plugins/custom_sql_table_slices/index.ts
rename to ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index 72f2075..cd31610 100644
--- a/ui/src/core_plugins/custom_sql_table_slices/index.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -18,15 +18,11 @@
 import {Actions} from '../../common/actions';
 import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
 import {LegacySelection} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTabConfigBase} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
-import {
-  NamedSliceTrack,
-  NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginDescriptor} from '../../public';
+import {OnSliceClickArgs} from '../base_slice_track';
+import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
+import {globals} from '../globals';
+import {NamedSliceTrack, NamedSliceTrackTypes} from '../named_slice_track';
+import {NewTrackArgs} from '../track';
 
 export interface CustomSqlImportConfig {
   modules: string[];
@@ -94,10 +90,8 @@
       });
     await this.engine.query(sql);
     return DisposableCallback.from(() => {
-      if (this.engine.isAlive) {
-        this.engine.query(`DROP VIEW ${this.tableName}`);
-        config.dispose?.dispose();
-      }
+      this.engine.tryQuery(`DROP VIEW ${this.tableName}`);
+      config.dispose?.dispose();
     });
   }
 
@@ -139,10 +133,3 @@
     }
   }
 }
-
-class CustomSqlTrackPlugin implements Plugin {}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CustomSqlTrack',
-  plugin: CustomSqlTrackPlugin,
-};
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index b67f2e5..429c391 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -82,11 +82,11 @@
   // Used to prevent global deselection if a pan/drag select occurred.
   private keepCurrentSelection = false;
 
-  private overviewTimelinePanel = new OverviewTimelinePanel('overview');
-  private timeAxisPanel = new TimeAxisPanel('timeaxis');
-  private timeSelectionPanel = new TimeSelectionPanel('timeselection');
-  private notesPanel = new NotesPanel('notes');
-  private tickmarkPanel = new TickmarkPanel('searchTickmarks');
+  private overviewTimelinePanel = new OverviewTimelinePanel();
+  private timeAxisPanel = new TimeAxisPanel();
+  private timeSelectionPanel = new TimeSelectionPanel();
+  private notesPanel = new NotesPanel();
+  private tickmarkPanel = new TickmarkPanel();
 
   private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
 
@@ -128,7 +128,7 @@
         currentY: number,
         editing: boolean,
       ) => {
-        const traceTime = globals.traceTime;
+        const traceTime = globals.traceContext;
         const {visibleTimeScale} = timeline;
         this.keepCurrentSelection = true;
         if (editing) {
@@ -231,8 +231,7 @@
       if (key) {
         const trackBundle = this.resolveTrack(key);
         headerPanel = new TrackGroupPanel({
-          trackGroupId: group.id,
-          key: `trackgroup-${group.id}`,
+          groupKey: group.key,
           trackFSM: trackBundle.trackFSM,
           labels: trackBundle.labels,
           tags: trackBundle.tags,
@@ -241,8 +240,7 @@
         });
       } else {
         headerPanel = new TrackGroupPanel({
-          trackGroupId: group.id,
-          key: `trackgroup-${group.id}`,
+          groupKey: group.key,
           collapsed: group.collapsed,
           title: group.name,
         });
@@ -268,7 +266,6 @@
         collapsed: group.collapsed,
         childPanels: childTracks,
         header: headerPanel,
-        trackGroupId: group.id,
       });
     }
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index aaef484..b4f20ca 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {runQuery} from '../../common/queries';
 import {addDebugSliceTrack} from '../../public';
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 
@@ -170,7 +169,7 @@
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
       name: 'Add track: Android jank CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() => {
+        ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => {
           addDebugSliceTrack(
             ctx.engine,
             {
@@ -189,9 +188,9 @@
       id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
       name: 'Run query: Android jank CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
-          ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'),
-        );
+        ctx.engine
+          .query(JANK_CUJ_QUERY_PRECONDITIONS)
+          .then(() => ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'));
       },
     });
 
@@ -223,7 +222,7 @@
       id: 'dev.perfetto.AndroidCujs#PinBlockingCalls',
       name: 'Add track: Android Blocking calls during CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
+        ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
           addDebugSliceTrack(
             ctx.engine,
             {
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 8f360b4..0666986 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -490,13 +490,25 @@
 const HIGH_CPU = `
   drop table if exists high_cpu;
   create table high_cpu as
-  with base as (
+  with cpu_cycles_args as (
     select
-      ts,
-      EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.uid') as uid,
-      EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.cluster') as cluster,
-      sum(EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.time_millis')) as time_millis
-    from track t join slice s on t.id = s.track_id
+      arg_set_id,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.uid', int_value, null)) as uid,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.cluster', int_value, null)) as cluster,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.time_millis', int_value, null)) as time_millis
+    from args
+    where key in (
+      'cpu_cycles_per_uid_cluster.uid',
+      'cpu_cycles_per_uid_cluster.cluster',
+      'cpu_cycles_per_uid_cluster.time_millis'
+    )
+    group by 1
+  ),
+  base as (
+    select ts, uid, cluster, sum(time_millis) as time_millis
+    from track t
+    join slice s on t.id = s.track_id
+    join cpu_cycles_args using (arg_set_id)
     where t.name = 'Statsd Atoms'
       and s.name = 'cpu_cycles_per_uid_cluster'
     group by 1, 2, 3
@@ -521,8 +533,7 @@
   with_ratio as (
     select
       ts,
-      100.0 * cpu_dur / dur as value,
-      dur,
+      iif(dur is null, 0, 100.0 * cpu_dur / dur) as value,
       case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
       case
           when uid = 0 then 'AID_ROOT'
@@ -533,17 +544,9 @@
           else pl.package_name
       end as pkg
     from with_windows left join app_package_list pl using(uid)
-    where cpu_dur is not null
-  ),
-  with_zeros as (
-      select ts, value, cluster, pkg
-      from with_ratio
-      union all
-      select ts + dur as ts, 0 as value, cluster, pkg
-      from with_ratio
   )
   select ts, sum(value) as value, cluster, pkg
-  from with_zeros
+  from with_ratio
   group by 1, 3, 4`;
 
 const WAKEUPS = `
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 2ec9223..b8feb57 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -14,7 +14,6 @@
 
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {addDebugSliceTrack} from '../../public';
-import {runQuery} from '../../common/queries';
 
 const PERF_TRACE_COUNTERS_PRECONDITION = `
   SELECT
@@ -27,8 +26,8 @@
 
 class AndroidPerfTraceCounters implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const resp = await runQuery(PERF_TRACE_COUNTERS_PRECONDITION, ctx.engine);
-    if (resp.totalRowCount === 0) return;
+    const resp = await ctx.engine.query(PERF_TRACE_COUNTERS_PRECONDITION);
+    if (resp.numRows() === 0) return;
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
       name: 'Add a track to show a thread runtime ipc',
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index 74f2554..7699bd4 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+import m from 'mithril';
+
 import {
   Plugin,
   PluginContext,
@@ -19,17 +21,22 @@
   PluginDescriptor,
 } from '../../public';
 import {duration, Span, Time, time, TimeSpan} from '../../base/time';
+import {redrawModal, showModal} from '../../widgets/modal';
 
 const PLUGIN_ID = 'dev.perfetto.TimelineSync';
 const DEFAULT_BROADCAST_CHANNEL = `${PLUGIN_ID}#broadcastChannel`;
 const VIEWPORT_UPDATE_THROTTLE_TIME_FOR_SENDING_AFTER_RECEIVING_MS = 1_000;
 const BIGINT_PRECISION_MULTIPLIER = 1_000_000_000n;
+const ADVERTISE_PERIOD_MS = 15_000;
 type ClientId = number;
+type SessionId = number;
 
 /**
  * Synchronizes the timeline of 2 or more perfetto traces.
  *
- * To trigger the sync, the command needs to be enabled.
+ * To trigger the sync, the command needs to be executed on one tab. It will
+ * prompt a list of other tabs to keep in sync. Each tab advertise itself
+ * on a BroadcastChannel upon trace load.
  *
  * This is able to sync between traces recorded at different times, even if
  * their durations don't match. The initial viewport bound for each trace is
@@ -38,11 +45,14 @@
 class TimelineSync implements Plugin {
   private _chan?: BroadcastChannel;
   private _ctx?: PluginContextTrace;
+  private _traceLoadTime = 0;
   // Attached to broadcast messages to allow other windows to remap viewports.
-  private _clientId: ClientId = 0;
+  private readonly _clientId: ClientId = Math.floor(Math.random() * 1_000_000);
   // Used to throttle sending updates after one has been received.
   private _lastReceivedUpdateMillis: number = 0;
   private _lastViewportBounds?: ViewportBounds;
+  private _advertisedClients = new Map<ClientId, ClientInfo>();
+  private _sessionId: SessionId = 0;
 
   // Contains the Viewport bounds of this window when it received the first sync
   // message from another one. This is used to re-scale timestamps, so that we
@@ -53,42 +63,160 @@
     ViewportBoundsSnapshot
   >();
 
-  private _active: boolean = false;
-
   onActivate(ctx: PluginContext): void {
     ctx.registerCommand({
       id: `dev.perfetto.SplitScreen#enableTimelineSync`,
-      name: 'Enable timeline sync with open windows',
-      callback: this.enableTimelineSync.bind(this),
+      name: 'Enable timeline sync with other Perfetto UI tabs',
+      callback: () => this.showTimelineSyncDialog(),
     });
     ctx.registerCommand({
       id: `dev.perfetto.SplitScreen#disableTimelineSync`,
       name: 'Disable timeline sync',
-      callback: this.disableTimelineSync.bind(this),
+      callback: () => this.disableTimelineSync(this._sessionId),
     });
+
+    // Start advertising this tab. This allows the command run in other
+    // instances to discover us.
+    this._chan = new BroadcastChannel(DEFAULT_BROADCAST_CHANNEL);
+    this._chan.onmessage = this.onmessage.bind(this);
+    document.addEventListener('visibilitychange', () => this.advertise());
+    setInterval(() => this.advertise(), ADVERTISE_PERIOD_MS);
   }
 
   onDeactivate(_: PluginContext) {
-    this.disableTimelineSync();
+    this.disableTimelineSync(this._sessionId);
   }
 
   async onTraceLoad(ctx: PluginContextTrace) {
     this._ctx = ctx;
+    this._traceLoadTime = Date.now();
+    this.advertise();
   }
 
-  private enableTimelineSync() {
-    this._active = true;
+  async onTraceUnload(_: PluginContextTrace) {
+    this.disableTimelineSync(this._sessionId);
+    this._ctx = undefined;
+  }
+
+  private advertise() {
+    if (this._ctx === undefined) return; // Don't advertise if no trace loaded.
+    this._chan?.postMessage({
+      perfettoSync: {
+        cmd: 'MSG_ADVERTISE',
+        title: document.title,
+        traceLoadTime: this._traceLoadTime,
+      },
+      clientId: this._clientId,
+    } as SyncMessage);
+  }
+
+  private showTimelineSyncDialog() {
+    const selectedClients = new Array<ClientId>();
+
+    // This nested function is invoked when the modal dialog buton is pressed.
+    const doStartSession = () => {
+      // Disable any prior session.
+      this.disableTimelineSync(this._sessionId);
+
+      const clients = selectedClients.concat(this._clientId);
+      this._sessionId = Math.floor(Math.random() * 1_000_000);
+      this._chan?.postMessage({
+        perfettoSync: {
+          cmd: 'MSG_SESSION_START',
+          sessionId: this._sessionId,
+          clients: clients,
+        },
+        clientId: this._clientId,
+      } as SyncMessage);
+      this._initialBoundsForSibling.clear();
+      this.scheduleViewportUpdateMessage();
+    };
+
+    // The function below is called on every mithril render pass. It's important
+    // that this function re-computes the list of other clients on every pass.
+    // The user will go to other tabs (which causes an advertise due to the
+    // visibilitychange listener) and come back on here while the modal dialog
+    // is still being displayed.
+    const renderModalContents = (): m.Children => {
+      const children: m.Children = [];
+      this.purgeInactiveClients();
+      const clients = Array.from(this._advertisedClients.entries());
+      clients.sort((a, b) => b[1].traceLoadTime - a[1].traceLoadTime);
+      for (const [clientId, info] of clients) {
+        const opened = new Date(info.traceLoadTime).toLocaleTimeString();
+        children.push(
+          m(
+            'option',
+            {value: clientId, selected: this._advertisedClients.size === 1},
+            `${info.title} (${opened})`,
+          ),
+        );
+      }
+      return m(
+        'div',
+        {style: 'display: flex;  flex-direction: column;'},
+        m(
+          'div',
+          'Select the perfetto UI tab(s) you want to keep in sync ' +
+            '(Ctrl+Click to select many).',
+        ),
+        m(
+          'div',
+          "If you don't see the trace listed here, temporarily focus the " +
+            'corresponding browser tab and then come back here.',
+        ),
+        m(
+          'select[multiple=multiple][size=8]',
+          {
+            onchange: (e: Event) => {
+              selectedClients.splice(0);
+              const sel = (e.target as HTMLSelectElement).selectedOptions;
+              for (let i = 0; i < sel.length; i++) {
+                const clientId = parseInt(sel[i].value);
+                if (!isNaN(clientId)) selectedClients.push(clientId);
+              }
+            },
+          },
+          children,
+        ),
+      );
+    };
+
+    showModal({
+      title: 'Synchronize timeline across several tabs',
+      content: renderModalContents,
+      buttons: [
+        {
+          primary: true,
+          text: `Synchronize timelines`,
+          action: doStartSession,
+        },
+      ],
+    });
+  }
+
+  private enableTimelineSync(sessionId: SessionId, clients: ClientId[]) {
+    if (sessionId === this._sessionId) return; // Already in this session id.
+    if (!clients.includes(this._clientId)) return; // Not for us.
+    this._sessionId = sessionId;
     this._initialBoundsForSibling.clear();
-    this._clientId = this.generateClientId();
-    this._chan = new BroadcastChannel(DEFAULT_BROADCAST_CHANNEL);
-    this._chan.onmessage = this.onmessage.bind(this);
     this.scheduleViewportUpdateMessage();
   }
 
-  private disableTimelineSync() {
-    this._active = false;
+  private disableTimelineSync(sessionId: SessionId, skipMsg = false) {
+    if (sessionId !== this._sessionId || this._sessionId === 0) return;
+
+    if (!skipMsg) {
+      this._chan?.postMessage({
+        perfettoSync: {
+          cmd: 'MSG_SESSION_STOP',
+          sessionId: this._sessionId,
+        },
+        clientId: this._clientId,
+      } as SyncMessage);
+    }
+    this._sessionId = 0;
     this._initialBoundsForSibling.clear();
-    this._chan?.close();
   }
 
   private shouldThrottleViewportUpdates() {
@@ -99,7 +227,7 @@
   }
 
   private scheduleViewportUpdateMessage() {
-    if (!this._active) return;
+    if (!this.active) return;
     const currentViewport = this.getCurrentViewportBounds();
     if (
       (!this._lastViewportBounds ||
@@ -116,6 +244,7 @@
     this._chan?.postMessage({
       perfettoSync: {
         cmd: 'MSG_SET_VIEWPORT',
+        sessionId: this._sessionId,
         viewportBounds,
       },
       clientId: this._clientId,
@@ -123,12 +252,33 @@
   }
 
   private onmessage(msg: MessageEvent) {
+    if (this._ctx === undefined) return; // Trace unloaded
     if (!('perfettoSync' in msg.data)) return;
     const msgData = msg.data as SyncMessage;
     const sync = msgData.perfettoSync;
     switch (sync.cmd) {
+      case 'MSG_ADVERTISE':
+        if (msgData.clientId !== this._clientId) {
+          this._advertisedClients.set(msgData.clientId, {
+            title: sync.title,
+            traceLoadTime: sync.traceLoadTime,
+            lastHeartbeat: Date.now(),
+          });
+          this.purgeInactiveClients();
+          redrawModal();
+        }
+        break;
+      case 'MSG_SESSION_START':
+        this.enableTimelineSync(sync.sessionId, sync.clients);
+        break;
+      case 'MSG_SESSION_STOP':
+        this.disableTimelineSync(sync.sessionId, /* skipMsg= */ true);
+        break;
       case 'MSG_SET_VIEWPORT':
-        this.onViewportSyncReceived(sync.viewportBounds, msgData.clientId);
+        if (sync.sessionId === this._sessionId) {
+          this.onViewportSyncReceived(sync.viewportBounds, msgData.clientId);
+        }
+        break;
     }
   }
 
@@ -136,6 +286,7 @@
     requestViewBounds: ViewportBounds,
     source: ClientId,
   ) {
+    if (!this.active) return;
     this.cacheSiblingInitialBoundIfNeeded(requestViewBounds, source);
     const remappedViewport = this.remapViewportBounds(
       requestViewBounds,
@@ -222,8 +373,17 @@
     return this._ctx!.timeline.viewport;
   }
 
-  private generateClientId(): ClientId {
-    return Math.floor(Math.random() * 1_000_000);
+  private purgeInactiveClients() {
+    const now = Date.now();
+    const TIMEOUT_MS = 30_000;
+    for (const [clientId, info] of this._advertisedClients.entries()) {
+      if (now - info.lastHeartbeat < TIMEOUT_MS) continue;
+      this._advertisedClients.delete(clientId);
+    }
+  }
+
+  private get active() {
+    return this._sessionId !== 0;
   }
 }
 
@@ -236,17 +396,45 @@
 
 interface MsgSetViewport {
   cmd: 'MSG_SET_VIEWPORT';
+  sessionId: SessionId;
   viewportBounds: ViewportBounds;
 }
 
+interface MsgAdvertise {
+  cmd: 'MSG_ADVERTISE';
+  title: string;
+  traceLoadTime: number;
+}
+
+interface MsgSessionStart {
+  cmd: 'MSG_SESSION_START';
+  sessionId: SessionId;
+  clients: ClientId[];
+}
+
+interface MsgSessionStop {
+  cmd: 'MSG_SESSION_STOP';
+  sessionId: SessionId;
+}
+
 // In case of new messages, they should be "or-ed" here.
-type SyncMessages = MsgSetViewport;
+type SyncMessages =
+  | MsgSetViewport
+  | MsgAdvertise
+  | MsgSessionStart
+  | MsgSessionStop;
 
 interface SyncMessage {
   perfettoSync: SyncMessages;
   clientId: ClientId;
 }
 
+interface ClientInfo {
+  title: string;
+  lastHeartbeat: number; // Datetime.now() of the last MSG_ADVERTISE.
+  traceLoadTime: number; // Datetime.now() of the onTraceLoad().
+}
+
 export const plugin: PluginDescriptor = {
   pluginId: PLUGIN_ID,
   plugin: TimelineSync,
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index eca39b4..9f627b8 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -20,7 +20,7 @@
   STR_NULL,
 } from '../../public';
 import {ASYNC_SLICE_TRACK_KIND} from '../../core_plugins/async_slices';
-import {AsyncSliceTrackV2} from '../../core_plugins/async_slices/async_slice_track_v2';
+import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
 
 // This plugin renders visualizations of runtime power state transitions for
 // Linux kernel devices (devices managed by Linux drivers).
@@ -50,7 +50,7 @@
         trackIds: [trackId],
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2(
+          return new AsyncSliceTrack(
             {
               engine: ctx.engine,
               trackKey,
diff --git a/ui/src/protos/index.ts b/ui/src/protos/index.ts
index 378fcfb..44270c2 100644
--- a/ui/src/protos/index.ts
+++ b/ui/src/protos/index.ts
@@ -75,9 +75,7 @@
 import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
 import StatusResult = protos.perfetto.protos.StatusResult;
 import SysStatsConfig = protos.perfetto.protos.SysStatsConfig;
-import Trace = protos.perfetto.protos.Trace;
 import TraceConfig = protos.perfetto.protos.TraceConfig;
-import TracePacket = protos.perfetto.protos.TracePacket;
 import TraceProcessorApiVersion = protos.perfetto.protos.TraceProcessorApiVersion;
 import TraceProcessorRpc = protos.perfetto.protos.TraceProcessorRpc;
 import TraceProcessorRpcStream = protos.perfetto.protos.TraceProcessorRpcStream;
@@ -144,9 +142,7 @@
   StatCounters,
   StatusResult,
   SysStatsConfig,
-  Trace,
   TraceConfig,
-  TracePacket,
   TraceProcessorApiVersion,
   TraceProcessorRpc,
   TraceProcessorRpcStream,
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 7fb2e44..9e540fd 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -22,6 +22,8 @@
 import {PanelSize} from '../frontend/panel';
 import {Engine} from '../trace_processor/engine';
 import {UntypedEventSet} from '../core/event_set';
+import {TraceContext} from '../frontend/globals';
+import {PromptOption} from '../frontend/omnibox_manager';
 
 export {Engine} from '../trace_processor/engine';
 export {
@@ -34,6 +36,7 @@
 } from '../trace_processor/query_result';
 export {BottomTabToSCSAdapter} from './utils';
 export {createStore, Migrate, Store} from '../base/store';
+export {PromptOption} from '../frontend/omnibox_manager';
 
 // This is a temporary fix until this is available in the plugin API.
 export {
@@ -433,10 +436,9 @@
   // Create a store mounted over the top of this plugin's persistent state.
   mountStore<T>(migrate: Migrate<T>): Store<T>;
 
-  trace: {
-    // A span representing the start and end time of the trace
-    readonly span: Span<time, duration>;
-  };
+  trace: TraceContext;
+
+  prompt(text: string, options?: PromptOption[]): Promise<string>;
 }
 
 export interface Plugin {
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 90901c5..d0f70af 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -14,7 +14,6 @@
 
 import {defer, Deferred} from '../base/deferred';
 import {assertExists, assertTrue} from '../base/logging';
-import {duration, Span, Time, time, TimeSpan} from '../base/time';
 import {
   ComputeMetricArgs,
   ComputeMetricResult,
@@ -31,17 +30,14 @@
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   createQueryResult,
-  LONG,
-  LONG_NULL,
-  NUM,
   QueryError,
   QueryResult,
-  STR,
   WritableQueryResult,
 } from './query_result';
 
 import TPM = TraceProcessorRpc.TraceProcessorMethod;
 import {Disposable} from '../base/disposable';
+import {Result} from '../base/utils';
 
 export interface LoadingTracker {
   beginLoading(): void;
@@ -67,16 +63,43 @@
 }
 
 export interface Engine {
-  execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult;
-  query(sqlQuery: string, tag?: string): Promise<QueryResult>;
-  getCpus(): Promise<number[]>;
-  getNumberOfGpus(): Promise<number>;
-  getTracingMetadataTimeBounds(): Promise<Span<time, duration>>;
+  /**
+   * Execute a query against the database, returning a promise that resolves
+   * when the query has completed but rejected when the query fails for whatever
+   * reason. On success, the promise will only resolve once all the resulting
+   * rows have been received.
+   *
+   * The promise will be rejected if the query fails.
+   *
+   * @param sql The query to execute.
+   * @param tag An optional tag used to trace the origin of the query.
+   */
+  query(sql: string, tag?: string): Promise<QueryResult>;
+
+  /**
+   * Execute a query against the database, returning a promise that resolves
+   * when the query has completed or failed. The promise will never get
+   * rejected, it will always successfully resolve. Use the returned wrapper
+   * object to determine whether the query completed successfully.
+   *
+   * The promise will only resolve once all the resulting rows have been
+   * received.
+   *
+   * @param sql The query to execute.
+   * @param tag An optional tag used to trace the origin of the query.
+   */
+  tryQuery(sql: string, tag?: string): Promise<Result<QueryResult, Error>>;
+
+  /**
+   * Execute one or more metric and get the result.
+   *
+   * @param metrics The metrics to run.
+   * @param format The format of the response.
+   */
   computeMetric(
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array>;
-  readonly isAlive: boolean;
 }
 
 // Abstract interface of a trace proccessor.
@@ -92,8 +115,6 @@
 // 2. Call onRpcResponseBytes() when response data is received.
 export abstract class EngineBase implements Engine {
   abstract readonly id: string;
-  private _cpus?: number[];
-  private _numGpus?: number;
   private loadingTracker: LoadingTracker;
   private txSeqId = 0;
   private rxSeqId = 0;
@@ -106,7 +127,6 @@
   private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
   private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
   private _isMetatracingEnabled = false;
-  readonly isAlive = false;
 
   constructor(tracker?: LoadingTracker) {
     this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
@@ -360,7 +380,10 @@
   //
   // Optional |tag| (usually a component name) can be provided to allow
   // attributing trace processor workload to different UI components.
-  execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult {
+  private streamingQuery(
+    sqlQuery: string,
+    tag?: string,
+  ): Promise<QueryResult> & QueryResult {
     const rpc = TraceProcessorRpc.create();
     rpc.request = TPM.TPM_QUERY_STREAMING;
     rpc.queryArgs = new QueryArgs();
@@ -376,13 +399,13 @@
     return result;
   }
 
-  // Wraps .execute(), captures errors and re-throws with current stack.
+  // Wraps .streamingQuery(), captures errors and re-throws with current stack.
   //
-  // Note: This function is less flexible that .execute() as it only returns a
+  // Note: This function is less flexible than .execute() as it only returns a
   // promise which must be unwrapped before the QueryResult may be accessed.
   async query(sqlQuery: string, tag?: string): Promise<QueryResult> {
     try {
-      return await this.execute(sqlQuery, tag);
+      return await this.streamingQuery(sqlQuery, tag);
     } catch (e) {
       // Replace the error's stack trace with the one from here
       // Note: It seems only V8 can trace the stack up the promise chain, so its
@@ -394,6 +417,19 @@
     }
   }
 
+  async tryQuery(
+    sql: string,
+    tag?: string,
+  ): Promise<Result<QueryResult, Error>> {
+    try {
+      const result = await this.query(sql, tag);
+      return {success: true, result};
+    } catch (error: unknown) {
+      // We know we only throw Error type objects so we can type assert safely
+      return {success: false, error: error as Error};
+    }
+  }
+
   isMetatracingEnabled(): boolean {
     return this._isMetatracingEnabled;
   }
@@ -438,79 +474,6 @@
     this.rpcSendRequestBytes(buf);
   }
 
-  // TODO(hjd): When streaming must invalidate this somehow.
-  async getCpus(): Promise<number[]> {
-    if (!this._cpus) {
-      const cpus = [];
-      const queryRes = await this.query(
-        'select distinct(cpu) as cpu from sched order by cpu;',
-      );
-      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
-        cpus.push(it.cpu);
-      }
-      this._cpus = cpus;
-    }
-    return this._cpus;
-  }
-
-  async getNumberOfGpus(): Promise<number> {
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!this._numGpus) {
-      const result = await this.query(`
-        select count(distinct(gpu_id)) as gpuCount
-        from gpu_counter_track
-        where name = 'gpufreq';
-      `);
-      this._numGpus = result.firstRow({gpuCount: NUM}).gpuCount;
-    }
-    return this._numGpus;
-  }
-
-  // TODO: This should live in code that's more specific to chrome, instead of
-  // in engine.
-  async getNumberOfProcesses(): Promise<number> {
-    const result = await this.query('select count(*) as cnt from process;');
-    return result.firstRow({cnt: NUM}).cnt;
-  }
-
-  async getTraceTimeBounds(): Promise<Span<time, duration>> {
-    const result = await this.query(
-      `select start_ts as startTs, end_ts as endTs from trace_bounds`,
-    );
-    const bounds = result.firstRow({
-      startTs: LONG,
-      endTs: LONG,
-    });
-    return new TimeSpan(
-      Time.fromRaw(bounds.startTs),
-      Time.fromRaw(bounds.endTs),
-    );
-  }
-
-  async getTracingMetadataTimeBounds(): Promise<Span<time, duration>> {
-    const queryRes = await this.query(`select
-         name,
-         int_value as intValue
-         from metadata
-         where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
-         or name = 'all_data_source_started_ns'`);
-    let startBound = Time.MIN;
-    let endBound = Time.MAX;
-    const it = queryRes.iter({name: STR, intValue: LONG_NULL});
-    for (; it.valid(); it.next()) {
-      const columnName = it.name;
-      const timestamp = it.intValue;
-      if (timestamp === null) continue;
-      if (columnName === 'tracing_disabled_ns') {
-        endBound = Time.min(endBound, Time.fromRaw(timestamp));
-      } else {
-        startBound = Time.max(startBound, Time.fromRaw(timestamp));
-      }
-    }
-
-    return new TimeSpan(startBound, endBound);
-  }
-
   getProxy(tag: string): EngineProxy {
     return new EngineProxy(this, tag);
   }
@@ -522,58 +485,42 @@
   private tag: string;
   private _isAlive: boolean;
 
-  get isAlive(): boolean {
-    return this._isAlive;
-  }
-
   constructor(engine: EngineBase, tag: string) {
     this.engine = engine;
     this.tag = tag;
     this._isAlive = true;
   }
 
-  execute(query: string, tag?: string): Promise<QueryResult> & QueryResult {
-    if (!this.isAlive) {
-      throw new Error(`EngineProxy ${this.tag} was disposed.`);
-    }
-    return this.engine.execute(query, tag || this.tag);
-  }
-
   async query(query: string, tag?: string): Promise<QueryResult> {
-    if (!this.isAlive) {
+    if (!this._isAlive) {
       throw new Error(`EngineProxy ${this.tag} was disposed.`);
     }
     return await this.engine.query(query, tag);
   }
 
+  async tryQuery(
+    query: string,
+    tag?: string,
+  ): Promise<Result<QueryResult, Error>> {
+    if (!this._isAlive) {
+      return {
+        success: false,
+        error: new Error(`EngineProxy ${this.tag} was disposed.`),
+      };
+    }
+    return await this.engine.tryQuery(query, tag);
+  }
+
   async computeMetric(
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array> {
-    if (!this.isAlive) {
+    if (!this._isAlive) {
       return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
     }
     return this.engine.computeMetric(metrics, format);
   }
 
-  async getCpus(): Promise<number[]> {
-    if (!this.isAlive) {
-      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
-    }
-    return this.engine.getCpus();
-  }
-
-  async getNumberOfGpus(): Promise<number> {
-    if (!this.isAlive) {
-      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
-    }
-    return this.engine.getNumberOfGpus();
-  }
-
-  async getTracingMetadataTimeBounds(): Promise<Span<time, bigint>> {
-    return this.engine.getTracingMetadataTimeBounds();
-  }
-
   get engineId(): string {
     return this.engine.id;
   }
diff --git a/ui/src/trace_processor/query_result.ts b/ui/src/trace_processor/query_result.ts
index ac48079..a29f4dc 100644
--- a/ui/src/trace_processor/query_result.ts
+++ b/ui/src/trace_processor/query_result.ts
@@ -57,6 +57,7 @@
 import {utf8Decode} from '../base/string_utils';
 import {Duration, duration, Time, time} from '../base/time';
 
+export const UNKNOWN: ColumnType = null;
 export const NUM = 0;
 export const STR = 'str';
 export const NUM_NULL: number | null = 1;
@@ -203,6 +204,8 @@
       return 'LONG';
     case LONG_NULL:
       return 'LONG_NULL';
+    case UNKNOWN:
+      return 'UNKNOWN';
     default:
       return `INVALID(${t})`;
   }
@@ -215,21 +218,25 @@
         expected === NUM_NULL ||
         expected === STR_NULL ||
         expected === BLOB_NULL ||
-        expected === LONG_NULL
+        expected === LONG_NULL ||
+        expected === UNKNOWN
       );
     case CellType.CELL_VARINT:
       return (
         expected === NUM ||
         expected === NUM_NULL ||
         expected === LONG ||
-        expected === LONG_NULL
+        expected === LONG_NULL ||
+        expected === UNKNOWN
       );
     case CellType.CELL_FLOAT64:
-      return expected === NUM || expected === NUM_NULL;
+      return expected === NUM || expected === NUM_NULL || expected === UNKNOWN;
     case CellType.CELL_STRING:
-      return expected === STR || expected === STR_NULL;
+      return expected === STR || expected === STR_NULL || expected === UNKNOWN;
     case CellType.CELL_BLOB:
-      return expected === BLOB || expected === BLOB_NULL;
+      return (
+        expected === BLOB || expected === BLOB_NULL || expected === UNKNOWN
+      );
     default:
       throw new Error(`Unknown CellType ${actual}`);
   }
@@ -285,11 +292,10 @@
   // If true all rows have been fetched. Calling iter() will iterate through the
   // last row. If false, iter() will return an iterator which might iterate
   // through some rows (or none) but will surely not reach the end.
-
   isComplete(): boolean;
 
   // Returns a promise that is resolved only when all rows (i.e. all batches)
-  // have been fetched. The promise return value is always the object iself.
+  // have been fetched. The promise return value is always the object itself.
   waitAllRows(): Promise<QueryResult>;
 
   // Returns a promise that is resolved when either:
diff --git a/ui/src/widgets/modal.ts b/ui/src/widgets/modal.ts
index 778f07e..3c85652 100644
--- a/ui/src/widgets/modal.ts
+++ b/ui/src/widgets/modal.ts
@@ -228,6 +228,15 @@
   return returnedClosePromise;
 }
 
+// Technically we don't need to redraw the whole app, but it's the more
+// pragmatic option. This is exposed to keep the plugin code more clear, so it's
+// evident why a redraw is requested.
+export function redrawModal() {
+  if (currentModal !== undefined) {
+    scheduleFullRedraw();
+  }
+}
+
 // Closes the full-screen modal dialog (if any).
 // `key` is optional: if provided it will close the modal dialog only if the key
 // matches. This is to avoid accidentally closing another dialog that popped
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 39be606..89185c3 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -75,31 +75,32 @@
     if (this.engine === undefined) {
       return '';
     }
-    const result = this.engine.execute(uri);
     try {
-      await result.waitAllRows();
+      const result = await this.engine.query(uri);
+      const columns = result.columns();
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const rows: any[] = [];
+      for (const it = result.iter({}); it.valid(); it.next()) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const row: any = {};
+        for (const name of columns) {
+          let value = it.get(name);
+          if (typeof value === 'bigint') {
+            value = Number(value);
+          }
+          row[name] = value;
+        }
+        rows.push(row);
+      }
+      return JSON.stringify(rows);
     } catch (e) {
       if (e instanceof QueryError) {
-        console.error(result.error());
+        console.error(e);
         return '';
+      } else {
+        throw e;
       }
     }
-    const columns = result.columns();
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const rows: any[] = [];
-    for (const it = result.iter({}); it.valid(); it.next()) {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const row: any = {};
-      for (const name of columns) {
-        let value = it.get(name);
-        if (typeof value === 'bigint') {
-          value = Number(value);
-        }
-        row[name] = value;
-      }
-      rows.push(row);
-    }
-    return JSON.stringify(rows);
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any