Merge "test: Remove old unused header" into main
diff --git a/Android.bp b/Android.bp
index 0efdda3..2341f3b 100644
--- a/Android.bp
+++ b/Android.bp
@@ -2467,6 +2467,8 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -12394,6 +12396,20 @@
     ],
 }
 
+// GN: //src/trace_processor/importers/art_method:art_method
+filegroup {
+    name: "perfetto_src_trace_processor_importers_art_method_art_method",
+    srcs: [
+        "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+    ],
+}
+
+// GN: //src/trace_processor/importers/art_method:art_method_event
+filegroup {
+    name: "perfetto_src_trace_processor_importers_art_method_art_method_event",
+}
+
 // GN: //src/trace_processor/importers/common:common
 filegroup {
     name: "perfetto_src_trace_processor_importers_common_common",
@@ -15583,6 +15599,8 @@
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
         ":perfetto_src_trace_processor_importers_android_bugreport_unittests",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16653,6 +16671,8 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -16901,6 +16921,7 @@
         ":perfetto_src_trace_processor_db_compare",
         ":perfetto_src_trace_processor_db_minimal",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
@@ -17072,6 +17093,8 @@
         ":perfetto_src_trace_processor_export_json",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_bugreport",
         ":perfetto_src_trace_processor_importers_android_bugreport_android_log_event",
+        ":perfetto_src_trace_processor_importers_art_method_art_method",
+        ":perfetto_src_trace_processor_importers_art_method_art_method_event",
         ":perfetto_src_trace_processor_importers_common_common",
         ":perfetto_src_trace_processor_importers_common_parser_types",
         ":perfetto_src_trace_processor_importers_common_trace_parser_hdr",
diff --git a/BUILD b/BUILD
index 5a733b4..4e69402 100644
--- a/BUILD
+++ b/BUILD
@@ -223,6 +223,8 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -1515,6 +1517,25 @@
     ],
 )
 
+# GN target: //src/trace_processor/importers/art_method:art_method
+perfetto_filegroup(
+    name = "src_trace_processor_importers_art_method_art_method",
+    srcs = [
+        "src/trace_processor/importers/art_method/art_method_parser_impl.cc",
+        "src/trace_processor/importers/art_method/art_method_parser_impl.h",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.cc",
+        "src/trace_processor/importers/art_method/art_method_tokenizer.h",
+    ],
+)
+
+# GN target: //src/trace_processor/importers/art_method:art_method_event
+perfetto_filegroup(
+    name = "src_trace_processor_importers_art_method_art_method_event",
+    srcs = [
+        "src/trace_processor/importers/art_method/art_method_event.h",
+    ],
+)
+
 # GN target: //src/trace_processor/importers/common:common
 perfetto_filegroup(
     name = "src_trace_processor_importers_common_common",
@@ -6357,6 +6378,8 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6556,6 +6579,8 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
@@ -6812,6 +6837,8 @@
         ":src_trace_processor_export_json",
         ":src_trace_processor_importers_android_bugreport_android_bugreport",
         ":src_trace_processor_importers_android_bugreport_android_log_event",
+        ":src_trace_processor_importers_art_method_art_method",
+        ":src_trace_processor_importers_art_method_art_method_event",
         ":src_trace_processor_importers_common_common",
         ":src_trace_processor_importers_common_parser_types",
         ":src_trace_processor_importers_common_trace_parser_hdr",
diff --git a/CHANGELOG b/CHANGELOG
index 03fa600..5c1cf03 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -6,9 +6,18 @@
     * Increased watchdog timeout to 180s from 30s to make watchdog crashes
       much less likely when system is under heavy load.
   SQL Standard library:
-    *
+    * Improved CPU cycles calculation in `linux.cpu.utilization` modules:
+     `process`, `system` and `thread` by fixing a bug responsible for too high
+      CPU cycles values.
+    * Introduces functions responsible for calculating CPU cycles with
+      breakdown by CPU, thread, process and slice for a given interval.
   Trace Processor:
-    *
+    * Renamed Trace Processor's C++ method `RegisterSqlModule()` to
+     `RegisterSqlPackage()`, which better represents the module/package
+      relationship. Package is the top level grouping of modules, which are
+      objects being included with `INCLUDE PERFETTO MODULE`.
+      `RegisterSqlModule()` is still available and runs `RegisterSqlPackage()`.
+      `RegisterSqlModule()` will be deprecated in v50.0.
   UI:
     * Scheduling wakeup information now reflects whether the wakeup came
       from an interrupt context. The per-cpu scheduling tracks now show only
diff --git a/include/perfetto/trace_processor/basic_types.h b/include/perfetto/trace_processor/basic_types.h
index f06b782..44ead6e 100644
--- a/include/perfetto/trace_processor/basic_types.h
+++ b/include/perfetto/trace_processor/basic_types.h
@@ -263,9 +263,9 @@
 };
 
 // Data used to register a new SQL package.
-struct SqlModule {
+struct SqlPackage {
   // Must be unique among modules, or can be used to override existing module if
-  // |allow_module_override| is set.
+  // |allow_override| is set.
   std::string name;
 
   // Pairs of strings used for |INCLUDE| with the contents of SQL files being
@@ -276,10 +276,18 @@
   // run, with slashes replaced by dots and without the SQL extension. For
   // example, 'android/camera/jank.sql' would be included by
   // 'android.camera.jank'.
-  std::vector<std::pair<std::string, std::string>> files;
+  std::vector<std::pair<std::string, std::string>> modules;
 
   // If true, SqlPackage will override registered module with the same name. Can
   // only be set if enable_dev_features is true, otherwise will throw an error.
+  bool allow_override = false;
+};
+
+// Deprecated. Use only with |trace_processor->RegisterSqlModule()|. Alias of
+// |SqlPackage|.
+struct SqlModule {
+  std::string name;
+  std::vector<std::pair<std::string, std::string>> files;
   bool allow_module_override = false;
 };
 
diff --git a/include/perfetto/trace_processor/trace_processor.h b/include/perfetto/trace_processor/trace_processor.h
index 56d6630..7f70653 100644
--- a/include/perfetto/trace_processor/trace_processor.h
+++ b/include/perfetto/trace_processor/trace_processor.h
@@ -63,7 +63,7 @@
   // PERFETTO MODULE camera.cpu.metrics". The first word of the string has to be
   // a package name and there can be only one package registered with a given
   // name.
-  virtual base::Status RegisterSqlModule(SqlModule) = 0;
+  virtual base::Status RegisterSqlPackage(SqlPackage) = 0;
 
   // Registers a metric at the given path which will run the specified SQL.
   virtual base::Status RegisterMetric(const std::string& path,
@@ -138,6 +138,11 @@
   // loaded by trace processor shell at runtime. The message is encoded as
   // DescriptorSet, defined in perfetto/trace_processor/trace_processor.proto.
   virtual std::vector<uint8_t> GetMetricDescriptors() = 0;
+
+  // Deprecated. Use |RegisterSqlPackage()| instead, which is identical in
+  // functionality to |RegisterSqlModule()| and the only difference is in
+  // the argument, which is directly translatable to |SqlPackage|.
+  virtual base::Status RegisterSqlModule(SqlModule) = 0;
 };
 
 }  // namespace trace_processor
diff --git a/protos/perfetto/trace_processor/trace_processor.proto b/protos/perfetto/trace_processor/trace_processor.proto
index c597206..049d089 100644
--- a/protos/perfetto/trace_processor/trace_processor.proto
+++ b/protos/perfetto/trace_processor/trace_processor.proto
@@ -88,13 +88,15 @@
   optional string fatal_error = 5;
 
   enum TraceProcessorMethod {
+    reserved 4, 12;
+    reserved "TPM_QUERY_RAW_DEPRECATED";
+    reserved "TPM_REGISTER_SQL_MODULE";
+
     TPM_UNSPECIFIED = 0;
     TPM_APPEND_TRACE_DATA = 1;
     TPM_FINALIZE_TRACE_DATA = 2;
     TPM_QUERY_STREAMING = 3;
     // Previously: TPM_QUERY_RAW_DEPRECATED
-    reserved 4;
-    reserved "TPM_QUERY_RAW_DEPRECATED";
     TPM_COMPUTE_METRIC = 5;
     TPM_GET_METRIC_DESCRIPTORS = 6;
     TPM_RESTORE_INITIAL_TABLES = 7;
@@ -102,7 +104,7 @@
     TPM_DISABLE_AND_READ_METATRACE = 9;
     TPM_GET_STATUS = 10;
     TPM_RESET_TRACE_PROCESSOR = 11;
-    TPM_REGISTER_SQL_MODULE = 12;
+    TPM_REGISTER_SQL_PACKAGE = 13;
   }
 
   oneof type {
@@ -134,8 +136,8 @@
     EnableMetatraceArgs enable_metatrace_args = 106;
     // For TPM_RESET_TRACE_PROCESSOR.
     ResetTraceProcessorArgs reset_trace_processor_args = 107;
-    // For TPM_REGISTER_SQL_MODULE.
-    RegisterSqlModuleArgs register_sql_module_args = 108;
+    // For TPM_REGISTER_SQL_PACKAGE.
+    RegisterSqlPackageArgs register_sql_package_args = 108;
 
     // TraceProcessorMethod response args.
     // For TPM_APPEND_TRACE_DATA.
@@ -150,8 +152,8 @@
     DisableAndReadMetatraceResult metatrace = 209;
     // For TPM_GET_STATUS.
     StatusResult status = 210;
-    // For TPM_REGISTER_SQL_MODULE.
-    RegisterSqlModuleResult register_sql_module_result = 211;
+    // For TPM_REGISTER_SQL_PACKAGE.
+    RegisterSqlPackageResult register_sql_package_result = 211;
   }
 
   // Previously: RawQueryArgs for TPM_QUERY_RAW_DEPRECATED
@@ -335,15 +337,16 @@
   optional bool ftrace_drop_until_all_cpus_valid = 4;
 }
 
-message RegisterSqlModuleArgs {
+message RegisterSqlPackageArgs {
   message Module {
     optional string name = 1;
     optional string sql = 2;
   }
-  optional string top_level_package_name = 1;
+  optional string package_name = 1;
   repeated Module modules = 2;
-  optional bool allow_module_override = 3;
+  optional bool allow_override = 3;
 }
-message RegisterSqlModuleResult {
+
+message RegisterSqlPackageResult {
   optional string error = 1;
 }
\ No newline at end of file
diff --git a/python/perfetto/trace_processor/trace_processor.descriptor b/python/perfetto/trace_processor/trace_processor.descriptor
index 0cc3646..c86d6c5 100644
--- a/python/perfetto/trace_processor/trace_processor.descriptor
+++ b/python/perfetto/trace_processor/trace_processor.descriptor
Binary files differ
diff --git a/python/tools/check_ratchet.py b/python/tools/check_ratchet.py
index 573e6a3..0c845d2 100755
--- a/python/tools/check_ratchet.py
+++ b/python/tools/check_ratchet.py
@@ -37,7 +37,7 @@
 from dataclasses import dataclass
 
 EXPECTED_ANY_COUNT = 59
-EXPECTED_RUN_METRIC_COUNT = 5
+EXPECTED_RUN_METRIC_COUNT = 4
 
 ROOT_DIR = os.path.dirname(
     os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
diff --git a/src/shared_lib/test/api_integrationtest.cc b/src/shared_lib/test/api_integrationtest.cc
index 025ef37..d5c7d45 100644
--- a/src/shared_lib/test/api_integrationtest.cc
+++ b/src/shared_lib/test/api_integrationtest.cc
@@ -66,6 +66,7 @@
 using ::testing::DoAll;
 using ::testing::ElementsAre;
 using ::testing::InSequence;
+using ::testing::IsNull;
 using ::testing::NiceMock;
 using ::testing::ResultOf;
 using ::testing::Return;
@@ -966,6 +967,56 @@
   PERFETTO_DS_TRACE(data_source_1, ctx) {}
 }
 
+TEST_F(SharedLibDataSourceTest, GetInstanceLockedSuccess) {
+  bool ignored = false;
+  void* const kInstancePtr = &ignored;
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
+      .WillOnce(Return(kInstancePtr));
+  TracingSession tracing_session =
+      TracingSession::Builder().set_data_source_name(kDataSourceName2).Build();
+
+  void* arg = nullptr;
+  PERFETTO_DS_TRACE(data_source_2, ctx) {
+    arg = PerfettoDsImplGetInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+    if (arg) {
+      PerfettoDsImplReleaseInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+    }
+  }
+
+  EXPECT_EQ(arg, kInstancePtr);
+}
+
+TEST_F(SharedLibDataSourceTest, GetInstanceLockedFailure) {
+  bool ignored = false;
+  void* const kInstancePtr = &ignored;
+  EXPECT_CALL(ds2_callbacks_, OnSetup(_, _, _, _, kDataSource2UserArg, _))
+      .WillOnce(Return(kInstancePtr));
+  TracingSession tracing_session =
+      TracingSession::Builder().set_data_source_name(kDataSourceName2).Build();
+
+  WaitableEvent inside_tracing;
+  WaitableEvent stopped;
+
+  std::thread t([&] {
+    PERFETTO_DS_TRACE(data_source_2, ctx) {
+      inside_tracing.Notify();
+      stopped.WaitForNotification();
+      void* arg =
+          PerfettoDsImplGetInstanceLocked(data_source_2.impl, ctx.impl.inst_id);
+      if (arg) {
+        PerfettoDsImplReleaseInstanceLocked(data_source_2.impl,
+                                            ctx.impl.inst_id);
+      }
+      EXPECT_THAT(arg, IsNull());
+    }
+  });
+
+  inside_tracing.WaitForNotification();
+  tracing_session.StopBlocking();
+  stopped.Notify();
+  t.join();
+}
+
 // Regression test for a `PerfettoDsImplReleaseInstanceLocked()`. Under very
 // specific circumstances, that depends on the implementation details of
 // `TracingMuxerImpl`, the following events can happen:
diff --git a/src/trace_processor/BUILD.gn b/src/trace_processor/BUILD.gn
index 331634c..89c5ee8 100644
--- a/src/trace_processor/BUILD.gn
+++ b/src/trace_processor/BUILD.gn
@@ -168,6 +168,7 @@
       "../protozero",
       "db",
       "importers/android_bugreport",
+      "importers/art_method",
       "importers/common",
       "importers/etw:full",
       "importers/ftrace:full",
diff --git a/src/trace_processor/forwarding_trace_parser.cc b/src/trace_processor/forwarding_trace_parser.cc
index 4200209..babde0c 100644
--- a/src/trace_processor/forwarding_trace_parser.cc
+++ b/src/trace_processor/forwarding_trace_parser.cc
@@ -68,6 +68,7 @@
     case kZipFile:
     case kAndroidLogcatTraceType:
     case kGeckoTraceType:
+    case kArtMethodTraceType:
       return TraceSorter::SortingMode::kFullSort;
 
     case kProtoTraceType:
diff --git a/src/trace_processor/importers/art_method/BUILD.gn b/src/trace_processor/importers/art_method/BUILD.gn
new file mode 100644
index 0000000..57f6c62
--- /dev/null
+++ b/src/trace_processor/importers/art_method/BUILD.gn
@@ -0,0 +1,45 @@
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import("../../../../gn/test.gni")
+
+source_set("art_method_event") {
+  sources = [ "art_method_event.h" ]
+  deps = [
+    "../../../../gn:default_deps",
+    "../../containers",
+  ]
+}
+
+source_set("art_method") {
+  sources = [
+    "art_method_parser_impl.cc",
+    "art_method_parser_impl.h",
+    "art_method_tokenizer.cc",
+    "art_method_tokenizer.h",
+  ]
+  deps = [
+    ":art_method_event",
+    "../../../../gn:default_deps",
+    "../../../../protos/perfetto/common:zero",
+    "../../../base",
+    "../../containers",
+    "../../importers/common",
+    "../../sorter",
+    "../../storage",
+    "../../types",
+    "../../util",
+    "../../util:trace_blob_view_reader",
+  ]
+}
diff --git a/src/trace_processor/importers/art_method/art_method_event.h b/src/trace_processor/importers/art_method/art_method_event.h
new file mode 100644
index 0000000..1a5edb2
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_event.h
@@ -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.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
+
+#include <cstdint>
+#include <optional>
+
+#include "src/trace_processor/containers/string_pool.h"
+
+namespace perfetto::trace_processor::art_method {
+
+struct alignas(8) ArtMethodEvent {
+  uint32_t tid;
+  StringPool::Id method;
+  enum { kEnter, kExit } action;
+  std::optional<StringPool::Id> pathname;
+  std::optional<uint32_t> line_number;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_EVENT_H_
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.cc b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
new file mode 100644
index 0000000..f80d2f4
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.cc
@@ -0,0 +1,62 @@
+/*
+ * 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/art_method/art_method_parser_impl.h"
+
+#include <cstdint>
+
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/args_tracker.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"
+#include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/types/variadic.h"
+
+namespace perfetto::trace_processor::art_method {
+
+ArtMethodParserImpl::ArtMethodParserImpl(TraceProcessorContext* context)
+    : context_(context),
+      pathname_id_(context->storage->InternString("pathname")),
+      line_number_id_(context->storage->InternString("line_number")) {}
+
+ArtMethodParserImpl::~ArtMethodParserImpl() = default;
+
+void ArtMethodParserImpl::ParseArtMethodEvent(int64_t ts, ArtMethodEvent e) {
+  UniqueTid utid = context_->process_tracker->GetOrCreateThread(e.tid);
+  TrackId track_id = context_->track_tracker->InternThreadTrack(utid);
+  switch (e.action) {
+    case ArtMethodEvent::kEnter:
+      context_->slice_tracker->Begin(
+          ts, track_id, kNullStringId, e.method,
+          [this, &e](ArgsTracker::BoundInserter* i) {
+            if (e.pathname) {
+              i->AddArg(pathname_id_, Variadic::String(*e.pathname));
+            }
+            if (e.line_number) {
+              i->AddArg(line_number_id_, Variadic::Integer(*e.line_number));
+            }
+          });
+      break;
+    case ArtMethodEvent::kExit:
+      context_->slice_tracker->End(ts, track_id);
+      break;
+  }
+}
+
+}  // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_parser_impl.h b/src/trace_processor/importers/art_method/art_method_parser_impl.h
new file mode 100644
index 0000000..09759cd
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_parser_impl.h
@@ -0,0 +1,45 @@
+/*
+ * 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_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
+
+#include <cstdint>
+
+#include "src/trace_processor/containers/string_pool.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/trace_parser.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodParserImpl : public ArtMethodParser {
+ public:
+  explicit ArtMethodParserImpl(TraceProcessorContext*);
+  ~ArtMethodParserImpl() override;
+
+  void ParseArtMethodEvent(int64_t ts, ArtMethodEvent) override;
+
+ private:
+  TraceProcessorContext* const context_;
+
+  StringPool::Id pathname_id_;
+  StringPool::Id line_number_id_;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_PARSER_IMPL_H_
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.cc b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
new file mode 100644
index 0000000..fd32c78
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.cc
@@ -0,0 +1,358 @@
+/*
+ * 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/art_method/art_method_tokenizer.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <cstring>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+
+#include "perfetto/base/compiler.h"
+#include "perfetto/base/logging.h"
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/status_or.h"
+#include "perfetto/ext/base/string_utils.h"
+#include "perfetto/ext/base/string_view.h"
+#include "perfetto/ext/base/utils.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/art_method/art_method_event.h"
+#include "src/trace_processor/importers/common/stack_profile_tracker.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/status_macros.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+#include "protos/perfetto/common/builtin_clock.pbzero.h"
+
+namespace perfetto::trace_processor::art_method {
+namespace {
+
+constexpr uint32_t kTraceMagic = 0x574f4c53;  // 'SLOW'
+
+std::string_view ReadLine(util::TraceBlobViewReader& reader,
+                          util::TraceBlobViewReader::Iterator& it) {
+  size_t begin = it.file_offset();
+  if (!it.MaybeFindAndAdvance('\n')) {
+    return {};
+  }
+  auto x = reader.SliceOff(begin, it.file_offset() - begin);
+  std::string_view str(reinterpret_cast<const char*>(x->data()), x->size());
+  PERFETTO_CHECK(it.MaybeAdvance(1));
+  return str;
+}
+
+std::string ConstructPathname(const std::string& class_name,
+                              const std::string& pathname) {
+  size_t index = class_name.rfind('/');
+  if (index != std::string::npos && base::EndsWith(pathname, ".java")) {
+    return class_name.substr(0, index + 1) + pathname;
+  }
+  return pathname;
+}
+
+uint64_t ToLong(const TraceBlobView& tbv) {
+  uint64_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+uint32_t ToInt(const TraceBlobView& tbv) {
+  uint32_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+uint16_t ToShort(const TraceBlobView& tbv) {
+  uint16_t x = 0;
+  memcpy(base::AssumeLittleEndian(&x), tbv.data(), tbv.size());
+  return x;
+}
+
+}  // namespace
+
+ArtMethodTokenizer::ArtMethodTokenizer(TraceProcessorContext* ctx)
+    : context_(ctx) {}
+ArtMethodTokenizer::~ArtMethodTokenizer() = default;
+
+base::Status ArtMethodTokenizer::Parse(TraceBlobView blob) {
+  reader_.PushBack(std::move(blob));
+  auto it = reader_.GetIterator();
+  for (bool cnt = true; cnt;) {
+    switch (mode_) {
+      case kHeaderDetection: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderDetection(it));
+        break;
+      }
+      case kHeaderVersion: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderVersion(it));
+        break;
+      }
+      case kHeaderOptions: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderOptions(it));
+        break;
+      }
+      case kHeaderThreads: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderThreads(it));
+        break;
+      }
+      case kHeaderMethods: {
+        ASSIGN_OR_RETURN(cnt, ParseHeaderMethods(it));
+        break;
+      }
+      case kDataHeader: {
+        ASSIGN_OR_RETURN(cnt, ParseDataHeader(it));
+        break;
+      }
+      case kData: {
+        size_t s = it.file_offset();
+        for (size_t i = s;; i += record_size_) {
+          auto record = reader_.SliceOff(i, record_size_);
+          if (!record) {
+            PERFETTO_CHECK(it.MaybeAdvance(i - s));
+            cnt = false;
+            break;
+          }
+
+          ArtMethodEvent evt{};
+          evt.tid = version_ == 1 ? record->data()[0]
+                                  : ToShort(record->slice_off(0, 2));
+          uint32_t methodid_action = ToInt(record->slice_off(2, 4));
+          uint32_t ts_delta = clock_ == kDual ? ToInt(record->slice_off(10, 4))
+                                              : ToInt(record->slice_off(6, 4));
+
+          uint32_t action = methodid_action & 0x03;
+          uint32_t method_id = methodid_action & ~0x03u;
+
+          const auto& m = method_map_[method_id];
+          evt.method = m.name;
+          evt.pathname = m.pathname;
+          evt.line_number = m.line_number;
+          switch (action) {
+            case 0:
+              evt.action = ArtMethodEvent::kEnter;
+              break;
+            case 1:
+            case 2:
+              evt.action = ArtMethodEvent::kExit;
+              break;
+          }
+          ASSIGN_OR_RETURN(int64_t ts,
+                           context_->clock_tracker->ToTraceTime(
+                               protos::pbzero::BUILTIN_CLOCK_MONOTONIC,
+                               (ts_ + ts_delta) * 1000));
+          context_->sorter->PushArtMethodEvent(ts, evt);
+        }
+        break;
+      }
+    }
+  }
+  reader_.PopFrontUntil(it.file_offset());
+  return base::OkStatus();
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderDetection(Iterator& it) {
+  auto smagic = reader_.SliceOff(it.file_offset(), 4);
+  if (!smagic) {
+    return false;
+  }
+  uint32_t magic = ToInt(*smagic);
+  if (magic == kTraceMagic) {
+    return base::ErrStatus(
+        "ART Method trace is in streaming format: this is not supported");
+  }
+  auto line = ReadLine(reader_, it);
+  if (line.empty()) {
+    return false;
+  }
+  context_->clock_tracker->SetTraceTimeClock(
+      protos::pbzero::BUILTIN_CLOCK_MONOTONIC);
+  RETURN_IF_ERROR(ParseHeaderSectionLine(line));
+  return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderVersion(Iterator& it) {
+  auto version_str = ReadLine(reader_, it);
+  if (version_str.empty()) {
+    return false;
+  }
+  auto version = base::StringToInt32(std::string(version_str));
+  if (!version || *version < 1 || *version > 3) {
+    return base::ErrStatus("ART Method trace: trace version (%s) not supported",
+                           std::string(version_str).c_str());
+  }
+  version_ = static_cast<uint32_t>(*version);
+  mode_ = kHeaderOptions;
+  return true;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderOptions(Iterator& it) {
+  for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+    auto res = base::SplitString(std::string(l), "=");
+    if (res.size() != 2) {
+      return base::ErrStatus("ART method tracing: unable to parse option");
+    }
+    if (res[0] == "clock") {
+      if (res[1] == "dual") {
+        clock_ = kDual;
+      } else if (res[1] == "wall") {
+        clock_ = kWall;
+      } else if (res[1] == "thread-cpu") {
+        return base::ErrStatus(
+            "ART method tracing: thread-cpu clock is *not* supported. Use wall "
+            "or dual clocks");
+      } else {
+        return base::ErrStatus("ART method tracing: unknown clock %s",
+                               res[1].c_str());
+      }
+    }
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderThreads(Iterator& it) {
+  for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseHeaderMethods(Iterator& it) {
+  for (auto l = ReadLine(reader_, it); !l.empty(); l = ReadLine(reader_, it)) {
+    if (l[0] == '*') {
+      RETURN_IF_ERROR(ParseHeaderSectionLine(l));
+      return true;
+    }
+    auto tokens = base::SplitString(std::string(l), "\t");
+    auto id = base::StringToUInt32(tokens[0], 16);
+    if (!id) {
+      return base::ErrStatus(
+          "ART method trace: unable to parse method id as integer: %s",
+          tokens[0].c_str());
+    }
+
+    std::string class_name = tokens[1];
+    std::string method_name;
+    std::string signature;
+    std::optional<StringId> pathname;
+    std::optional<uint32_t> line_number;
+    if (tokens.size() == 6) {
+      method_name = tokens[2];
+      signature = tokens[3];
+      pathname = context_->storage->InternString(
+          base::StringView(ConstructPathname(class_name, tokens[4])));
+      line_number = base::StringToUInt32(tokens[5]);
+    } else if (tokens.size() > 2) {
+      if (base::StartsWith(tokens[3], "(")) {
+        method_name = tokens[2];
+        signature = tokens[3];
+        if (tokens.size() >= 5) {
+          pathname =
+              context_->storage->InternString(base::StringView(tokens[4]));
+        }
+      } else {
+        pathname = context_->storage->InternString(base::StringView(tokens[2]));
+        line_number = base::StringToUInt32(tokens[3]);
+      }
+    }
+    base::StackString<2048> slice_name("%s.%s: %s", class_name.c_str(),
+                                       method_name.c_str(), signature.c_str());
+    method_map_[*id] = {
+        context_->storage->InternString(slice_name.string_view()),
+        pathname,
+        line_number,
+    };
+  }
+  return false;
+}
+
+base::StatusOr<bool> ArtMethodTokenizer::ParseDataHeader(Iterator& it) {
+  size_t begin = it.file_offset();
+  if (!it.MaybeAdvance(32)) {
+    return false;
+  }
+  auto header = reader_.SliceOff(begin, it.file_offset() - begin);
+  uint32_t magic = ToInt(header->slice_off(0, 4));
+  if (magic != kTraceMagic) {
+    return base::ErrStatus("ART Method trace: expected pre-data magic");
+  }
+  uint16_t version = ToShort(header->slice_off(4, 2));
+  if (version != version_) {
+    return base::ErrStatus(
+        "ART Method trace: trace version does not match data version");
+  }
+  ts_ = static_cast<int64_t>(ToLong(header->slice_off(8, 8)));
+  switch (version_) {
+    case 1:
+      record_size_ = 9;
+      break;
+    case 2:
+      record_size_ = 10;
+      break;
+    case 3:
+      record_size_ = ToShort(header->slice_off(16, 2));
+      break;
+    default:
+      PERFETTO_FATAL("Illegal version %u", version_);
+  }
+  mode_ = kData;
+  return true;
+}
+
+base::Status ArtMethodTokenizer::ParseHeaderSectionLine(std::string_view line) {
+  if (line == "*version") {
+    mode_ = kHeaderVersion;
+    return base::OkStatus();
+  }
+  if (line == "*threads") {
+    mode_ = kHeaderThreads;
+    return base::OkStatus();
+  }
+  if (line == "*methods") {
+    mode_ = kHeaderMethods;
+    return base::OkStatus();
+  }
+  if (line == "*end") {
+    mode_ = kDataHeader;
+    return base::OkStatus();
+  }
+  return base::ErrStatus(
+      "ART Method trace: unexpected line (%s) when expecting section header "
+      "(line starting with *)",
+      std::string(line).c_str());
+}
+
+base::Status ArtMethodTokenizer::NotifyEndOfFile() {
+  // DNS: also add a check here for whether our state machine reached the end
+  // too.
+  if (!reader_.empty() || mode_ != kData) {
+    return base::ErrStatus("ART Method trace: trace is incomplete");
+  }
+  return base::OkStatus();
+}
+
+}  // namespace perfetto::trace_processor::art_method
diff --git a/src/trace_processor/importers/art_method/art_method_tokenizer.h b/src/trace_processor/importers/art_method/art_method_tokenizer.h
new file mode 100644
index 0000000..91a0eed
--- /dev/null
+++ b/src/trace_processor/importers/art_method/art_method_tokenizer.h
@@ -0,0 +1,84 @@
+/*
+ * 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_ART_METHOD_ART_METHOD_TOKENIZER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
+
+#include <cstdint>
+#include <limits>
+#include <optional>
+#include <string_view>
+
+#include "perfetto/base/status.h"
+#include "perfetto/ext/base/flat_hash_map.h"
+#include "perfetto/ext/base/status_or.h"
+#include "src/trace_processor/importers/common/chunked_trace_reader.h"
+#include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+#include "src/trace_processor/util/trace_blob_view_reader.h"
+
+namespace perfetto::trace_processor::art_method {
+
+class ArtMethodTokenizer : public ChunkedTraceReader {
+ public:
+  explicit ArtMethodTokenizer(TraceProcessorContext*);
+  ~ArtMethodTokenizer() override;
+
+  base::Status Parse(TraceBlobView) override;
+  base::Status NotifyEndOfFile() override;
+
+ private:
+  using Iterator = util::TraceBlobViewReader::Iterator;
+  struct Method {
+    StringId name;
+    std::optional<StringId> pathname;
+    std::optional<uint32_t> line_number;
+  };
+
+  base::StatusOr<bool> ParseHeaderDetection(Iterator&);
+  base::StatusOr<bool> ParseHeaderVersion(Iterator&);
+  base::StatusOr<bool> ParseHeaderOptions(Iterator&);
+  base::StatusOr<bool> ParseHeaderThreads(Iterator&);
+  base::StatusOr<bool> ParseHeaderMethods(Iterator&);
+  base::StatusOr<bool> ParseDataHeader(Iterator&);
+
+  base::Status ParseHeaderSectionLine(std::string_view);
+
+  TraceProcessorContext* const context_;
+  util::TraceBlobViewReader reader_;
+  enum {
+    kHeaderDetection,
+    kHeaderVersion,
+    kHeaderOptions,
+    kHeaderThreads,
+    kHeaderMethods,
+    kDataHeader,
+    kData,
+  } mode_ = kHeaderDetection;
+  enum {
+    kWall,
+    kDual,
+  } clock_ = kWall;
+
+  uint32_t version_ = std::numeric_limits<uint32_t>::max();
+  int64_t ts_ = std::numeric_limits<int64_t>::max();
+  uint32_t record_size_ = std::numeric_limits<uint32_t>::max();
+  base::FlatHashMap<uint32_t, Method> method_map_;
+};
+
+}  // namespace perfetto::trace_processor::art_method
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_ART_METHOD_ART_METHOD_TOKENIZER_H_
diff --git a/src/trace_processor/importers/common/trace_parser.cc b/src/trace_processor/importers/common/trace_parser.cc
index 01345cc..12f3589 100644
--- a/src/trace_processor/importers/common/trace_parser.cc
+++ b/src/trace_processor/importers/common/trace_parser.cc
@@ -27,6 +27,7 @@
 ProtoTraceParser::~ProtoTraceParser() = default;
 SpeRecordParser::~SpeRecordParser() = default;
 GeckoTraceParser::~GeckoTraceParser() = default;
+ArtMethodParser::~ArtMethodParser() = default;
 
 }  // namespace trace_processor
 }  // namespace perfetto
diff --git a/src/trace_processor/importers/common/trace_parser.h b/src/trace_processor/importers/common/trace_parser.h
index 34475d6..735d27a 100644
--- a/src/trace_processor/importers/common/trace_parser.h
+++ b/src/trace_processor/importers/common/trace_parser.h
@@ -30,6 +30,9 @@
 namespace gecko_importer {
 struct GeckoEvent;
 }
+namespace art_method {
+struct ArtMethodEvent;
+}
 
 struct AndroidLogEvent;
 class PacketSequenceStateGeneration;
@@ -97,6 +100,12 @@
   virtual void ParseGeckoEvent(int64_t, gecko_importer::GeckoEvent) = 0;
 };
 
+class ArtMethodParser {
+ public:
+  virtual ~ArtMethodParser();
+  virtual void ParseArtMethodEvent(int64_t, art_method::ArtMethodEvent) = 0;
+};
+
 }  // namespace perfetto::trace_processor
 
 #endif  // SRC_TRACE_PROCESSOR_IMPORTERS_COMMON_TRACE_PARSER_H_
diff --git a/src/trace_processor/importers/perf/spe_tokenizer.cc b/src/trace_processor/importers/perf/spe_tokenizer.cc
index 8186bbb..6c637b6 100644
--- a/src/trace_processor/importers/perf/spe_tokenizer.cc
+++ b/src/trace_processor/importers/perf/spe_tokenizer.cc
@@ -20,6 +20,7 @@
 #include <cstring>
 #include <memory>
 #include <optional>
+#include <utility>
 
 #include "perfetto/base/logging.h"
 #include "perfetto/base/status.h"
@@ -56,7 +57,7 @@
 }
 
 bool SpeTokenizer::ProcessRecord() {
-  for (auto it = buffer_.begin(); it;) {
+  for (auto it = buffer_.GetIterator(); it;) {
     uint8_t byte_0 = *it;
     // Must be true (we passed the for loop condition).
     it.MaybeAdvance(1);
diff --git a/src/trace_processor/rpc/rpc.cc b/src/trace_processor/rpc/rpc.cc
index 1f56688..88b7e82 100644
--- a/src/trace_processor/rpc/rpc.cc
+++ b/src/trace_processor/rpc/rpc.cc
@@ -324,10 +324,10 @@
       resp.Send(rpc_response_fn_);
       break;
     }
-    case RpcProto::TPM_REGISTER_SQL_MODULE: {
+    case RpcProto::TPM_REGISTER_SQL_PACKAGE: {
       Response resp(tx_seq_id_++, req_type);
-      base::Status status = RegisterSqlModule(req.register_sql_module_args());
-      auto* res = resp->set_register_sql_module_result();
+      base::Status status = RegisterSqlPackage(req.register_sql_package_args());
+      auto* res = resp->set_register_sql_package_result();
       if (!status.ok()) {
         res->set_error(status.message());
       }
@@ -410,16 +410,16 @@
   ResetTraceProcessorInternal(config);
 }
 
-base::Status Rpc::RegisterSqlModule(protozero::ConstBytes bytes) {
-  protos::pbzero::RegisterSqlModuleArgs::Decoder args(bytes);
-  SqlModule package;
-  package.name = args.top_level_package_name().ToStdString();
-  package.allow_module_override = args.allow_module_override();
+base::Status Rpc::RegisterSqlPackage(protozero::ConstBytes bytes) {
+  protos::pbzero::RegisterSqlPackageArgs::Decoder args(bytes);
+  SqlPackage package;
+  package.name = args.package_name().ToStdString();
+  package.allow_override = args.allow_override();
   for (auto it = args.modules(); it; ++it) {
-    protos::pbzero::RegisterSqlModuleArgs::Module::Decoder m(*it);
-    package.files.emplace_back(m.name().ToStdString(), m.sql().ToStdString());
+    protos::pbzero::RegisterSqlPackageArgs::Module::Decoder m(*it);
+    package.modules.emplace_back(m.name().ToStdString(), m.sql().ToStdString());
   }
-  return trace_processor_->RegisterSqlModule(package);
+  return trace_processor_->RegisterSqlPackage(package);
 }
 
 void Rpc::MaybePrintProgress() {
diff --git a/src/trace_processor/rpc/rpc.h b/src/trace_processor/rpc/rpc.h
index 96bbfbc..fe28748 100644
--- a/src/trace_processor/rpc/rpc.h
+++ b/src/trace_processor/rpc/rpc.h
@@ -131,7 +131,7 @@
  private:
   void ParseRpcRequest(const uint8_t*, size_t);
   void ResetTraceProcessor(const uint8_t*, size_t);
-  base::Status RegisterSqlModule(protozero::ConstBytes);
+  base::Status RegisterSqlPackage(protozero::ConstBytes);
   void ResetTraceProcessorInternal(const Config&);
   void MaybePrintProgress();
   Iterator QueryInternal(const uint8_t*, size_t);
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index eb2da54..327d80b 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -30,6 +30,7 @@
     "../../../include/perfetto/trace_processor:storage",
     "../../base",
     "../importers/android_bugreport:android_log_event",
+    "../importers/art_method:art_method_event",
     "../importers/common:parser_types",
     "../importers/common:trace_parser_hdr",
     "../importers/fuchsia:fuchsia_record",
diff --git a/src/trace_processor/sorter/trace_sorter.cc b/src/trace_processor/sorter/trace_sorter.cc
index e2fc178..0ee3ef7 100644
--- a/src/trace_processor/sorter/trace_sorter.cc
+++ b/src/trace_processor/sorter/trace_sorter.cc
@@ -28,6 +28,7 @@
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.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"
@@ -267,6 +268,10 @@
       context.gecko_trace_parser->ParseGeckoEvent(
           event.ts, token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
       return;
+    case TimestampedEvent::Type::kArtMethodEvent:
+      context.art_method_parser->ParseArtMethodEvent(
+          event.ts, token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+      return;
     case TimestampedEvent::Type::kInlineSchedSwitch:
     case TimestampedEvent::Type::kInlineSchedWaking:
     case TimestampedEvent::Type::kEtwEvent:
@@ -300,6 +305,7 @@
     case TimestampedEvent::Type::kAndroidLogEvent:
     case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
     case TimestampedEvent::Type::kGeckoEvent:
+    case TimestampedEvent::Type::kArtMethodEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -335,6 +341,7 @@
     case TimestampedEvent::Type::kAndroidLogEvent:
     case TimestampedEvent::Type::kLegacyV8CpuProfileEvent:
     case TimestampedEvent::Type::kGeckoEvent:
+    case TimestampedEvent::Type::kArtMethodEvent:
       PERFETTO_FATAL("Invalid event type");
   }
   PERFETTO_FATAL("For GCC");
@@ -389,6 +396,10 @@
       base::ignore_result(
           token_buffer_.Extract<gecko_importer::GeckoEvent>(id));
       return;
+    case TimestampedEvent::Type::kArtMethodEvent:
+      base::ignore_result(
+          token_buffer_.Extract<art_method::ArtMethodEvent>(id));
+      return;
   }
   PERFETTO_FATAL("For GCC");
 }
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index 1ff3d18..4cb7951 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -35,6 +35,7 @@
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event.h"
+#include "src/trace_processor/importers/art_method/art_method_event.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"
@@ -247,11 +248,18 @@
   }
 
   inline void PushGeckoEvent(int64_t timestamp,
-                             gecko_importer::GeckoEvent event) {
-    TraceTokenBuffer::Id id = token_buffer_.Append(std::move(event));
+                             const gecko_importer::GeckoEvent& event) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(event);
     AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kGeckoEvent, id);
   }
 
+  inline void PushArtMethodEvent(int64_t timestamp,
+                                 const art_method::ArtMethodEvent& event) {
+    TraceTokenBuffer::Id id = token_buffer_.Append(event);
+    AppendNonFtraceEvent(timestamp, TimestampedEvent::Type::kArtMethodEvent,
+                         id);
+  }
+
   inline void PushInlineFtraceEvent(
       uint32_t cpu,
       int64_t timestamp,
@@ -333,11 +341,12 @@
       kTracePacket,
       kTrackEvent,
       kGeckoEvent,
+      kArtMethodEvent,
       kMax = kGeckoEvent,
     };
 
     // Number of bits required to store the max element in |Type|.
-    static constexpr uint32_t kMaxTypeBits = 4;
+    static constexpr uint32_t kMaxTypeBits = 6;
     static_assert(static_cast<uint8_t>(Type::kMax) <= (1 << kMaxTypeBits),
                   "Max type does not fit inside storage");
 
diff --git a/src/trace_processor/trace_database_integrationtest.cc b/src/trace_processor/trace_database_integrationtest.cc
index 52852cd..d2d53f3 100644
--- a/src/trace_processor/trace_database_integrationtest.cc
+++ b/src/trace_processor/trace_database_integrationtest.cc
@@ -767,11 +767,11 @@
 }
 
 TEST_F(TraceProcessorIntegrationTest, ErrorMessageModule) {
-  SqlModule module;
+  SqlPackage module;
   module.name = "foo";
-  module.files.push_back(std::make_pair("foo.bar", "select t from slice"));
+  module.modules.push_back(std::make_pair("foo.bar", "select t from slice"));
 
-  ASSERT_TRUE(Processor()->RegisterSqlModule(module).ok());
+  ASSERT_TRUE(Processor()->RegisterSqlPackage(module).ok());
 
   auto it = Query("include perfetto module foo.bar;");
   ASSERT_FALSE(it.Next());
diff --git a/src/trace_processor/trace_processor_impl.cc b/src/trace_processor/trace_processor_impl.cc
index 5a60433..b7f013c 100644
--- a/src/trace_processor/trace_processor_impl.cc
+++ b/src/trace_processor/trace_processor_impl.cc
@@ -46,6 +46,8 @@
 #include "perfetto/trace_processor/trace_processor.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_event_parser_impl.h"
 #include "src/trace_processor/importers/android_bugreport/android_log_reader.h"
+#include "src/trace_processor/importers/art_method/art_method_parser_impl.h"
+#include "src/trace_processor/importers/art_method/art_method_tokenizer.h"
 #include "src/trace_processor/importers/common/clock_tracker.h"
 #include "src/trace_processor/importers/common/trace_file_tracker.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
@@ -442,6 +444,11 @@
 #endif
   }
 
+  context_.reader_registry->RegisterTraceReader<art_method::ArtMethodTokenizer>(
+      kArtMethodTraceType);
+  context_.art_method_parser =
+      std::make_unique<art_method::ArtMethodParserImpl>(&context_);
+
   if (context_.config.analyze_trace_proto_content) {
     context_.content_analyzer =
         std::make_unique<ProtoContentAnalyzer>(&context_);
@@ -586,10 +593,10 @@
   return field_idx != nullptr;
 }
 
-base::Status TraceProcessorImpl::RegisterSqlModule(SqlModule sql_package) {
+base::Status TraceProcessorImpl::RegisterSqlPackage(SqlPackage sql_package) {
   sql_modules::RegisteredPackage new_package;
   std::string name = sql_package.name;
-  if (engine_->FindPackage(name) && !sql_package.allow_module_override) {
+  if (engine_->FindPackage(name) && !sql_package.allow_override) {
     return base::ErrStatus(
         "Package '%s' is already registered. Choose a different name.\n"
         "If you want to replace the existing package using trace processor "
@@ -598,7 +605,7 @@
         "to pass the module path.",
         name.c_str());
   }
-  for (auto const& module_name_and_sql : sql_package.files) {
+  for (auto const& module_name_and_sql : sql_package.modules) {
     if (sql_modules::GetPackageName(module_name_and_sql.first) != name) {
       return base::ErrStatus(
           "Module name doesn't match the package name. First part of module "
@@ -608,7 +615,7 @@
     new_package.modules.Insert(module_name_and_sql.first,
                                {module_name_and_sql.second, false});
   }
-  manually_registered_sql_packages_.push_back(SqlModule(sql_package));
+  manually_registered_sql_packages_.push_back(SqlPackage(sql_package));
   engine_->RegisterPackage(name, std::move(new_package));
   return base::OkStatus();
 }
@@ -861,8 +868,8 @@
   auto packages = GetStdlibPackages();
   for (auto package = packages.GetIterator(); package; ++package) {
     base::Status status =
-        RegisterSqlModule({/*name=*/package.key(), /*modules=*/package.value(),
-                           /*allow_package_override=*/false});
+        RegisterSqlPackage({/*name=*/package.key(), /*modules=*/package.value(),
+                            /*allow_override=*/false});
     if (!status.ok())
       PERFETTO_ELOG("%s", status.c_message());
   }
@@ -1088,7 +1095,7 @@
 
   // Reregister manually added stdlib packages.
   for (const auto& package : manually_registered_sql_packages_) {
-    RegisterSqlModule(package);
+    RegisterSqlPackage(package);
   }
 }
 
diff --git a/src/trace_processor/trace_processor_impl.h b/src/trace_processor/trace_processor_impl.h
index 473eefd..8f95290 100644
--- a/src/trace_processor/trace_processor_impl.h
+++ b/src/trace_processor/trace_processor_impl.h
@@ -67,7 +67,7 @@
   base::Status RegisterMetric(const std::string& path,
                               const std::string& sql) override;
 
-  base::Status RegisterSqlModule(SqlModule) override;
+  base::Status RegisterSqlPackage(SqlPackage) override;
 
   base::Status ExtendMetricsProto(const uint8_t* data, size_t size) override;
 
@@ -97,6 +97,15 @@
   base::Status DisableAndReadMetatrace(
       std::vector<uint8_t>* trace_proto) override;
 
+  base::Status RegisterSqlModule(SqlModule module) override {
+    SqlPackage package;
+    package.name = std::move(module.name);
+    package.modules = std::move(module.files);
+    package.allow_override = module.allow_module_override;
+
+    return RegisterSqlPackage(package);
+  }
+
  private:
   // Needed for iterators to be able to access the context.
   friend class IteratorImpl;
@@ -120,7 +129,7 @@
 
   // Manually registeres SQL packages are stored here, to be able to restore
   // them when running |RestoreInitialTables()|.
-  std::vector<SqlModule> manually_registered_sql_packages_;
+  std::vector<SqlPackage> manually_registered_sql_packages_;
 
   std::unordered_map<std::string, std::string> proto_field_to_sql_metric_path_;
   std::unordered_map<std::string, std::string> proto_fn_name_to_path_;
diff --git a/src/trace_processor/trace_processor_shell.cc b/src/trace_processor/trace_processor_shell.cc
index 77f0f53..42fc979 100644
--- a/src/trace_processor/trace_processor_shell.cc
+++ b/src/trace_processor/trace_processor_shell.cc
@@ -1256,9 +1256,9 @@
         .first->push_back({import_key, file_contents});
   }
   for (auto module_it = modules.GetIterator(); module_it; ++module_it) {
-    auto status = g_tp->RegisterSqlModule(
-        {/*name=*/module_it.key(), /*files=*/module_it.value(),
-         /*allow_module_override=*/allow_override});
+    auto status = g_tp->RegisterSqlPackage({/*name=*/module_it.key(),
+                                            /*files=*/module_it.value(),
+                                            /*allow_override=*/allow_override});
     if (!status.ok())
       return status;
   }
@@ -1294,9 +1294,9 @@
         .first->push_back({module_name, module_file});
   }
   for (auto package = packages.GetIterator(); package; ++package) {
-    g_tp->RegisterSqlModule({/*name=*/package.key(),
-                             /*files=*/package.value(),
-                             /*allow_module_override=*/true});
+    g_tp->RegisterSqlPackage({/*name=*/package.key(),
+                              /*files=*/package.value(),
+                              /*allow_override=*/true});
   }
 
   return base::OkStatus();
diff --git a/src/trace_processor/trace_reader_registry.cc b/src/trace_processor/trace_reader_registry.cc
index 4b1b6c3..30f5205 100644
--- a/src/trace_processor/trace_reader_registry.cc
+++ b/src/trace_processor/trace_reader_registry.cc
@@ -52,6 +52,7 @@
     case kAndroidLogcatTraceType:
     case kAndroidDumpstateTraceType:
     case kGeckoTraceType:
+    case kArtMethodTraceType:
       return false;
   }
   PERFETTO_FATAL("For GCC");
diff --git a/src/trace_processor/types/trace_processor_context.h b/src/trace_processor/types/trace_processor_context.h
index 84682eb..7a84bb9 100644
--- a/src/trace_processor/types/trace_processor_context.h
+++ b/src/trace_processor/types/trace_processor_context.h
@@ -31,6 +31,7 @@
 class AndroidLogEventParser;
 class ArgsTracker;
 class ArgsTranslationTable;
+class ArtMethodParser;
 class AsyncTrackSetTracker;
 class ChunkedTraceReader;
 class ClockConverter;
@@ -175,6 +176,7 @@
   std::unique_ptr<InstrumentsRowParser> instruments_row_parser;
   std::unique_ptr<AndroidLogEventParser> android_log_event_parser;
   std::unique_ptr<GeckoTraceParser> gecko_trace_parser;
+  std::unique_ptr<ArtMethodParser> art_method_parser;
 
   // This field contains the list of proto descriptors that can be used by
   // reflection-based parsers.
diff --git a/src/trace_processor/util/bump_allocator.h b/src/trace_processor/util/bump_allocator.h
index 985b5bc..9e92471 100644
--- a/src/trace_processor/util/bump_allocator.h
+++ b/src/trace_processor/util/bump_allocator.h
@@ -54,7 +54,7 @@
  public:
   // The limit on the total number of bits which can be used to represent
   // the chunk id.
-  static constexpr uint64_t kMaxIdBits = 60;
+  static constexpr uint64_t kMaxIdBits = 58;
 
   // The limit on the total amount of memory which can be allocated.
   static constexpr uint64_t kAllocLimit = 1ull << kMaxIdBits;
diff --git a/src/trace_processor/util/trace_blob_view_reader.h b/src/trace_processor/util/trace_blob_view_reader.h
index 69e5aa3..158881f 100644
--- a/src/trace_processor/util/trace_blob_view_reader.h
+++ b/src/trace_processor/util/trace_blob_view_reader.h
@@ -20,6 +20,7 @@
 #include <cstddef>
 #include <cstdint>
 #include <cstdio>
+#include <cstring>
 #include <optional>
 
 #include "perfetto/base/logging.h"
@@ -46,12 +47,56 @@
  public:
   class Iterator {
    public:
-    Iterator(const Iterator&) = default;
+    ~Iterator() = default;
+
+    Iterator(const Iterator&) = delete;
+    Iterator& operator=(const Iterator&) = delete;
+
     Iterator(Iterator&&) = default;
-    Iterator& operator=(const Iterator&) = default;
     Iterator& operator=(Iterator&&) = default;
 
-    ~Iterator() = default;
+    // Tries to advance the iterator |size| bytes forward. Returns true if
+    // the advance was successful and false if it would overflow the iterator.
+    // If false is returned, the state of the iterator is not changed.
+    bool MaybeAdvance(size_t delta) {
+      file_offset_ += delta;
+      if (PERFETTO_LIKELY(file_offset_ < iter_->end_offset())) {
+        return true;
+      }
+      if (file_offset_ == end_offset_) {
+        return true;
+      }
+      if (file_offset_ > end_offset_) {
+        file_offset_ -= delta;
+        return false;
+      }
+      do {
+        ++iter_;
+      } while (file_offset_ >= iter_->end_offset());
+      return true;
+    }
+
+    // Tries to find a byte equal to |chr| in the iterator and, if found,
+    // advance to it. Returns true if the byte was found and could be advanced
+    // to and false if no such byte was found before the end of the iterator. If
+    // false is returned, the state of the iterator is not changed.
+    bool MaybeFindAndAdvance(uint8_t chr) {
+      size_t off = file_offset_;
+      while (off < end_offset_) {
+        size_t iter_off = off - iter_->start_offset;
+        size_t iter_rem = iter_->data.size() - iter_off;
+        const auto* p = reinterpret_cast<const uint8_t*>(
+            memchr(iter_->data.data() + iter_off, chr, iter_rem));
+        if (p) {
+          file_offset_ =
+              iter_->start_offset + static_cast<size_t>(p - iter_->data.data());
+          return true;
+        }
+        off = iter_->end_offset();
+        ++iter_;
+      }
+      return false;
+    }
 
     uint8_t operator*() const {
       PERFETTO_DCHECK(file_offset_ < iter_->end_offset());
@@ -62,45 +107,20 @@
 
     size_t file_offset() const { return file_offset_; }
 
-    bool MaybeAdvance(size_t delta) {
-      if (delta == 0) {
-        return true;
-      }
-      if (delta > end_offset_ - file_offset_) {
-        return false;
-      }
-      file_offset_ += delta;
-      if (PERFETTO_LIKELY(file_offset_ < iter_->end_offset())) {
-        return true;
-      }
-      while (file_offset_ > iter_->end_offset()) {
-        ++iter_;
-      }
-      if (file_offset_ == iter_->end_offset()) {
-        ++iter_;
-      }
-
-      return true;
-    }
-
    private:
     friend TraceBlobViewReader;
     Iterator(base::CircularQueue<Entry>::Iterator iter,
              size_t file_offset,
              size_t end_offset)
-        : iter_(std::move(iter)),
-          file_offset_(file_offset),
-          end_offset_(end_offset) {}
+        : iter_(iter), file_offset_(file_offset), end_offset_(end_offset) {}
+
     base::CircularQueue<Entry>::Iterator iter_;
     size_t file_offset_;
     size_t end_offset_;
   };
 
-  Iterator begin() const {
-    return Iterator(data_.begin(), start_offset(), end_offset());
-  }
-  Iterator end() const {
-    return Iterator(data_.end(), end_offset(), end_offset());
+  Iterator GetIterator() const {
+    return {data_.begin(), start_offset(), end_offset()};
   }
 
   // Adds a `TraceBlobView` at the back.
@@ -129,6 +149,7 @@
   //
   // NOTE: If `offset` < 'file_offset()' this method will CHECK fail.
   std::optional<TraceBlobView> SliceOff(size_t offset, size_t length) const;
+
   // Returns the offset to the start of the available data.
   size_t start_offset() const {
     return data_.empty() ? end_offset_ : data_.front().start_offset;
diff --git a/src/trace_processor/util/trace_type.cc b/src/trace_processor/util/trace_type.cc
index 62f586f..e5690bb 100644
--- a/src/trace_processor/util/trace_type.cc
+++ b/src/trace_processor/util/trace_type.cc
@@ -37,10 +37,9 @@
 constexpr char kFuchsiaMagic[] = {'\x10', '\x00', '\x04', '\x46',
                                   '\x78', '\x54', '\x16', '\x00'};
 constexpr char kPerfMagic[] = {'P', 'E', 'R', 'F', 'I', 'L', 'E', '2'};
-
 constexpr char kZipMagic[] = {'P', 'K', '\x03', '\x04'};
-
 constexpr char kGzipMagic[] = {'\x1f', '\x8b'};
+constexpr char kArtMethodStreamingMagic[] = {'S', 'L', 'O', 'W'};
 
 constexpr uint8_t kTracePacketTag =
     protozero::proto_utils::MakeTagLengthDelimited(
@@ -130,6 +129,8 @@
       return "android_bugreport";
     case kGeckoTraceType:
       return "gecko";
+    case kArtMethodTraceType:
+      return "art_method";
     case kUnknownTraceType:
       return "unknown";
   }
@@ -157,6 +158,10 @@
     return kGzipTraceType;
   }
 
+  if (MatchesMagic(data, size, kArtMethodStreamingMagic)) {
+    return kArtMethodTraceType;
+  }
+
   std::string start(reinterpret_cast<const char*>(data),
                     std::min<size_t>(size, kGuessTraceMaxLookahead));
 
@@ -168,6 +173,10 @@
   if (base::StartsWith(start_minus_white_space, "[{\""))
     return kJsonTraceType;
 
+  // ART method traces (non-streaming).
+  if (base::StartsWith(start, "*version\n"))
+    return kArtMethodTraceType;
+
   // Systrace with header but no leading HTML.
   if (base::Contains(start, "# tracer"))
     return kSystraceTraceType;
diff --git a/src/trace_processor/util/trace_type.h b/src/trace_processor/util/trace_type.h
index 963e8d8..5a29c61 100644
--- a/src/trace_processor/util/trace_type.h
+++ b/src/trace_processor/util/trace_type.h
@@ -39,6 +39,7 @@
   kZipFile,
   kInstrumentsXmlTraceType,
   kGeckoTraceType,
+  kArtMethodTraceType,
 };
 
 constexpr size_t kGuessTraceMaxLookahead = 64;
diff --git a/test/data/art-method-tracing.trace.sha256 b/test/data/art-method-tracing.trace.sha256
new file mode 100644
index 0000000..38910a6
--- /dev/null
+++ b/test/data/art-method-tracing.trace.sha256
@@ -0,0 +1 @@
+ebd46d41eaa4656ad06535dacc1d3c6f6018a180f89c546515fed4f7e1df2337
\ No newline at end of file
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index c9e7a10..f8183da 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -60,6 +60,7 @@
 from diff_tests.parser.android.tests_surfaceflinger_transactions import SurfaceFlingerTransactions
 from diff_tests.parser.android.tests_viewcapture import ViewCapture
 from diff_tests.parser.android.tests_windowmanager import WindowManager
+from diff_tests.parser.art_method.tests import ArtMethodParser
 from diff_tests.parser.atrace.tests import Atrace
 from diff_tests.parser.atrace.tests_error_handling import AtraceErrorHandling
 from diff_tests.parser.chrome.tests import ChromeParser
@@ -67,9 +68,9 @@
 from diff_tests.parser.chrome.tests_v8 import ChromeV8Parser
 from diff_tests.parser.cros.tests import Cros
 from diff_tests.parser.fs.tests import Fs
-from diff_tests.parser.gecko.tests import GeckoParser
 from diff_tests.parser.ftrace.ftrace_crop_tests import FtraceCrop
 from diff_tests.parser.fuchsia.tests import Fuchsia
+from diff_tests.parser.gecko.tests import GeckoParser
 from diff_tests.parser.graphics.tests import GraphicsParser
 from diff_tests.parser.graphics.tests_drm_related_ftrace_events import GraphicsDrmRelatedFtraceEvents
 from diff_tests.parser.graphics.tests_gpu_trace import GraphicsGpuTrace
@@ -241,7 +242,9 @@
                          'AndroidInputEvent').fetch(),
       *Instruments(index_path, 'parser/instruments', 'Instruments').fetch(),
       *Gzip(index_path, 'parser/gzip', 'Gzip').fetch(),
-      *GeckoParser(index_path, 'parser/gecko', 'Gecko').fetch(),
+      *GeckoParser(index_path, 'parser/gecko', 'GeckoParser').fetch(),
+      *ArtMethodParser(index_path, 'parser/art_method',
+                       'ArtMethodParser').fetch(),
   ]
 
   metrics_tests = [
diff --git a/test/trace_processor/diff_tests/parser/art_method/tests.py b/test/trace_processor/diff_tests/parser/art_method/tests.py
new file mode 100644
index 0000000..4074b0f
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/art_method/tests.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+# Copyright (C) 2023 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import DataPath
+from python.generators.diff_tests.testing import Csv
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ArtMethodParser(TestSuite):
+
+  def test_art_method_smoke(self):
+    return DiffTestBlueprint(
+        trace=DataPath('art-method-tracing.trace'),
+        query="""
+          SELECT ts, dur, name, extract_arg(arg_set_id, 'pathname') AS pathname
+          FROM slice
+          LIMIT 10
+        """,
+        out=Csv('''
+          "ts","dur","name","pathname"
+          430421819465000,-1,"com.android.internal.os.ZygoteInit.main: ([Ljava/lang/String;)V","ZygoteInit.java"
+          430421819468000,-1,"com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run: ()V","RuntimeInit.java"
+          430421819469000,-1,"java.lang.reflect.Method.invoke: (Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;","Method.java"
+          430421819472000,-1,"android.app.ActivityThread.main: ([Ljava/lang/String;)V","ActivityThread.java"
+          430421819473000,-1,"android.os.Looper.loop: ()V","Looper.java"
+          430421819473000,-1,"android.os.Looper.loopOnce: (Landroid/os/Looper;JI)Z","Looper.java"
+          430421819475000,-1,"android.os.MessageQueue.next: ()Landroid/os/Message;","MessageQueue.java"
+          430421819476000,-1,"android.os.MessageQueue.nativePollOnce: (JI)V","MessageQueue.java"
+          430421819490000,-1,"java.lang.Thread.run: ()V","Thread.java"
+          430421819508000,-1,"java.lang.Daemons$Daemon.run: ()V","Daemons.java"
+        '''))
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index f2e90a5..b0c0fc6 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -36,3 +36,22 @@
 // Make field K required in T
 export type RequiredField<T, K extends keyof T> = Omit<T, K> &
   Required<Pick<T, K>>;
+
+// The lowest common denoninator between Map<> and WeakMap<>.
+// This is just to avoid duplication of the getOrCreate below.
+interface MapLike<K, V> {
+  get(key: K): V | undefined;
+  set(key: K, value: V): this;
+}
+
+export function getOrCreate<K, V>(
+  map: MapLike<K, V>,
+  key: K,
+  factory: () => V,
+): V {
+  let value = map.get(key);
+  if (value !== undefined) return value;
+  value = factory();
+  map.set(key, value);
+  return value;
+}
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index c4ca69c..3cef09d 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -47,7 +47,6 @@
   return {
     version: STATE_VERSION,
     nextId: '-1',
-    queries: {},
 
     recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
       ? autosaveConfigStore.get()
@@ -55,8 +54,6 @@
     displayConfigAsPbtxt: false,
     lastLoadedConfig: {type: 'NONE'},
 
-    traceConversionInProgress: false,
-
     perfDebug: false,
     sidebarVisible: true,
     hoveredUtid: -1,
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index 6173528..38cab82 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -174,8 +174,6 @@
 
   debugTrackId?: string;
   lastTrackReloadRequest?: number;
-  queries: ObjectById<QueryConfig>;
-  traceConversionInProgress: boolean;
   flamegraphModalDismissed: boolean;
 
   // Show track perf debugging overlay
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 891cbad..037b63a 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -13,21 +13,19 @@
 // limitations under the License.
 
 import {assertExists, assertTrue} from '../base/logging';
-import {Duration, time, Time, TimeSpan} from '../base/time';
+import {time, Time, TimeSpan} from '../base/time';
 import {Actions} from '../common/actions';
 import {cacheTrace} from '../common/cache_manager';
 import {
   getEnabledMetatracingCategories,
   isMetatracingEnabled,
 } from '../common/metatracing';
-import {EngineConfig, PendingDeeplinkState} from '../common/state';
+import {EngineConfig} from '../common/state';
 import {featureFlags, Flag} from '../core/feature_flags';
-import {globals, QuantizedLoad, ThreadDesc} from '../frontend/globals';
+import {globals, ThreadDesc} from '../frontend/globals';
 import {
-  clearOverviewData,
   publishHasFtrace,
   publishMetricError,
-  publishOverviewData,
   publishThreads,
 } from '../frontend/publish';
 import {addQueryResultsTab} from '../public/lib/query_table/query_result_tab';
@@ -62,6 +60,7 @@
 import {TraceImpl} from '../core/trace_impl';
 import {SerializedAppState} from '../public/state_serialization_schema';
 import {TraceSource} from '../public/trace_source';
+import {RouteArgs} from '../core/route_schema';
 
 type States = 'init' | 'loading_trace' | 'ready';
 
@@ -299,7 +298,7 @@
     await engine.restoreInitialTables();
   }
   for (const p of globals.extraSqlPackages) {
-    await engine.registerSqlModules(p);
+    await engine.registerSqlPackages(p);
   }
 
   const traceDetails = await getTraceInfo(engine, traceSource);
@@ -338,10 +337,6 @@
   decideTabs(trace);
 
   await listThreads(engine);
-  await loadTimelineOverview(
-    engine,
-    new TimeSpan(traceDetails.start, traceDetails.end),
-  );
 
   {
     // Check if we have any ftrace events at all
@@ -357,7 +352,7 @@
 
   const pendingDeeplink = AppImpl.instance.getAndClearInitialRouteArgs();
   if (pendingDeeplink !== undefined) {
-    await selectPendingDeeplink(trace, pendingDeeplink);
+    await selectInitialRouteArgs(trace, pendingDeeplink);
     if (
       pendingDeeplink.visStart !== undefined &&
       pendingDeeplink.visEnd !== undefined
@@ -405,12 +400,9 @@
   return trace;
 }
 
-async function selectPendingDeeplink(
-  trace: TraceImpl,
-  link: PendingDeeplinkState,
-) {
+async function selectInitialRouteArgs(trace: TraceImpl, args: RouteArgs) {
   const conditions = [];
-  const {ts, dur} = link;
+  const {ts, dur} = args;
 
   if (ts !== undefined) {
     conditions.push(`ts = ${ts}`);
@@ -498,86 +490,6 @@
   publishThreads(threads);
 }
 
-async function loadTimelineOverview(engine: Engine, trace: TimeSpan) {
-  clearOverviewData();
-  const stepSize = Duration.max(1n, trace.duration / 100n);
-  const hasSchedSql = 'select ts from sched limit 1';
-  const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
-  if (hasSchedOverview) {
-    const stepPromises = [];
-    for (
-      let start = trace.start;
-      start < trace.end;
-      start = Time.add(start, stepSize)
-    ) {
-      const progress = start - trace.start;
-      const ratio = Number(progress) / Number(trace.duration);
-      updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`);
-      const end = Time.add(start, stepSize);
-      // The (async() => {})() queues all the 100 async promises in one batch.
-      // Without that, we would wait for each step to be rendered before
-      // kicking off the next one. That would interleave an animation frame
-      // between each step, slowing down significantly the overall process.
-      stepPromises.push(
-        (async () => {
-          const schedResult = await engine.query(
-            `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
-              `where ts >= ${start} and ts < ${end} and utid != 0 ` +
-              'group by cpu order by cpu',
-          );
-          const schedData: {[key: string]: QuantizedLoad} = {};
-          const it = schedResult.iter({load: NUM, cpu: NUM});
-          for (; it.valid(); it.next()) {
-            const load = it.load;
-            const cpu = it.cpu;
-            schedData[cpu] = {start, end, load};
-          }
-          publishOverviewData(schedData);
-        })(),
-      );
-    } // for(start = ...)
-    await Promise.all(stepPromises);
-    return;
-  } // if (hasSchedOverview)
-
-  // Slices overview.
-  const sliceResult = await engine.query(`select
-            bucket,
-            upid,
-            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
-          from thread
-          inner join (
-            select
-              ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket,
-              sum(dur) as utid_sum,
-              utid
-            from slice
-            inner join thread_track on slice.track_id = thread_track.id
-            group by bucket, utid
-          ) using(utid)
-          where upid is not null
-          group by bucket, upid`);
-
-  const slicesData: {[key: string]: QuantizedLoad[]} = {};
-  const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
-  for (; it.valid(); it.next()) {
-    const bucket = it.bucket;
-    const upid = it.upid;
-    const load = it.load;
-
-    const start = Time.add(trace.start, stepSize * bucket);
-    const end = Time.add(start, stepSize);
-
-    const upidStr = upid.toString();
-    let loadArray = slicesData[upidStr];
-    if (loadArray === undefined) {
-      loadArray = slicesData[upidStr] = [];
-    }
-    loadArray.push({start, end, load});
-  }
-  publishOverviewData(slicesData);
-}
-
 async function initialiseHelperViews(engine: Engine) {
   updateStatus('Creating annotation counter track table');
   // Create the helper tables for all the annotations related data.
diff --git a/ui/src/core/selection_manager.ts b/ui/src/core/selection_manager.ts
index 1f930d7..4fd43c2 100644
--- a/ui/src/core/selection_manager.ts
+++ b/ui/src/core/selection_manager.ts
@@ -15,18 +15,13 @@
 import {assertTrue, assertUnreachable} from '../base/logging';
 import {
   Selection,
-  LegacySelection,
   Area,
   SelectionOpts,
   SelectionManager,
   AreaSelectionAggregator,
   SqlSelectionResolver,
 } from '../public/selection';
-import {duration, Time, time, TimeSpan} from '../base/time';
-import {
-  GenericSliceDetailsTabConfig,
-  GenericSliceDetailsTabConfigBase,
-} from '../public/details_panel';
+import {TimeSpan} from '../base/time';
 import {raf} from './raf_scheduler';
 import {exists, Optional} from '../base/utils';
 import {TrackManagerImpl} from './track_manager';
@@ -49,7 +44,6 @@
 //    requires querying the SQL engine, which is an async operation.
 export class SelectionManagerImpl implements SelectionManager {
   private _selection: Selection = {kind: 'empty'};
-  private _selectedDetails?: LegacySelectionDetails;
   private _aggregationManager: SelectionAggregationManager;
   // Incremented every time _selection changes.
   private readonly selectionResolvers = new Array<SqlSelectionResolver>();
@@ -177,62 +171,10 @@
     });
   }
 
-  // There is no matching addLegacy as we did not support multi-single
-  // selection with the legacy selection system.
-  selectLegacy(legacySelection: LegacySelection, opts?: SelectionOpts): void {
-    this.setSelection(
-      {
-        kind: 'legacy',
-        legacySelection,
-      },
-      opts,
-    );
-  }
-
-  selectGenericSlice(args: {
-    id: number;
-    sqlTableName: string;
-    start: time;
-    duration: duration;
-    trackUri: string;
-    detailsPanelConfig: {
-      kind: string;
-      config: GenericSliceDetailsTabConfigBase;
-    };
-  }): void {
-    const detailsPanelConfig: GenericSliceDetailsTabConfig = {
-      id: args.id,
-      ...args.detailsPanelConfig.config,
-    };
-    this.setSelection({
-      kind: 'legacy',
-      legacySelection: {
-        kind: 'GENERIC_SLICE',
-        id: args.id,
-        sqlTableName: args.sqlTableName,
-        start: args.start,
-        duration: args.duration,
-        trackUri: args.trackUri,
-        detailsPanelConfig: {
-          kind: args.detailsPanelConfig.kind,
-          config: detailsPanelConfig,
-        },
-      },
-    });
-  }
-
   get selection(): Selection {
     return this._selection;
   }
 
-  get legacySelection(): LegacySelection | null {
-    return toLegacySelection(this._selection);
-  }
-
-  get legacySelectionDetails(): LegacySelectionDetails | undefined {
-    return this._selectedDetails;
-  }
-
   registerSqlSelectionResolver(resolver: SqlSelectionResolver): void {
     this.selectionResolvers.push(resolver);
   }
@@ -321,19 +263,37 @@
       switch (this.selection.kind) {
         case 'track_event':
           return this.selection.trackUri;
-        case 'legacy':
-          return this.selection.legacySelection.trackUri;
+        // TODO(stevegolton): Handle scrolling to area and note selections.
         default:
           return undefined;
       }
     })();
-    const range = this.findTimeRangeOfSelection();
+    const range = this.findFocusRangeOfSelection();
     this.scrollHelper.scrollTo({
       time: range ? {...range} : undefined,
       track: uri ? {uri: uri, expandGroup: true} : undefined,
     });
   }
 
+  // Finds the time range range that we should actually focus on - using dummy
+  // values for instant and incomplete slices, so we don't end up super zoomed
+  // in.
+  private findFocusRangeOfSelection(): Optional<TimeSpan> {
+    const sel = this.selection;
+    if (sel.kind === 'track_event') {
+      // The focus range of slices is different to that of the actual span
+      if (sel.dur === -1n) {
+        return TimeSpan.fromTimeAndDuration(sel.ts, INCOMPLETE_SLICE_DURATION);
+      } else if (sel.dur === 0n) {
+        return TimeSpan.fromTimeAndDuration(sel.ts, INSTANT_FOCUS_DURATION);
+      } else {
+        return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
+      }
+    } else {
+      return this.findTimeRangeOfSelection();
+    }
+  }
+
   findTimeRangeOfSelection(): Optional<TimeSpan> {
     const sel = this.selection;
     if (sel.kind === 'area') {
@@ -358,18 +318,6 @@
       return TimeSpan.fromTimeAndDuration(sel.ts, sel.dur);
     }
 
-    const legacySel = this.legacySelection;
-    if (!exists(legacySel)) {
-      return undefined;
-    }
-
-    if (legacySel.kind === 'GENERIC_SLICE') {
-      return findTimeRangeOfSlice({
-        ts: legacySel.start,
-        dur: legacySel.duration,
-      });
-    }
-
     return undefined;
   }
 
@@ -377,51 +325,3 @@
     return this._aggregationManager;
   }
 }
-
-function toLegacySelection(selection: Selection): LegacySelection | null {
-  switch (selection.kind) {
-    case 'area':
-    case 'track_event':
-    case 'empty':
-    case 'note':
-      return null;
-    case 'union':
-      for (const child of selection.selections) {
-        const result = toLegacySelection(child);
-        if (result !== null) {
-          return result;
-        }
-      }
-      return null;
-    case 'legacy':
-      return selection.legacySelection;
-    default:
-      assertUnreachable(selection);
-      return null;
-  }
-}
-
-// Returns the start and end points of a slice-like object If slice is instant
-// or incomplete, dummy time will be returned which instead.
-function findTimeRangeOfSlice(slice: {ts?: time; dur?: duration}): TimeSpan {
-  if (exists(slice.ts) && exists(slice.dur)) {
-    if (slice.dur === -1n) {
-      return TimeSpan.fromTimeAndDuration(slice.ts, INCOMPLETE_SLICE_DURATION);
-    } else if (slice.dur === 0n) {
-      return TimeSpan.fromTimeAndDuration(slice.ts, INSTANT_FOCUS_DURATION);
-    } else {
-      return TimeSpan.fromTimeAndDuration(slice.ts, slice.dur);
-    }
-  } else {
-    // TODO(primiano): unclear why we dont return undefined here.
-    return new TimeSpan(Time.INVALID, Time.INVALID);
-  }
-}
-
-export interface LegacySelectionDetails {
-  ts?: time;
-  dur?: duration;
-  // Additional information for sched selection, used to draw the wakeup arrow.
-  wakeupTs?: time;
-  wakerCpu?: number;
-}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
index 3f5888a..6e6b909 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -12,20 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
 import {NAMED_ROW} from '../../frontend/named_slice_track';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {Slice} from '../../public/track';
 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';
+import {TrackEventDetails} from '../../public/selection';
+import {Duration, Time} from '../../base/time';
 
 export const CRITICAL_USER_INTERACTIONS_KIND =
   'org.chromium.CriticalUserInteraction.track';
@@ -42,28 +38,6 @@
   type: string;
 }
 
-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 {
   static readonly kind = `/critical_user_interactions`;
 
@@ -84,64 +58,34 @@
     };
   }
 
-  getDetailsPanel(
-    args: OnSliceClickArgs<CriticalUserInteractionSlice>,
-  ): CustomSqlDetailsPanelConfig {
-    let detailsPanel = {
-      kind: GenericSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Interaction',
-      },
-    };
+  async getSelectionDetails(
+    id: number,
+  ): Promise<TrackEventDetails | undefined> {
+    const query = `
+      SELECT
+        ts,
+        dur,
+        type
+      FROM (${this.getSqlSource()})
+      WHERE id = ${id}
+    `;
 
-    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;
+    const result = await this.engine.query(query);
+    if (result.numRows() === 0) {
+      return undefined;
     }
-    return detailsPanel;
-  }
 
-  onSliceClick(args: OnSliceClickArgs<CriticalUserInteractionSlice>) {
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    this.trace.selection.selectGenericSlice({
-      id: args.slice.scopedId,
-      sqlTableName: this.tableName,
-      start: args.slice.ts,
-      duration: args.slice.dur,
-      trackUri: this.uri,
-      detailsPanelConfig: {
-        kind: detailsPanelConfig.kind,
-        config: detailsPanelConfig.config,
-      },
+    const row = result.iter({
+      ts: LONG,
+      dur: LONG,
+      type: STR,
     });
+
+    return {
+      ts: Time.fromRaw(row.ts),
+      dur: Duration.fromRaw(row.dur),
+      interactionType: row.type,
+    };
   }
 
   getSqlImports(): CustomSqlImportConfig {
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 23560c9..1660f10 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -12,9 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {BottomTabToSCSAdapter} from '../../public/utils';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
 import {PageLoadDetailsPanel} from './page_load_details_panel';
@@ -22,6 +19,8 @@
 import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
 import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
 import {TrackNode} from '../../public/workspace';
+import {TrackEventSelection} from '../../public/selection';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
 
 class CriticalUserInteractionPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
@@ -48,65 +47,24 @@
         trace: ctx,
         uri: CriticalUserInteractionTrack.kind,
       }),
+      detailsPanel: (sel: TrackEventSelection) => {
+        switch (sel.interactionType) {
+          case 'chrome_page_loads':
+            return new PageLoadDetailsPanel(ctx, sel.eventId);
+          case 'chrome_startups':
+            return new StartupDetailsPanel(ctx, sel.eventId);
+          case 'chrome_web_content_interactions':
+            return new WebContentInteractionPanel(ctx, sel.eventId);
+          default:
+            return new GenericSliceDetailsTab(
+              ctx,
+              'chrome_interactions',
+              sel.eventId,
+              'Chrome Interaction',
+            );
+        }
+      },
     });
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === PageLoadDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new PageLoadDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === StartupDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new StartupDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind ===
-              WebContentInteractionPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new WebContentInteractionPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 }
 
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
index e01e4a8..e2d0b54 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/page_load_details_panel.ts
@@ -13,29 +13,24 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {
   Details,
   DetailsSchema,
 } from '../../frontend/widgets/sql/details/details';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 import d = DetailsSchema;
 
-export class PageLoadDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.PageLoadDetailsPanel';
+export class PageLoadDetailsPanel implements TrackEventDetailsPanel {
   private data: Details;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): PageLoadDetailsPanel {
-    return new PageLoadDetailsPanel(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.data = new Details(this.trace, 'chrome_page_loads', this.config.id, {
+  constructor(
+    private readonly trace: Trace,
+    id: number,
+  ) {
+    this.data = new Details(this.trace, 'chrome_page_loads', id, {
       'Navigation start': d.Timestamp('navigation_start_ts'),
       'FCP event': d.Timestamp('fcp_ts'),
       'FCP': d.Interval('navigation_start_ts', 'fcp'),
@@ -65,21 +60,13 @@
     });
   }
 
-  viewTab() {
+  render() {
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Chrome Page Load',
       },
       m(GridLayout, m(GridLayoutColumn, this.data.render())),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data.isLoading();
-  }
 }
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
index ec8667f..0c26623 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/startup_details_panel.ts
@@ -14,8 +14,6 @@
 
 import m from 'mithril';
 import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
 import {LONG, NUM, STR, STR_NULL} from '../../trace_processor/query_result';
@@ -25,6 +23,8 @@
 import {SqlRef} from '../../widgets/sql_ref';
 import {dictToTreeNodes, Tree} from '../../widgets/tree';
 import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
+import {Trace} from '../../public/trace';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
 
 interface Data {
   startupId: number;
@@ -35,24 +35,16 @@
   upid: Upid;
 }
 
-export class StartupDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.StartupDetailsPanel';
-  private loaded = false;
-  private data: Data | undefined;
+export class StartupDetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): StartupDetailsPanel {
-    return new StartupDetailsPanel(args);
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         activity_id AS startupId,
         name,
@@ -64,7 +56,7 @@
         launch_cause AS launchCause,
         browser_upid AS upid
       FROM chrome_startups
-      WHERE id = ${this.config.id};
+      WHERE id = ${this.id};
     `);
 
     const iter = queryResult.firstRow({
@@ -87,8 +79,6 @@
     if (iter.launchCause) {
       this.data.launchCause = iter.launchCause;
     }
-
-    this.loaded = true;
   }
 
   private getDetailsDictionary() {
@@ -106,20 +96,20 @@
     }
     details['SQL ID'] = m(SqlRef, {
       table: 'chrome_startups',
-      id: this.config.id,
+      id: this.id,
     });
     return details;
   }
 
-  viewTab() {
-    if (this.isLoading()) {
+  render() {
+    if (!this.data) {
       return m('h2', 'Loading');
     }
 
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Chrome Startup',
       },
       m(
         GridLayout,
@@ -134,12 +124,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
index cb6a7fb..25e49aa 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/web_content_interaction_details_panel.ts
@@ -28,8 +28,6 @@
 
 import m from 'mithril';
 import {duration, Time, time} from '../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {asUpid, Upid} from '../../trace_processor/sql_utils/core_types';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
@@ -39,6 +37,8 @@
 import {Section} from '../../widgets/section';
 import {SqlRef} from '../../widgets/sql_ref';
 import {dictToTreeNodes, Tree} from '../../widgets/tree';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 interface Data {
   ts: time;
@@ -48,24 +48,16 @@
   upid: Upid;
 }
 
-export class WebContentInteractionPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.WebContentInteractionPanel';
-  private loaded = false;
-  private data: Data | undefined;
+export class WebContentInteractionPanel implements TrackEventDetailsPanel {
+  private data?: Data;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): WebContentInteractionPanel {
-    return new WebContentInteractionPanel(args);
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         ts,
         dur,
@@ -73,7 +65,7 @@
         total_duration_ms AS totalDurationMs,
         renderer_upid AS upid
       FROM chrome_web_content_interactions
-      WHERE id = ${this.config.id};
+      WHERE id = ${this.id};
     `);
 
     const iter = queryResult.firstRow({
@@ -91,8 +83,6 @@
       totalDurationMs: iter.totalDurationMs,
       upid: asUpid(iter.upid),
     };
-
-    this.loaded = true;
   }
 
   private getDetailsDictionary() {
@@ -107,20 +97,20 @@
     });
     details['SQL ID'] = m(SqlRef, {
       table: 'chrome_web_content_interactions',
-      id: this.config.id,
+      id: this.id,
     });
     return details;
   }
 
-  viewTab() {
-    if (this.isLoading()) {
+  render() {
+    if (!this.data) {
       return m('h2', 'Loading');
     }
 
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Chrome Web Content Interaction',
       },
       m(
         GridLayout,
@@ -135,12 +125,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts b/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
deleted file mode 100644
index 1df20a2..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/chrome_tasks_scroll_jank_track.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2022 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {
-  NAMED_ROW,
-  NamedRow,
-  NamedSliceTrack,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Slice} from '../../public/track';
-
-export class ChromeTasksScrollJankTrack extends NamedSliceTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
-  }
-
-  getRowSpec(): NamedRow {
-    return NAMED_ROW;
-  }
-
-  rowToSlice(row: NamedRow): Slice {
-    return this.rowToSliceBase(row);
-  }
-
-  getSqlSource(): string {
-    return `
-      select
-        s2.ts as ts,
-        s2.dur as dur,
-        s2.id as id,
-        0 as depth,
-        s1.full_name as name
-      from chrome_tasks_delaying_input_processing s1
-      join slice s2 on s2.id=s1.slice_id
-    `;
-  }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
deleted file mode 100644
index c6232a2..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-// Copyright (C) 2024 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {ObjectByKey} from '../../common/state';
-import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
-
-export const ENABLE_CHROME_SCROLL_JANK_PLUGIN = featureFlags.register({
-  id: 'enableChromeScrollJankPlugin',
-  name: 'Enable Chrome Scroll Jank plugin',
-  description: 'Adds new tracks for scroll jank in Chrome',
-  defaultValue: false,
-});
-
-export interface ScrollJankTrackSpec {
-  key: string;
-  sqlTableName: string;
-  detailsPanelConfig: CustomSqlDetailsPanelConfig;
-}
-
-// Global state for the scroll jank plugin.
-export class ScrollJankPluginState {
-  private static instance?: ScrollJankPluginState;
-  private tracks: ObjectByKey<ScrollJankTrackSpec>;
-
-  private constructor() {
-    this.tracks = {};
-  }
-
-  public static getInstance(): ScrollJankPluginState {
-    if (!ScrollJankPluginState.instance) {
-      ScrollJankPluginState.instance = new ScrollJankPluginState();
-    }
-
-    return ScrollJankPluginState.instance;
-  }
-
-  public registerTrack(args: {
-    kind: string;
-    trackUri: string;
-    tableName: string;
-    detailsPanelConfig: CustomSqlDetailsPanelConfig;
-  }): void {
-    this.tracks[args.kind] = {
-      key: args.trackUri,
-      sqlTableName: args.tableName,
-      detailsPanelConfig: args.detailsPanelConfig,
-    };
-  }
-
-  public unregisterTrack(kind: string): void {
-    delete this.tracks[kind];
-  }
-
-  public getTrack(kind: string): ScrollJankTrackSpec | undefined {
-    return this.tracks[kind];
-  }
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
index 2d629ea..ae6b128 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_details_panel.ts
@@ -13,10 +13,8 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Duration, duration, time} from '../../base/time';
+import {Duration, duration, Time, time} from '../../base/time';
 import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {hasArgs, renderArguments} from '../../frontend/slice_args';
 import {renderDetails} from '../../frontend/slice_details';
 import {
@@ -37,7 +35,7 @@
   widgetColumn,
 } from '../../frontend/tables/table';
 import {TreeTable, TreeTableAttrs} from '../../frontend/widgets/treetable';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
 import {Section} from '../../widgets/section';
@@ -51,13 +49,10 @@
   getScrollJankCauseStage,
 } from './scroll_jank_cause_link_utils';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
-import {
-  getScrollJankSlices,
-  getSliceForTrack,
-  ScrollJankSlice,
-} from './scroll_jank_slice';
 import {sliceRef} from '../../frontend/widgets/slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 // Given a node in the slice tree, return a path from root to it.
 function getPath(slice: SliceTreeNode): string[] {
@@ -104,15 +99,17 @@
   return `${delta > 0 ? '+' : ''}${Duration.humanise(delta)}`;
 }
 
-export class EventLatencySliceDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.EventLatencySliceDetailsPanel';
-
-  private loaded = false;
+export class EventLatencySliceDetailsPanel implements TrackEventDetailsPanel {
   private name = '';
   private topEventLatencyId: SliceSqlId | undefined = undefined;
 
   private sliceDetails?: SliceDetails;
-  private jankySlice?: ScrollJankSlice;
+  private jankySlice?: {
+    ts: time;
+    dur: duration;
+    id: number;
+    causeOfJank: string;
+  };
 
   // Whether this stage has caused jank. This is also true for top level
   // EventLatency slices where a descendant is a cause of jank.
@@ -132,31 +129,24 @@
 
   private tracksByTrackId: Map<number, string>;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): EventLatencySliceDetailsPanel {
-    return new EventLatencySliceDetailsPanel(args);
-  }
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {
     this.tracksByTrackId = new Map<number, string>();
     this.trace.tracks.getAllTracks().forEach((td) => {
       td.tags?.trackIds?.forEach((trackId) => {
         this.tracksByTrackId.set(trackId, td.uri);
       });
     });
-
-    this.loadData();
   }
 
-  async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         name
-      FROM ${this.config.sqlTableName}
-      WHERE id = ${this.config.id}
+      FROM slice
+      WHERE id = ${this.id}
       `);
 
     const iter = queryResult.firstRow({
@@ -169,14 +159,12 @@
     await this.loadJankSlice();
     await this.loadRelevantThreads();
     await this.loadEventLatencyBreakdown();
-
-    this.loaded = true;
   }
 
   async loadSlice() {
     this.sliceDetails = await getSlice(
-      this.engine,
-      asSliceSqlId(this.config.id),
+      this.trace.engine,
+      asSliceSqlId(this.id),
     );
     raf.scheduleRedraw();
   }
@@ -194,14 +182,25 @@
       );
     }
 
-    const possibleSlices = await getScrollJankSlices(
-      this.engine,
-      this.topEventLatencyId,
-    );
-    // We may not get any slices if the EventLatency doesn't indicate any
-    // jank occurred.
-    if (possibleSlices.length > 0) {
-      this.jankySlice = possibleSlices[0];
+    const it = (
+      await this.trace.engine.query(`
+      SELECT ts, dur, id, cause_of_jank as causeOfJank
+      FROM chrome_janky_frame_presentation_intervals
+      WHERE event_latency_id = ${this.topEventLatencyId}`)
+    ).iter({
+      id: NUM,
+      ts: LONG,
+      dur: LONG,
+      causeOfJank: STR,
+    });
+
+    if (it.valid()) {
+      this.jankySlice = {
+        id: it.id,
+        ts: Time.fromRaw(it.ts),
+        dur: Duration.fromRaw(it.dur),
+        causeOfJank: it.causeOfJank,
+      };
     }
   }
 
@@ -214,7 +213,7 @@
     if (this.sliceDetails.name === 'EventLatency' && !this.jankySlice) return;
 
     const possibleScrollJankStage = await getScrollJankCauseStage(
-      this.engine,
+      this.trace.engine,
       this.topEventLatencyId,
     );
     if (this.sliceDetails.name === 'EventLatency') {
@@ -237,7 +236,7 @@
 
     if (this.relevantThreadStage) {
       this.relevantThreadTracks = await getEventLatencyCauseTracks(
-        this.engine,
+        this.trace.engine,
         this.relevantThreadStage,
       );
     }
@@ -248,7 +247,7 @@
       return;
     }
     this.eventLatencyBreakdown = await getDescendantSliceTree(
-      this.engine,
+      this.trace.engine,
       this.topEventLatencyId,
     );
 
@@ -264,7 +263,7 @@
     AND HAS_DESCENDANT_SLICE_WITH_NAME(
       id,
       'SubmitCompositorFrameToPresentationCompositorFrame')`;
-    const prevEventLatency = await getSliceFromConstraints(this.engine, {
+    const prevEventLatency = await getSliceFromConstraints(this.trace.engine, {
       filters: [
         `name = 'EventLatency'`,
         `id < ${this.topEventLatencyId}`,
@@ -275,12 +274,12 @@
     });
     if (prevEventLatency.length > 0) {
       this.prevEventLatencyBreakdown = await getDescendantSliceTree(
-        this.engine,
+        this.trace.engine,
         prevEventLatency[0].id,
       );
     }
 
-    const nextEventLatency = await getSliceFromConstraints(this.engine, {
+    const nextEventLatency = await getSliceFromConstraints(this.trace.engine, {
       filters: [
         `name = 'EventLatency'`,
         `id > ${this.topEventLatencyId}`,
@@ -291,7 +290,7 @@
     });
     if (nextEventLatency.length > 0) {
       this.nextEventLatencyBreakdown = await getDescendantSliceTree(
-        this.engine,
+        this.trace.engine,
         nextEventLatency[0].id,
       );
     }
@@ -381,7 +380,7 @@
   private async getOldestAncestorSliceId(): Promise<number> {
     let eventLatencyId = -1;
     if (!this.sliceDetails) return eventLatencyId;
-    const queryResult = await this.engine.query(`
+    const queryResult = await this.trace.engine.query(`
       SELECT
         id
       FROM ancestor_slice(${this.sliceDetails.id})
@@ -415,16 +414,15 @@
             : 'EventLatency in context of other Input events',
           right: this.sliceDetails ? '' : 'N/A',
         }),
-        m(TreeNode, {
-          left: this.jankySlice
-            ? getSliceForTrack(
-                this.jankySlice,
-                SCROLL_JANK_V3_TRACK_KIND,
-                'Jank Interval',
-              )
-            : 'Jank Interval',
-          right: this.jankySlice ? '' : 'N/A',
-        }),
+        this.jankySlice &&
+          m(TreeNode, {
+            left: renderSliceRef({
+              trace: this.trace,
+              id: this.jankySlice.id,
+              trackUri: JANKS_TRACK_URI,
+              title: this.jankySlice.causeOfJank,
+            }),
+          }),
       ),
     );
   }
@@ -498,7 +496,7 @@
     );
   }
 
-  viewTab() {
+  render() {
     if (this.sliceDetails) {
       const slice = this.sliceDetails;
 
@@ -544,12 +542,4 @@
       return m(DetailsShell, {title: 'Slice', description: 'Loading...'});
     }
   }
-
-  isLoading() {
-    return !this.loaded;
-  }
-
-  getTitle(): string {
-    return `Current Selection`;
-  }
 }
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 8581258..59e10c7 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
@@ -12,20 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {globals} from '../../frontend/globals';
 import {NamedRow} from '../../frontend/named_slice_track';
 import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public/track_kinds';
 import {Slice} from '../../public/track';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {JANK_COLOR} from './jank_colors';
-import {ScrollJankPluginState} from './common';
-import {exists} from '../../base/utils';
 
 export const JANKY_LATENCY_NAME = 'Janky EventLatency';
 
@@ -35,32 +29,12 @@
     private baseTable: string,
   ) {
     super(args);
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: CHROME_EVENT_LATENCY_TRACK_KIND,
-      trackUri: this.uri,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      CHROME_EVENT_LATENCY_TRACK_KIND,
-    );
   }
 
   getSqlSource(): string {
     return `SELECT * FROM ${this.baseTable}`;
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: EventLatencySliceDetailsPanel.kind,
-      config: {title: '', sqlTableName: this.tableName},
-    };
-  }
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       sqlTableName: this.baseTable,
@@ -76,22 +50,6 @@
     }
   }
 
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      const currentSelection = this.trace.selection.legacySelection;
-      const isSelected =
-        exists(currentSelection) &&
-        currentSelection.kind === 'GENERIC_SLICE' &&
-        currentSelection.id !== undefined &&
-        currentSelection.id === slice.id;
-
-      const highlighted = globals.state.highlightedSliceId === slice.id;
-      const hasFocus = highlighted || isSelected;
-      slice.isHighlighted = !!hasFocus;
-    }
-    super.onUpdatedSlices(slices);
-  }
-
   // At the moment we will just display the slice details. However, on select,
   // this behavior should be customized to show jank-related data.
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index e67921f..ab0dde0 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -12,24 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {v4 as uuidv4} from 'uuid';
 import {uuidv4Sql} from '../../base/uuid';
 import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
 import {featureFlags} from '../../core/feature_flags';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {BottomTabToSCSAdapter} from '../../public/utils';
-import {
-  CHROME_EVENT_LATENCY_TRACK_KIND,
-  CHROME_TOPLEVEL_SCROLLS_KIND,
-  CHROME_SCROLL_JANK_TRACK_KIND,
-  SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public/track_kinds';
-import {NUM} from '../../trace_processor/query_result';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {Engine} from '../../trace_processor/engine';
-import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
-import {ENABLE_CHROME_SCROLL_JANK_PLUGIN} from './common';
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {EventLatencyTrack, JANKY_LATENCY_NAME} from './event_latency_track';
 import {ScrollDetailsPanel} from './scroll_details_panel';
@@ -38,9 +25,6 @@
 import {TopLevelScrollTrack} from './scroll_track';
 import {ScrollJankCauseMap} from './scroll_jank_cause_map';
 import {TrackNode} from '../../public/workspace';
-import {getOrCreateGroupForThread} from '../../public/standard_groups';
-import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab';
-import {ThreadSliceDetailsPanel} from '../../frontend/thread_slice_details_tab';
 
 const ENABLE_SCROLL_JANK_PLUGIN_V2 = featureFlags.register({
   id: 'enableScrollJankPluginV2',
@@ -51,38 +35,6 @@
 
 class ChromeScrollJankPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    if (ENABLE_CHROME_SCROLL_JANK_PLUGIN.get()) {
-      await this.addChromeScrollJankTrack(ctx);
-
-      if (!(await isChromeTrace(ctx.engine))) {
-        return;
-      }
-
-      // Initialise the chrome_tasks_delaying_input_processing table. It will be
-      // used in the tracks above.
-      await ctx.engine.query(`
-        INCLUDE PERFETTO MODULE deprecated.v42.common.slices;
-        SELECT RUN_METRIC(
-          'chrome/chrome_tasks_delaying_input_processing.sql',
-          'duration_causing_jank_ms',
-          /* duration_causing_jank_ms = */ '8');`);
-
-      const query = `
-         select
-           s1.full_name,
-           s1.duration_ms,
-           s1.slice_id,
-           s1.thread_dur_ms,
-           s2.id,
-           s2.ts,
-           s2.dur,
-           s2.track_id
-         from chrome_tasks_delaying_input_processing s1
-         join slice s2 on s1.slice_id=s2.id
-         `;
-      addQueryResultsTab(ctx, {query, title: 'Scroll Jank: long tasks'});
-    }
-
     if (ENABLE_SCROLL_JANK_PLUGIN_V2.get()) {
       const group = new TrackNode({
         title: 'Chrome Scroll Jank',
@@ -98,46 +50,6 @@
     }
   }
 
-  private async addChromeScrollJankTrack(ctx: Trace): Promise<void> {
-    const queryResult = await ctx.engine.query(`
-      select
-        utid,
-        upid
-      from thread
-      where name='CrBrowserMain'
-    `);
-
-    if (queryResult.numRows() === 0) {
-      return;
-    }
-
-    const it = queryResult.firstRow({
-      utid: NUM,
-      upid: NUM,
-    });
-
-    const {upid, utid} = it;
-    const uri = 'perfetto.ChromeScrollJank';
-    const title = 'Scroll Jank causes - long tasks';
-    ctx.tracks.registerTrack({
-      uri,
-      title,
-      tags: {
-        kind: CHROME_SCROLL_JANK_TRACK_KIND,
-        upid,
-        utid,
-      },
-      track: new ChromeTasksScrollJankTrack({
-        trace: ctx,
-        uri,
-      }),
-      detailsPanel: () => new ThreadSliceDetailsPanel(ctx, 'slice'),
-    });
-    const group = getOrCreateGroupForThread(ctx.workspace, utid);
-    const track = new TrackNode({uri, title});
-    group.addChildInOrder(track);
-  }
-
   private async addTopLevelScrollTrack(
     ctx: Trace,
     group: TrackNode,
@@ -153,36 +65,17 @@
     ctx.tracks.registerTrack({
       uri,
       title,
-      tags: {
-        kind: CHROME_TOPLEVEL_SCROLLS_KIND,
-      },
       track: new TopLevelScrollTrack({
         trace: ctx,
         uri,
       }),
+      detailsPanel: (sel) => {
+        return new ScrollDetailsPanel(ctx, sel.eventId);
+      },
     });
 
     const track = new TrackNode({uri, title});
     group.addChildInOrder(track);
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ScrollDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ScrollDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 
   private async addEventLatencyTrack(
@@ -291,34 +184,14 @@
     ctx.tracks.registerTrack({
       uri,
       title,
-      tags: {
-        kind: CHROME_EVENT_LATENCY_TRACK_KIND,
-      },
       track: new EventLatencyTrack({trace: ctx, uri}, baseTable),
+      detailsPanel: (sel) => {
+        return new EventLatencySliceDetailsPanel(ctx, sel.eventId);
+      },
     });
 
     const track = new TrackNode({uri, title});
     group.addChildInOrder(track);
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind ===
-              EventLatencySliceDetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new EventLatencySliceDetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 
   private async addScrollJankV3ScrollTrack(
@@ -335,54 +208,18 @@
     ctx.tracks.registerTrack({
       uri,
       title,
-      tags: {
-        kind: SCROLL_JANK_V3_TRACK_KIND,
-      },
       track: new ScrollJankV3Track({
         trace: ctx,
         uri,
       }),
+      detailsPanel: (sel) => new ScrollJankV3DetailsPanel(ctx, sel.eventId),
     });
 
     const track = new TrackNode({uri, title});
     group.addChildInOrder(track);
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ScrollJankV3DetailsPanel.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ScrollJankV3DetailsPanel({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 }
 
-async function isChromeTrace(engine: Engine) {
-  const queryResult = await engine.query(`
-      select utid, upid
-      from thread
-      where name='CrBrowserMain'
-      `);
-
-  const it = queryResult.iter({
-    utid: NUM,
-    upid: NUM,
-  });
-
-  return it.valid();
-}
-
 export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.ChromeScrollJank',
   plugin: ChromeScrollJankPlugin,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
index c184ba7..30ab521 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_details_panel.ts
@@ -15,19 +15,21 @@
 import m from 'mithril';
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {
   ColumnDescriptor,
-  numberColumn,
   Table,
   TableData,
   widgetColumn,
 } from '../../frontend/tables/table';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
-import {LONG, NUM, STR} from '../../trace_processor/query_result';
+import {
+  LONG,
+  LONG_NULL,
+  NUM,
+  NUM_NULL,
+  STR,
+} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
 import {Section} from '../../widgets/section';
@@ -41,12 +43,9 @@
   getPredictorJankDeltas,
   getPresentedScrollDeltas,
 } from './scroll_delta_graph';
-import {
-  getScrollJankSlices,
-  getSliceForTrack,
-  ScrollJankSlice,
-} from './scroll_jank_slice';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
+import {JANKS_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 interface Data {
   // Scroll ID.
@@ -71,32 +70,27 @@
 
 interface JankSliceDetails {
   cause: string;
-  jankSlice: ScrollJankSlice;
-  delayDur: duration;
-  delayVsync: number;
+  id: number;
+  ts: time;
+  dur?: duration;
+  delayVsync?: number;
 }
 
-export class ScrollDetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.ScrollDetailsPanel';
-  loaded = false;
-  data: Data | undefined;
-  metrics: Metrics = {};
-  orderedJankSlices: JankSliceDetails[] = [];
-  scrollDeltas: m.Child;
+export class ScrollDetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
+  private metrics: Metrics = {};
+  private orderedJankSlices: JankSliceDetails[] = [];
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScrollDetailsPanel {
-    return new ScrollDetailsPanel(args);
-  }
+  // TODO(altimin): Don't store Mithril vnodes between render cycles.
+  private scrollDeltas: m.Child;
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       WITH scrolls AS (
         SELECT
           id,
@@ -107,7 +101,7 @@
               THEN gesture_scroll_begin_ts + dur
             ELSE ts + dur
           END AS end_ts
-        FROM chrome_scrolls WHERE id = ${this.config.id})
+        FROM chrome_scrolls WHERE id = ${this.id})
       SELECT
         id,
         start_ts AS ts,
@@ -126,8 +120,6 @@
     };
 
     await this.loadMetrics();
-    this.loaded = true;
-    raf.scheduleFullRedraw();
   }
 
   private async loadMetrics() {
@@ -139,7 +131,7 @@
 
   private async loadInputEventCount() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           COUNT(*) AS inputEventCount
         FROM slice s
@@ -159,7 +151,7 @@
 
   private async loadFrameStats() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           IFNULL(frame_count, 0) AS frameCount,
           IFNULL(missed_vsyncs, 0) AS missedVsyncs,
@@ -190,39 +182,36 @@
 
   private async loadDelayData() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
+          id,
+          ts,
+          dur,
           IFNULL(sub_cause_of_jank, IFNULL(cause_of_jank, 'Unknown')) AS cause,
-          IFNULL(event_latency_id, 0) AS eventLatencyId,
-          IFNULL(dur, 0) AS delayDur,
-          IFNULL(delayed_frame_count, 0) AS delayVsync
+          event_latency_id AS eventLatencyId,
+          delayed_frame_count AS delayVsync
         FROM chrome_janky_frame_presentation_intervals s
         WHERE s.ts >= ${this.data.ts}
           AND s.ts + s.dur <= ${this.data.ts + this.data.dur}
-        ORDER by delayDur DESC;
+        ORDER by dur DESC;
       `);
 
-      const iter = queryResult.iter({
+      const it = queryResult.iter({
+        id: NUM,
+        ts: LONG,
+        dur: LONG_NULL,
         cause: STR,
-        eventLatencyId: NUM,
-        delayDur: LONG,
-        delayVsync: NUM,
+        eventLatencyId: NUM_NULL,
+        delayVsync: NUM_NULL,
       });
 
-      for (; iter.valid(); iter.next()) {
-        if (iter.delayDur <= 0) {
-          break;
-        }
-        const jankSlices = await getScrollJankSlices(
-          this.engine,
-          iter.eventLatencyId,
-        );
-
+      for (; it.valid(); it.next()) {
         this.orderedJankSlices.push({
-          cause: iter.cause,
-          jankSlice: jankSlices[0],
-          delayDur: iter.delayDur,
-          delayVsync: iter.delayVsync,
+          id: it.id,
+          ts: Time.fromRaw(it.ts),
+          dur: it.dur ?? undefined,
+          cause: it.cause,
+          delayVsync: it.delayVsync ?? undefined,
         });
       }
     }
@@ -230,17 +219,20 @@
 
   private async loadScrollOffsets() {
     if (exists(this.data)) {
-      const inputDeltas = await getInputScrollDeltas(this.engine, this.data.id);
+      const inputDeltas = await getInputScrollDeltas(
+        this.trace.engine,
+        this.data.id,
+      );
       const presentedDeltas = await getPresentedScrollDeltas(
-        this.engine,
+        this.trace.engine,
         this.data.id,
       );
       const predictorDeltas = await getPredictorJankDeltas(
-        this.engine,
+        this.trace.engine,
         this.data.id,
       );
       const jankIntervals = await getJankIntervals(
-        this.engine,
+        this.trace.engine,
         this.data.ts,
         this.data.dur,
       );
@@ -310,31 +302,27 @@
 
   private getDelayTable(): m.Child {
     if (this.orderedJankSlices.length > 0) {
-      interface DelayData {
-        jankLink: m.Child;
-        dur: m.Child;
-        delayedVSyncs: number;
-      }
-
-      const columns: ColumnDescriptor<DelayData>[] = [
-        widgetColumn<DelayData>('Cause', (x) => x.jankLink),
-        widgetColumn<DelayData>('Duration', (x) => x.dur),
-        numberColumn<DelayData>('Delayed Vsyncs', (x) => x.delayedVSyncs),
+      const columns: ColumnDescriptor<JankSliceDetails>[] = [
+        widgetColumn<JankSliceDetails>('Cause', (jankSlice) =>
+          renderSliceRef({
+            trace: this.trace,
+            id: jankSlice.id,
+            trackUri: JANKS_TRACK_URI,
+            title: jankSlice.cause,
+          }),
+        ),
+        widgetColumn<JankSliceDetails>('Duration', (jankSlice) =>
+          jankSlice.dur !== undefined
+            ? m(DurationWidget, {dur: jankSlice.dur})
+            : 'NULL',
+        ),
+        widgetColumn<JankSliceDetails>(
+          'Delayed Vsyncs',
+          (jankSlice) => jankSlice.delayVsync,
+        ),
       ];
-      const data: DelayData[] = [];
-      for (const jankSlice of this.orderedJankSlices) {
-        data.push({
-          jankLink: getSliceForTrack(
-            jankSlice.jankSlice,
-            SCROLL_JANK_V3_TRACK_KIND,
-            jankSlice.cause,
-          ),
-          dur: m(DurationWidget, {dur: jankSlice.delayDur}),
-          delayedVSyncs: jankSlice.delayVsync,
-        });
-      }
 
-      const tableData = new TableData(data);
+      const tableData = new TableData(this.orderedJankSlices);
 
       return m(Table, {
         data: tableData,
@@ -391,8 +379,8 @@
     );
   }
 
-  viewTab() {
-    if (this.isLoading() || this.data == undefined) {
+  render() {
+    if (this.data == undefined) {
       return m('h2', 'Loading');
     }
 
@@ -400,13 +388,13 @@
       'Scroll ID': this.data.id,
       'Start time': m(Timestamp, {ts: this.data.ts}),
       'Duration': m(DurationWidget, {dur: this.data.dur}),
-      'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.config.id}),
+      'SQL ID': m(SqlRef, {table: 'chrome_scrolls', id: this.id}),
     });
 
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Scroll',
       },
       m(
         GridLayout,
@@ -437,12 +425,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
deleted file mode 100644
index 2b3e33c..0000000
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
+++ /dev/null
@@ -1,214 +0,0 @@
-// Copyright (C) 2023 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import m from 'mithril';
-import {Icons} from '../../base/semantic_icons';
-import {duration, time, Time} from '../../base/time';
-import {globals} from '../../frontend/globals';
-import {SliceSqlId} from '../../trace_processor/sql_utils/core_types';
-import {Engine} from '../../trace_processor/engine';
-import {LONG, NUM} from '../../trace_processor/query_result';
-import {
-  constraintsToQuerySuffix,
-  SQLConstraints,
-} from '../../trace_processor/sql_utils';
-import {Anchor} from '../../widgets/anchor';
-import {ScrollJankPluginState, ScrollJankTrackSpec} from './common';
-import {
-  CHROME_EVENT_LATENCY_TRACK_KIND,
-  SCROLL_JANK_V3_TRACK_KIND,
-} from '../../public/track_kinds';
-import {scrollTo} from '../../public/scroll_helper';
-
-interface BasicSlice {
-  // ID of slice.
-  sliceId: number;
-  // Timestamp of the beginning of this slice in nanoseconds.
-  ts: time;
-  // Duration of this slice in nanoseconds.
-  dur: duration;
-}
-
-async function getSlicesFromTrack(
-  engine: Engine,
-  track: ScrollJankTrackSpec,
-  constraints: SQLConstraints,
-): Promise<BasicSlice[]> {
-  const query = await engine.query(`
-    SELECT
-      id AS sliceId,
-      ts,
-      dur AS dur
-    FROM ${track.sqlTableName}
-    ${constraintsToQuerySuffix(constraints)}`);
-  const it = query.iter({
-    sliceId: NUM,
-    ts: LONG,
-    dur: LONG,
-  });
-
-  const result: BasicSlice[] = [];
-  for (; it.valid(); it.next()) {
-    result.push({
-      sliceId: it.sliceId as number,
-      ts: Time.fromRaw(it.ts),
-      dur: it.dur,
-    });
-  }
-  return result;
-}
-
-export type ScrollJankSlice = BasicSlice;
-export async function getScrollJankSlices(
-  engine: Engine,
-  id: number,
-): Promise<ScrollJankSlice[]> {
-  const track = ScrollJankPluginState.getInstance().getTrack(
-    SCROLL_JANK_V3_TRACK_KIND,
-  );
-  if (track == undefined) {
-    throw new Error(`${SCROLL_JANK_V3_TRACK_KIND} track is not registered.`);
-  }
-
-  const slices = await getSlicesFromTrack(engine, track, {
-    filters: [`event_latency_id=${id}`],
-  });
-  return slices;
-}
-
-export type EventLatencySlice = BasicSlice;
-export async function getEventLatencySlice(
-  engine: Engine,
-  id: number,
-): Promise<EventLatencySlice | undefined> {
-  const track = ScrollJankPluginState.getInstance().getTrack(
-    CHROME_EVENT_LATENCY_TRACK_KIND,
-  );
-  if (track == undefined) {
-    throw new Error(
-      `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
-    );
-  }
-
-  const slices = await getSlicesFromTrack(engine, track, {
-    filters: [`id=${id}`],
-  });
-  return slices[0];
-}
-
-export async function getEventLatencyDescendantSlice(
-  engine: Engine,
-  id: number,
-  descendant: string | undefined,
-): Promise<EventLatencySlice | undefined> {
-  const query = await engine.query(`
-    SELECT
-      id as sliceId,
-      ts,
-      dur as dur
-    FROM descendant_slice(${id})
-    WHERE name='${descendant}'`);
-  const it = query.iter({
-    sliceId: NUM,
-    ts: LONG,
-    dur: LONG,
-  });
-
-  const result: EventLatencySlice[] = [];
-
-  for (; it.valid(); it.next()) {
-    result.push({
-      sliceId: it.sliceId as SliceSqlId,
-      ts: Time.fromRaw(it.ts),
-      dur: it.dur,
-    });
-  }
-
-  const eventLatencyTrack = ScrollJankPluginState.getInstance().getTrack(
-    CHROME_EVENT_LATENCY_TRACK_KIND,
-  );
-  if (eventLatencyTrack == undefined) {
-    throw new Error(
-      `${CHROME_EVENT_LATENCY_TRACK_KIND} track is not registered.`,
-    );
-  }
-
-  if (result.length > 1) {
-    throw new Error(`
-        Slice table and track view ${eventLatencyTrack.sqlTableName} has more than one descendant of slice id ${id} with name ${descendant}`);
-  }
-  if (result.length === 0) {
-    return undefined;
-  }
-  return result[0];
-}
-
-interface BasicScrollJankSliceRefAttrs {
-  id: number;
-  ts: time;
-  dur: duration;
-  name: string;
-  kind: string;
-}
-
-export class ScrollJankSliceRef
-  implements m.ClassComponent<BasicScrollJankSliceRefAttrs>
-{
-  view(vnode: m.Vnode<BasicScrollJankSliceRefAttrs>) {
-    return m(
-      Anchor,
-      {
-        icon: Icons.UpdateSelection,
-        onclick: () => {
-          const track = ScrollJankPluginState.getInstance().getTrack(
-            vnode.attrs.kind,
-          );
-          if (track == undefined) {
-            throw new Error(`${vnode.attrs.kind} track is not registered.`);
-          }
-
-          const trackUri = track.key;
-          globals.selectionManager.selectGenericSlice({
-            id: vnode.attrs.id,
-            sqlTableName: track.sqlTableName,
-            start: vnode.attrs.ts,
-            duration: vnode.attrs.dur,
-            trackUri,
-            detailsPanelConfig: track.detailsPanelConfig,
-          });
-
-          scrollTo({
-            track: {uri: trackUri, expandGroup: true},
-            time: {start: vnode.attrs.ts},
-          });
-        },
-      },
-      vnode.attrs.name,
-    );
-  }
-}
-
-export function getSliceForTrack(
-  state: BasicSlice,
-  trackKind: string,
-  name: string,
-): m.Child {
-  return m(ScrollJankSliceRef, {
-    id: state.sliceId,
-    ts: state.ts,
-    dur: state.dur,
-    name: name,
-    kind: trackKind,
-  });
-}
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
index 48fd988..280fd18 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
@@ -16,8 +16,6 @@
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
 import {raf} from '../../core/raf_scheduler';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
 import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
 import {DurationWidget} from '../../frontend/widgets/duration';
@@ -30,13 +28,9 @@
 import {SqlRef} from '../../widgets/sql_ref';
 import {MultiParagraphText, TextParagraph} from '../../widgets/text_paragraph';
 import {dictToTreeNodes, Tree, TreeNode} from '../../widgets/tree';
-import {
-  EventLatencySlice,
-  getEventLatencyDescendantSlice,
-  getEventLatencySlice,
-  getSliceForTrack,
-} from './scroll_jank_slice';
-import {CHROME_EVENT_LATENCY_TRACK_KIND} from '../../public/track_kinds';
+import {EVENT_LATENCY_TRACK_URI, renderSliceRef} from './selection_utils';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 
 interface Data {
   name: string;
@@ -64,10 +58,8 @@
   return getSlice(engine, asSliceSqlId(id));
 }
 
-export class ScrollJankV3DetailsPanel extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.perfetto.ScrollJankV3DetailsPanel';
-  data: Data | undefined;
-  loaded = false;
+export class ScrollJankV3DetailsPanel implements TrackEventDetailsPanel {
+  private data?: Data;
 
   //
   // Linking to associated slices
@@ -80,29 +72,34 @@
 
   // Link to the Event Latency in the EventLatencyTrack (subset of event
   // latencies associated with input events).
-  private eventLatencySliceDetails?: EventLatencySlice;
+  private eventLatencySliceDetails?: {
+    ts: time;
+    dur: duration;
+  };
 
   // Link to the scroll jank cause stage of the associated EventLatencyTrack
   // slice. May be unknown.
-  private causeSliceDetails?: EventLatencySlice;
+  private causeSliceDetails?: {
+    id: number;
+    ts: time;
+    dur: duration;
+  };
 
   // Link to the scroll jank sub-cause stage of the associated EventLatencyTrack
   // slice. Does not apply to all causes.
-  private subcauseSliceDetails?: EventLatencySlice;
+  private subcauseSliceDetails?: {
+    id: number;
+    ts: time;
+    dur: duration;
+  };
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScrollJankV3DetailsPanel {
-    return new ScrollJankV3DetailsPanel(args);
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly id: number,
+  ) {}
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  private async loadData() {
-    const queryResult = await this.engine.query(`
+  async load() {
+    const queryResult = await this.trace.engine.query(`
       SELECT
         IIF(
           cause_of_jank IS NOT NULL,
@@ -117,7 +114,7 @@
         IFNULL(cause_of_jank, "UNKNOWN") AS causeOfJank,
         IFNULL(sub_cause_of_jank, "UNKNOWN") AS subcauseOfJank
       FROM chrome_janky_frame_presentation_intervals
-      WHERE id = ${this.config.id}`);
+      WHERE id = ${this.id}`);
 
     const iter = queryResult.firstRow({
       name: STR,
@@ -143,7 +140,6 @@
     await this.loadJankyFrames();
 
     await this.loadSlices();
-    this.loaded = true;
     raf.scheduleFullRedraw();
   }
 
@@ -164,35 +160,62 @@
   private async loadSlices() {
     if (exists(this.data)) {
       this.sliceDetails = await getSliceDetails(
-        this.engine,
+        this.trace.engine,
         this.data.eventLatencyId,
       );
-      this.eventLatencySliceDetails = await getEventLatencySlice(
-        this.engine,
-        this.data.eventLatencyId,
-      );
+      const it = (
+        await this.trace.engine.query(`
+        SELECT ts, dur
+        FROM slice
+        WHERE id = ${this.data.eventLatencyId}
+      `)
+      ).iter({ts: LONG, dur: LONG});
+      this.eventLatencySliceDetails = {
+        ts: Time.fromRaw(it.ts),
+        dur: it.dur,
+      };
 
       if (this.hasCause()) {
-        this.causeSliceDetails = await getEventLatencyDescendantSlice(
-          this.engine,
-          this.data.eventLatencyId,
-          this.data.jankCause,
-        );
+        const it = (
+          await this.trace.engine.query(`
+          SELECT id, ts, dur
+          FROM descendant_slice(${this.data.eventLatencyId})
+          WHERE name = "${this.data.jankCause}"
+        `)
+        ).iter({id: NUM, ts: LONG, dur: LONG});
+
+        if (it.valid()) {
+          this.causeSliceDetails = {
+            id: it.id,
+            ts: Time.fromRaw(it.ts),
+            dur: it.dur,
+          };
+        }
       }
 
       if (this.hasSubcause()) {
-        this.subcauseSliceDetails = await getEventLatencyDescendantSlice(
-          this.engine,
-          this.data.eventLatencyId,
-          this.data.jankSubcause,
-        );
+        const it = (
+          await this.trace.engine.query(`
+          SELECT id, ts, dur
+          FROM descendant_slice(${this.data.eventLatencyId})
+          WHERE name = "${this.data.jankSubcause}"
+        `)
+        ).iter({id: NUM, ts: LONG, dur: LONG});
+
+        if (it.valid()) {
+          this.subcauseSliceDetails = {
+            id: it.id,
+            ts: Time.fromRaw(it.ts),
+            dur: it.dur,
+          };
+        }
       }
     }
   }
 
   private async loadJankyFrames() {
     if (exists(this.data)) {
-      const queryResult = await this.engine.query(`
+      const queryResult = await this.trace.engine.query(`
         SELECT
           COUNT(*) AS jankyFrames
         FROM chrome_frame_info_with_delay
@@ -263,33 +286,36 @@
     const result: {[key: string]: m.Child} = {};
 
     if (exists(this.sliceDetails) && exists(this.data)) {
-      result['Janked Event Latency stage'] = exists(this.causeSliceDetails)
-        ? getSliceForTrack(
-            this.causeSliceDetails,
-            CHROME_EVENT_LATENCY_TRACK_KIND,
-            this.data.jankCause,
-          )
-        : this.data.jankCause;
+      result['Janked Event Latency stage'] =
+        exists(this.causeSliceDetails) &&
+        renderSliceRef({
+          trace: this.trace,
+          id: this.causeSliceDetails.id,
+          trackUri: EVENT_LATENCY_TRACK_URI,
+          title: this.data.jankCause,
+        });
 
       if (this.hasSubcause()) {
-        result['Sub-cause of Jank'] = exists(this.subcauseSliceDetails)
-          ? getSliceForTrack(
-              this.subcauseSliceDetails,
-              CHROME_EVENT_LATENCY_TRACK_KIND,
-              this.data.jankSubcause,
-            )
-          : this.data.jankSubcause;
+        result['Sub-cause of Jank'] =
+          exists(this.subcauseSliceDetails) &&
+          renderSliceRef({
+            trace: this.trace,
+            id: this.subcauseSliceDetails.id,
+            trackUri: EVENT_LATENCY_TRACK_URI,
+            title: this.data.jankCause,
+          });
       }
 
       const children = dictToTreeNodes(result);
       if (exists(this.eventLatencySliceDetails)) {
         children.unshift(
           m(TreeNode, {
-            left: getSliceForTrack(
-              this.eventLatencySliceDetails,
-              CHROME_EVENT_LATENCY_TRACK_KIND,
-              'Input EventLatency in context of ScrollUpdates',
-            ),
+            left: renderSliceRef({
+              trace: this.trace,
+              id: this.data.eventLatencyId,
+              trackUri: EVENT_LATENCY_TRACK_URI,
+              title: this.data.jankCause,
+            }),
             right: '',
           }),
         );
@@ -303,7 +329,7 @@
     return dictToTreeNodes(result);
   }
 
-  viewTab() {
+  render() {
     if (this.data === undefined) {
       return m('h2', 'Loading');
     }
@@ -313,7 +339,7 @@
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'EventLatency',
       },
       m(
         GridLayout,
@@ -326,12 +352,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return !this.loaded;
-  }
 }
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 81175e4..1df0c75 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
@@ -12,35 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {globals} from '../../frontend/globals';
 import {NamedRow} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {SCROLL_JANK_V3_TRACK_KIND} from '../../public/track_kinds';
 import {Slice} from '../../public/track';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
 import {JANK_COLOR} from './jank_colors';
-import {ScrollJankV3DetailsPanel} from './scroll_jank_v3_details_panel';
 import {getColorForSlice} from '../../core/colorizer';
-import {ScrollJankPluginState} from './common';
 
 const UNKNOWN_SLICE_NAME = 'Unknown';
 const JANK_SLICE_NAME = ' Jank';
 
 export class ScrollJankV3Track extends CustomSqlTableSliceTrack {
-  constructor(args: NewTrackArgs) {
-    super(args);
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: SCROLL_JANK_V3_TRACK_KIND,
-      trackUri: this.uri,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       columns: [
@@ -58,23 +42,6 @@
     };
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScrollJankV3DetailsPanel.kind,
-      config: {
-        sqlTableName: 'chrome_janky_frame_presentation_intervals',
-        title: 'Chrome Scroll Janks',
-      },
-    };
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      SCROLL_JANK_V3_TRACK_KIND,
-    );
-  }
-
   rowToSlice(row: NamedRow): Slice {
     const slice = super.rowToSlice(row);
 
@@ -92,20 +59,4 @@
       return {...slice, colorScheme: getColorForSlice(stage)};
     }
   }
-
-  onUpdatedSlices(slices: Slice[]) {
-    for (const slice of slices) {
-      const currentSelection = globals.selectionManager.legacySelection;
-      const isSelected =
-        currentSelection &&
-        currentSelection.kind === 'GENERIC_SLICE' &&
-        currentSelection.id !== undefined &&
-        currentSelection.id === slice.id;
-
-      const highlighted = globals.state.highlightedSliceId === slice.id;
-      const hasFocus = highlighted || isSelected;
-      slice.isHighlighted = !!hasFocus;
-    }
-    super.onUpdatedSlices(slices);
-  }
 }
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 1ee7d2c..fcd20fd 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -12,51 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {NewTrackArgs} from '../../frontend/track';
-import {CHROME_TOPLEVEL_SCROLLS_KIND} from '../../public/track_kinds';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScrollJankPluginState} from './common';
-import {ScrollDetailsPanel} from './scroll_details_panel';
 
 export class TopLevelScrollTrack extends CustomSqlTableSliceTrack {
-  public static kind = CHROME_TOPLEVEL_SCROLLS_KIND;
-
   getSqlDataSource(): CustomSqlTableDefConfig {
     return {
       columns: [`printf("Scroll %s", CAST(id AS STRING)) AS name`, '*'],
       sqlTableName: 'chrome_scrolls',
     };
   }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScrollDetailsPanel.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Top Level Scrolls',
-      },
-    };
-  }
-
-  constructor(args: NewTrackArgs) {
-    super(args);
-
-    ScrollJankPluginState.getInstance().registerTrack({
-      kind: TopLevelScrollTrack.kind,
-      trackUri: this.uri,
-      tableName: this.tableName,
-      detailsPanelConfig: this.getDetailsPanel(),
-    });
-  }
-
-  async onDestroy(): Promise<void> {
-    await super.onDestroy();
-    ScrollJankPluginState.getInstance().unregisterTrack(
-      TopLevelScrollTrack.kind,
-    );
-  }
 }
diff --git a/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.ts
new file mode 100644
index 0000000..4b79e05
--- /dev/null
+++ b/ui/src/core_plugins/chrome_scroll_jank/selection_utils.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 m from 'mithril';
+import {Anchor} from '../../widgets/anchor';
+import {Icons} from '../../base/semantic_icons';
+import {Trace} from '../../public/trace';
+
+export const SCROLLS_TRACK_URI = 'perfetto.ChromeScrollJank#toplevelScrolls';
+export const EVENT_LATENCY_TRACK_URI = 'perfetto.ChromeScrollJank#eventLatency';
+export const JANKS_TRACK_URI = 'perfetto.ChromeScrollJank#scrollJankV3';
+
+export function renderSliceRef(args: {
+  trace: Trace;
+  id: number;
+  trackUri: string;
+  title: m.Children;
+}) {
+  return m(
+    Anchor,
+    {
+      icon: Icons.UpdateSelection,
+      onclick: () => {
+        args.trace.selection.selectTrackEvent(args.trackUri, args.id, {
+          scrollToSelection: true,
+        });
+      },
+    },
+    args.title,
+  );
+}
diff --git a/ui/src/core_plugins/chrome_tasks/details.ts b/ui/src/core_plugins/chrome_tasks/details.ts
index 56acae1..ac96447 100644
--- a/ui/src/core_plugins/chrome_tasks/details.ts
+++ b/ui/src/core_plugins/chrome_tasks/details.ts
@@ -13,25 +13,21 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {
   Details,
   DetailsSchema,
 } from '../../frontend/widgets/sql/details/details';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {Trace} from '../../public/trace';
 import d = DetailsSchema;
 
-export class ChromeTasksDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'org.chromium.ChromeTasks.TaskDetailsTab';
+export class ChromeTasksDetailsPanel implements TrackEventDetailsPanel {
+  private readonly data: Details;
 
-  private data: Details;
-
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
-    this.data = new Details(this.trace, 'chrome_tasks', this.config.id, {
+  constructor(trace: Trace, eventId: number) {
+    this.data = new Details(trace, 'chrome_tasks', eventId, {
       'Task name': 'name',
       'Start time': d.Timestamp('ts'),
       'Duration': d.Interval('ts', 'dur'),
@@ -41,21 +37,13 @@
     });
   }
 
-  viewTab() {
+  render() {
     return m(
       DetailsShell,
       {
-        title: this.getTitle(),
+        title: 'Chrome Tasks',
       },
       m(GridLayout, m(GridLayoutColumn, this.data.render())),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data.isLoading();
-  }
 }
diff --git a/ui/src/core_plugins/chrome_tasks/index.ts b/ui/src/core_plugins/chrome_tasks/index.ts
index 513da9f..9c79a0d 100644
--- a/ui/src/core_plugins/chrome_tasks/index.ts
+++ b/ui/src/core_plugins/chrome_tasks/index.ts
@@ -12,18 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {uuidv4} from '../../base/uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {addSqlTableTab} from '../../frontend/sql_table_tab_interface';
 import {asUtid} from '../../trace_processor/sql_utils/core_types';
-import {BottomTabToSCSAdapter} from '../../public/utils';
 import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {ChromeTasksDetailsTab} from './details';
+import {ChromeTasksDetailsPanel} from './details';
 import {chromeTasksTable} from './table';
 import {ChromeTasksThreadTrack} from './track';
 import {TrackNode} from '../../public/workspace';
+import {TrackEventSelection} from '../../public/selection';
 
 class ChromeTasksPlugin implements PerfettoPlugin {
   onActivate() {}
@@ -103,30 +101,14 @@
         uri,
         track: new ChromeTasksThreadTrack(ctx, uri, asUtid(utid)),
         title,
+        detailsPanel: (sel: TrackEventSelection) => {
+          return new ChromeTasksDetailsPanel(ctx, sel.eventId);
+        },
       });
       const track = new TrackNode({uri, title});
       group.addChildInOrder(track);
       ctx.workspace.addChildInOrder(group);
     }
-
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === ChromeTasksDetailsTab.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new ChromeTasksDetailsTab({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 }
 
diff --git a/ui/src/core_plugins/chrome_tasks/track.ts b/ui/src/core_plugins/chrome_tasks/track.ts
index e96735c..24203ea 100644
--- a/ui/src/core_plugins/chrome_tasks/track.ts
+++ b/ui/src/core_plugins/chrome_tasks/track.ts
@@ -14,11 +14,9 @@
 
 import {Utid} from '../../trace_processor/sql_utils/core_types';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ChromeTasksDetailsTab} from './details';
 import {Trace} from '../../public/trace';
 
 export class ChromeTasksThreadTrack extends CustomSqlTableSliceTrack {
@@ -37,14 +35,4 @@
       whereClause: `utid = ${this.utid}`,
     };
   }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ChromeTasksDetailsTab.kind,
-      config: {
-        sqlTableName: 'chrome_tasks',
-        title: 'Chrome Tasks',
-      },
-    };
-  }
 }
diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts
index 3a43977..3ff5fcb 100644
--- a/ui/src/core_plugins/debug/index.ts
+++ b/ui/src/core_plugins/debug/index.ts
@@ -12,16 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {uuidv4} from '../../base/uuid';
 import {
   addDebugCounterTrack,
   addDebugSliceTrack,
 } from '../../public/lib/debug_tracks/debug_tracks';
-import {BottomTabToSCSAdapter} from '../../public/utils';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {DebugSliceDetailsTab} from '../../public/lib/debug_tracks/details_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {Optional, exists} from '../../base/utils';
 
 class DebugTracksPlugin implements PerfettoPlugin {
@@ -65,28 +61,6 @@
         }
       },
     });
-
-    // TODO(stevegolton): While debug tracks are in their current state, we rely
-    // on this plugin to provide the details panel for them. In the future, this
-    // details panel will become part of the debug track's definition.
-    ctx.tabs.registerDetailsPanel(
-      new BottomTabToSCSAdapter({
-        tabFactory: (selection) => {
-          if (
-            selection.kind === 'GENERIC_SLICE' &&
-            selection.detailsPanelConfig.kind === DebugSliceDetailsTab.kind
-          ) {
-            const config = selection.detailsPanelConfig.config;
-            return new DebugSliceDetailsTab({
-              config: config as GenericSliceDetailsTabConfig,
-              trace: ctx,
-              uuid: uuidv4(),
-            });
-          }
-          return undefined;
-        },
-      }),
-    );
   }
 }
 
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index b7e41e2..5c8ee65 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -12,14 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {uuidv4} from '../../base/uuid';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {TrackNode} from '../../public/workspace';
-import {BottomTabToSCSAdapter} from '../../public/utils';
 import {NUM} from '../../trace_processor/query_result';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
-import {ScreenshotTab} from './screenshot_panel';
+import {ScreenshotDetailsPanel} from './screenshot_panel';
 import {ScreenshotsTrack} from './screenshots_track';
 
 class ScreenshotsPlugin implements PerfettoPlugin {
@@ -45,28 +42,10 @@
         tags: {
           kind: ScreenshotsTrack.kind,
         },
+        detailsPanel: () => new ScreenshotDetailsPanel(ctx.engine),
       });
       const trackNode = new TrackNode({uri, title, sortOrder: -60});
       ctx.workspace.addChildInOrder(trackNode);
-
-      ctx.tabs.registerDetailsPanel(
-        new BottomTabToSCSAdapter({
-          tabFactory: (selection) => {
-            if (
-              selection.kind === 'GENERIC_SLICE' &&
-              selection.detailsPanelConfig.kind === ScreenshotTab.kind
-            ) {
-              const config = selection.detailsPanelConfig.config;
-              return new ScreenshotTab({
-                config: config as GenericSliceDetailsTabConfig,
-                trace: ctx,
-                uuid: uuidv4(),
-              });
-            }
-            return undefined;
-          },
-        }),
-      );
     }
   }
 }
diff --git a/ui/src/core_plugins/screenshots/screenshot_panel.ts b/ui/src/core_plugins/screenshots/screenshot_panel.ts
index a1d8cd4..a5424af 100644
--- a/ui/src/core_plugins/screenshots/screenshot_panel.ts
+++ b/ui/src/core_plugins/screenshots/screenshot_panel.ts
@@ -15,44 +15,25 @@
 import m from 'mithril';
 import {assertTrue} from '../../base/logging';
 import {exists} from '../../base/utils';
-import {BottomTab, NewBottomTabArgs} from '../../public/lib/bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../trace_processor/sql_utils/slice';
 import {asSliceSqlId} from '../../trace_processor/sql_utils/core_types';
 import {Engine} from '../../trace_processor/engine';
+import {TrackEventDetailsPanel} from '../../public/details_panel';
+import {TrackEventSelection} from '../../public/selection';
 
-async function getSliceDetails(
-  engine: Engine,
-  id: number,
-): Promise<SliceDetails | undefined> {
-  return getSlice(engine, asSliceSqlId(id));
-}
-
-export class ScreenshotTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.ScreenshotDetailsPanel';
-
+export class ScreenshotDetailsPanel implements TrackEventDetailsPanel {
   private sliceDetails?: SliceDetails;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): ScreenshotTab {
-    return new ScreenshotTab(args);
-  }
+  constructor(private readonly engine: Engine) {}
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    getSliceDetails(this.engine, this.config.id).then(
-      (sliceDetails) => (this.sliceDetails = sliceDetails),
+  async load(selection: TrackEventSelection) {
+    this.sliceDetails = await getSlice(
+      this.engine,
+      asSliceSqlId(selection.eventId),
     );
   }
 
-  renderTabCanvas() {}
-
-  getTitle() {
-    return this.config.title;
-  }
-
-  viewTab() {
+  render() {
     if (
       !exists(this.sliceDetails) ||
       !exists(this.sliceDetails.args) ||
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
index d88010e..e9decdc 100644
--- a/ui/src/core_plugins/screenshots/screenshots_track.ts
+++ b/ui/src/core_plugins/screenshots/screenshots_track.ts
@@ -13,11 +13,9 @@
 // limitations under the License.
 
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../frontend/tracks/custom_sql_table_slice_track';
-import {ScreenshotTab} from './screenshot_panel';
 
 export class ScreenshotsTrack extends CustomSqlTableSliceTrack {
   static readonly kind = 'dev.perfetto.ScreenshotsTrack';
@@ -28,14 +26,4 @@
       columns: ['*'],
     };
   }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScreenshotTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Screenshots',
-      },
-    };
-  }
 }
diff --git a/ui/src/core_plugins/test_plugin/index.ts b/ui/src/core_plugins/test_plugin/index.ts
index 2b7ccf1..0979836 100644
--- a/ui/src/core_plugins/test_plugin/index.ts
+++ b/ui/src/core_plugins/test_plugin/index.ts
@@ -19,6 +19,7 @@
   SimpleSliceTrackConfig,
 } from '../../frontend/simple_slice_track';
 import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
 
 class Plugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
@@ -49,10 +50,13 @@
 
     const title = 'Test Track';
     const uri = `/test_track`;
+    const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new SimpleSliceTrack(ctx, {trackUri: uri}, config),
+      track,
+      detailsPanel: ({eventId}) =>
+        new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
     });
 
     this.addNestedTracks(ctx, uri);
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 79aae89..18005df 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -21,7 +21,7 @@
 import {cropText} from '../base/string_utils';
 import {colorCompare} from '../public/color';
 import {UNEXPECTED_PINK} from '../core/colorizer';
-import {LegacySelection, TrackEventDetails} from '../public/selection';
+import {TrackEventDetails} from '../public/selection';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {Track} from '../public/track';
@@ -277,10 +277,6 @@
     }
   }
 
-  protected isSelectionHandled(_selection: LegacySelection): boolean {
-    return false;
-  }
-
   private getTitleFont(): string {
     const size = this.sliceLayout.titleSizePx ?? 12;
     return `${size}px Roboto Condensed`;
@@ -397,21 +393,11 @@
       visibleWindow.end.toTime('ceil'),
     );
 
-    let selectedId: number | undefined = undefined;
     const selection = globals.selectionManager.selection;
-    switch (selection.kind) {
-      case 'track_event':
-        if (selection.trackUri === this.uri) {
-          selectedId = selection.eventId;
-        }
-        break;
-      case 'legacy':
-        const legacySelection = selection.legacySelection;
-        if (this.isSelectionHandled(legacySelection)) {
-          selectedId = (legacySelection as {id: number}).id;
-        }
-        break;
-    }
+    const selectedId =
+      selection.kind === 'track_event' && selection.trackUri === this.uri
+        ? selection.eventId
+        : undefined;
 
     if (selectedId === undefined) {
       this.selectedSlice = undefined;
diff --git a/ui/src/frontend/generic_slice_details_tab.ts b/ui/src/frontend/generic_slice_details_tab.ts
index 289e217..2389134 100644
--- a/ui/src/frontend/generic_slice_details_tab.ts
+++ b/ui/src/frontend/generic_slice_details_tab.ts
@@ -13,8 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {GenericSliceDetailsTabConfig} from '../public/details_panel';
-import {raf} from '../core/raf_scheduler';
+import {Columns, TrackEventDetailsPanel} from '../public/details_panel';
 import {ColumnType} from '../trace_processor/query_result';
 import {sqlValueToReadableString} from '../trace_processor/sql_utils';
 import {DetailsShell} from '../widgets/details_shell';
@@ -22,7 +21,7 @@
 import {Section} from '../widgets/section';
 import {SqlRef} from '../widgets/sql_ref';
 import {dictToTree, Tree, TreeNode} from '../widgets/tree';
-import {BottomTab, NewBottomTabArgs} from '../public/lib/bottom_tab';
+import {Trace} from '../public/trace';
 
 export {
   ColumnConfig,
@@ -34,41 +33,36 @@
 // A details tab, which fetches slice-like object from a given SQL table by id
 // and renders it according to the provided config, specifying which columns
 // need to be rendered and how.
-export class GenericSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.GenericSliceDetailsTab';
+export class GenericSliceDetailsTab implements TrackEventDetailsPanel {
+  private data?: {[key: string]: ColumnType};
 
-  data: {[key: string]: ColumnType} | undefined;
+  constructor(
+    private readonly trace: Trace,
+    private readonly sqlTableName: string,
+    private readonly id: number,
+    private readonly title: string,
+    private readonly columns?: Columns,
+  ) {}
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): GenericSliceDetailsTab {
-    return new GenericSliceDetailsTab(args);
+  async load() {
+    const result = await this.trace.engine.query(
+      `select * from ${this.sqlTableName} where id = ${this.id}`,
+    );
+
+    this.data = result.firstRow({});
   }
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-
-    this.engine
-      .query(
-        `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
-      )
-      .then((queryResult) => {
-        this.data = queryResult.firstRow({});
-        raf.scheduleFullRedraw();
-      });
-  }
-
-  viewTab() {
-    if (this.data === undefined) {
+  render() {
+    if (!this.data) {
       return m('h2', 'Loading');
     }
 
     const args: {[key: string]: m.Child} = {};
-    if (this.config.columns !== undefined) {
-      for (const key of Object.keys(this.config.columns)) {
+    if (this.columns !== undefined) {
+      for (const key of Object.keys(this.columns)) {
         let argKey = key;
-        if (this.config.columns[key].displayName !== undefined) {
-          argKey = this.config.columns[key].displayName!;
+        if (this.columns[key].displayName !== undefined) {
+          argKey = this.columns[key].displayName!;
         }
         args[argKey] = sqlValueToReadableString(this.data[key]);
       }
@@ -83,7 +77,7 @@
     return m(
       DetailsShell,
       {
-        title: this.config.title,
+        title: this.title,
       },
       m(
         GridLayout,
@@ -95,8 +89,8 @@
             m(TreeNode, {
               left: 'SQL ID',
               right: m(SqlRef, {
-                table: this.config.sqlTableName,
-                id: this.config.id,
+                table: this.sqlTableName,
+                id: this.id,
               }),
             }),
           ]),
@@ -104,12 +98,4 @@
       ),
     );
   }
-
-  getTitle(): string {
-    return this.config.title;
-  }
-
-  isLoading() {
-    return this.data === undefined;
-  }
 }
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index e4a9a46..b58e5f9 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -14,7 +14,6 @@
 
 import {assertExists} from '../base/logging';
 import {createStore, Store} from '../base/store';
-import {time} from '../base/time';
 import {Actions, DeferredAction} from '../common/actions';
 import {CommandManagerImpl} from '../core/command_manager';
 import {
@@ -37,13 +36,6 @@
 type DispatchMultiple = (actions: DeferredAction[]) => void;
 type TrackDataStore = Map<string, {}>;
 
-export interface QuantizedLoad {
-  start: time;
-  end: time;
-  load: number;
-}
-type OverviewStore = Map<string, QuantizedLoad[]>;
-
 export interface ThreadDesc {
   utid: number;
   tid: number;
@@ -68,7 +60,6 @@
 
   // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
   private _trackDataStore?: TrackDataStore = undefined;
-  private _overviewStore?: OverviewStore = undefined;
   private _threadMap?: ThreadMap = undefined;
   private _bufferUsage?: number = undefined;
   private _recordingLog?: string = undefined;
@@ -122,7 +113,6 @@
     // initialize() is only called ever once. (But then i'm going to kill this
     // entire file soon).
     this._trackDataStore = new Map<string, {}>();
-    this._overviewStore = new Map<string, QuantizedLoad[]>();
     this._threadMap = new Map<number, ThreadDesc>();
   }
 
@@ -188,10 +178,6 @@
     return this.trace.traceInfo;
   }
 
-  get overviewStore(): OverviewStore {
-    return assertExists(this._overviewStore);
-  }
-
   get trackDataStore(): TrackDataStore {
     return assertExists(this._trackDataStore);
   }
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index 3545ba1..719746a 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {Time, TimeSpan, time} from '../base/time';
+import {Duration, Time, TimeSpan, duration, time} from '../base/time';
 import {colorForCpu} from '../core/colorizer';
 import {timestampFormat, TimestampFormat} from '../core/timestamp_format';
 import {
@@ -25,7 +25,6 @@
 import {InnerDragStrategy} from './drag/inner_drag_strategy';
 import {OuterDragStrategy} from './drag/outer_drag_strategy';
 import {DragGestureHandler} from '../base/drag_gesture_handler';
-import {globals} from './globals';
 import {
   getMaxMajorTicks,
   MIN_PX_PER_STEP,
@@ -36,23 +35,37 @@
 import {Panel} from './panel_container';
 import {TimeScale} from '../base/time_scale';
 import {HighPrecisionTimeSpan} from '../base/high_precision_time_span';
+import {TraceImpl} from '../core/trace_impl';
+import {LONG, NUM} from '../trace_processor/query_result';
+import {raf} from '../core/raf_scheduler';
+import {getOrCreate} from '../base/utils';
+
+const tracesData = new WeakMap<TraceImpl, OverviewDataLoader>();
 
 export class OverviewTimelinePanel implements Panel {
   private static HANDLE_SIZE_PX = 5;
   readonly kind = 'panel';
   readonly selectable = false;
-
   private width = 0;
   private gesture?: DragGestureHandler;
   private timeScale?: TimeScale;
   private dragStrategy?: DragStrategy;
   private readonly boundOnMouseMove = this.onMouseMove.bind(this);
+  private readonly overviewData: OverviewDataLoader;
+
+  constructor(private trace: TraceImpl) {
+    this.overviewData = getOrCreate(
+      tracesData,
+      trace,
+      () => new OverviewDataLoader(trace),
+    );
+  }
 
   // Must explicitly type now; arguments types are no longer auto-inferred.
   // https://github.com/Microsoft/TypeScript/issues/1373
   onupdate({dom}: m.CVnodeDOM) {
     this.width = dom.getBoundingClientRect().width;
-    const traceTime = globals.traceContext;
+    const traceTime = this.trace.traceInfo;
     if (this.width > TRACK_SHELL_WIDTH) {
       const pxBounds = {left: TRACK_SHELL_WIDTH, right: this.width};
       const hpTraceTime = HighPrecisionTimeSpan.fromTime(
@@ -103,16 +116,17 @@
   renderCanvas(ctx: CanvasRenderingContext2D, size: Size2D) {
     if (this.width === undefined) return;
     if (this.timeScale === undefined) return;
+
     const headerHeight = 20;
     const tracksHeight = size.height - headerHeight;
     const traceContext = new TimeSpan(
-      globals.traceContext.start,
-      globals.traceContext.end,
+      this.trace.traceInfo.start,
+      this.trace.traceInfo.end,
     );
 
     if (size.width > TRACK_SHELL_WIDTH && traceContext.duration > 0n) {
       const maxMajorTicks = getMaxMajorTicks(this.width - TRACK_SHELL_WIDTH);
-      const offset = globals.trace.timeline.timestampOffset();
+      const offset = this.trace.timeline.timestampOffset();
       const tickGen = generateTicks(traceContext, maxMajorTicks, offset);
 
       // Draw time labels
@@ -124,7 +138,7 @@
         if (xPos > this.width) break;
         if (type === TickType.MAJOR) {
           ctx.fillRect(xPos - 1, 0, 1, headerHeight - 5);
-          const domainTime = globals.trace.timeline.toDomainTime(time);
+          const domainTime = this.trace.timeline.toDomainTime(time);
           renderTimestamp(ctx, domainTime, xPos + 5, 18, MIN_PX_PER_STEP);
         } else if (type == TickType.MEDIUM) {
           ctx.fillRect(xPos - 1, 0, 1, 8);
@@ -135,12 +149,13 @@
     }
 
     // Draw mini-tracks with quanitzed density for each process.
-    if (globals.overviewStore.size > 0) {
-      const numTracks = globals.overviewStore.size;
+    const overviewData = this.overviewData.overviewData;
+    if (overviewData.size > 0) {
+      const numTracks = overviewData.size;
       let y = 0;
       const trackHeight = (tracksHeight - 1) / numTracks;
-      for (const key of globals.overviewStore.keys()) {
-        const loads = globals.overviewStore.get(key)!;
+      for (const key of overviewData.keys()) {
+        const loads = overviewData.get(key)!;
         for (let i = 0; i < loads.length; i++) {
           const xStart = Math.floor(this.timeScale.timeToPx(loads[i].start));
           const xEnd = Math.ceil(this.timeScale.timeToPx(loads[i].end));
@@ -159,9 +174,7 @@
     ctx.fillRect(0, size.height - 1, this.width, 1);
 
     // Draw semi-opaque rects that occlude the non-visible time range.
-    const [vizStartPx, vizEndPx] = OverviewTimelinePanel.extractBounds(
-      this.timeScale,
-    );
+    const [vizStartPx, vizEndPx] = this.extractBounds(this.timeScale);
 
     ctx.fillStyle = OVERVIEW_TIMELINE_NON_VISIBLE_COLOR;
     ctx.fillRect(
@@ -203,9 +216,7 @@
 
   private chooseCursor(x: number) {
     if (this.timeScale === undefined) return 'default';
-    const [startBound, endBound] = OverviewTimelinePanel.extractBounds(
-      this.timeScale,
-    );
+    const [startBound, endBound] = this.extractBounds(this.timeScale);
     if (
       OverviewTimelinePanel.inBorderRange(x, startBound) ||
       OverviewTimelinePanel.inBorderRange(x, endBound)
@@ -227,7 +238,7 @@
 
   onDragStart(x: number) {
     if (this.timeScale === undefined) return;
-    const pixelBounds = OverviewTimelinePanel.extractBounds(this.timeScale);
+    const pixelBounds = this.extractBounds(this.timeScale);
     if (
       OverviewTimelinePanel.inBorderRange(x, pixelBounds[0]) ||
       OverviewTimelinePanel.inBorderRange(x, pixelBounds[1])
@@ -245,8 +256,8 @@
     this.dragStrategy = undefined;
   }
 
-  private static extractBounds(timeScale: TimeScale): [number, number] {
-    const vizTime = globals.timeline.visibleWindow;
+  private extractBounds(timeScale: TimeScale): [number, number] {
+    const vizTime = this.trace.timeline.visibleWindow;
     return [
       Math.floor(timeScale.hpTimeToPx(vizTime.start)),
       Math.ceil(timeScale.hpTimeToPx(vizTime.end)),
@@ -308,3 +319,126 @@
   const {dhhmmss} = timecode;
   ctx.fillText(dhhmmss, x, y, minWidth);
 }
+
+interface QuantizedLoad {
+  start: time;
+  end: time;
+  load: number;
+}
+
+// Kicks of a sequence of promises that load the overiew data in steps.
+// Each step schedules an animation frame.
+class OverviewDataLoader {
+  overviewData = new Map<string, QuantizedLoad[]>();
+
+  constructor(private trace: TraceImpl) {
+    this.beginLoad();
+  }
+
+  async beginLoad() {
+    const traceSpan = new TimeSpan(
+      this.trace.traceInfo.start,
+      this.trace.traceInfo.end,
+    );
+    const engine = this.trace.engine;
+    const stepSize = Duration.max(1n, traceSpan.duration / 100n);
+    const hasSchedSql = 'select ts from sched limit 1';
+    const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0;
+    if (hasSchedOverview) {
+      await this.loadSchedOverview(traceSpan, stepSize);
+    } else {
+      await this.loadSliceOverview(traceSpan, stepSize);
+    }
+  }
+
+  async loadSchedOverview(traceSpan: TimeSpan, stepSize: duration) {
+    const stepPromises = [];
+    for (
+      let start = traceSpan.start;
+      start < traceSpan.end;
+      start = Time.add(start, stepSize)
+    ) {
+      const progress = start - traceSpan.start;
+      const ratio = Number(progress) / Number(traceSpan.duration);
+      this.trace.omnibox.showStatusMessage(
+        'Loading overview ' + `${Math.round(ratio * 100)}%`,
+      );
+      const end = Time.add(start, stepSize);
+      // The (async() => {})() queues all the 100 async promises in one batch.
+      // Without that, we would wait for each step to be rendered before
+      // kicking off the next one. That would interleave an animation frame
+      // between each step, slowing down significantly the overall process.
+      stepPromises.push(
+        (async () => {
+          const schedResult = await this.trace.engine.query(
+            `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` +
+              `where ts >= ${start} and ts < ${end} and utid != 0 ` +
+              'group by cpu order by cpu',
+          );
+          const schedData: {[key: string]: QuantizedLoad} = {};
+          const it = schedResult.iter({load: NUM, cpu: NUM});
+          for (; it.valid(); it.next()) {
+            const load = it.load;
+            const cpu = it.cpu;
+            schedData[cpu] = {start, end, load};
+          }
+          this.appendData(schedData);
+        })(),
+      );
+    } // for(start = ...)
+    await Promise.all(stepPromises);
+  }
+
+  async loadSliceOverview(traceSpan: TimeSpan, stepSize: duration) {
+    // Slices overview.
+    const sliceResult = await this.trace.engine.query(`select
+            bucket,
+            upid,
+            ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
+          from thread
+          inner join (
+            select
+              ifnull(cast((ts - ${traceSpan.start})/${stepSize} as int), 0) as bucket,
+              sum(dur) as utid_sum,
+              utid
+            from slice
+            inner join thread_track on slice.track_id = thread_track.id
+            group by bucket, utid
+          ) using(utid)
+          where upid is not null
+          group by bucket, upid`);
+
+    const slicesData: {[key: string]: QuantizedLoad[]} = {};
+    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
+    for (; it.valid(); it.next()) {
+      const bucket = it.bucket;
+      const upid = it.upid;
+      const load = it.load;
+
+      const start = Time.add(traceSpan.start, stepSize * bucket);
+      const end = Time.add(start, stepSize);
+
+      const upidStr = upid.toString();
+      let loadArray = slicesData[upidStr];
+      if (loadArray === undefined) {
+        loadArray = slicesData[upidStr] = [];
+      }
+      loadArray.push({start, end, load});
+    }
+    this.appendData(slicesData);
+  }
+
+  appendData(data: {[key: string]: QuantizedLoad | QuantizedLoad[]}) {
+    for (const [key, value] of Object.entries(data)) {
+      if (!this.overviewData.has(key)) {
+        this.overviewData.set(key, []);
+      }
+      if (value instanceof Array) {
+        this.overviewData.get(key)!.push(...value);
+      } else {
+        this.overviewData.get(key)!.push(value);
+      }
+    }
+    raf.scheduleRedraw();
+  }
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index ab30a19..a32cb86 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -15,28 +15,7 @@
 import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
 import {raf} from '../core/raf_scheduler';
 import {HttpRpcState} from '../trace_processor/http_rpc_engine';
-import {globals, QuantizedLoad, ThreadDesc} from './globals';
-
-export function publishOverviewData(data: {
-  [key: string]: QuantizedLoad | QuantizedLoad[];
-}) {
-  for (const [key, value] of Object.entries(data)) {
-    if (!globals.overviewStore.has(key)) {
-      globals.overviewStore.set(key, []);
-    }
-    if (value instanceof Array) {
-      globals.overviewStore.get(key)!.push(...value);
-    } else {
-      globals.overviewStore.get(key)!.push(value);
-    }
-  }
-  raf.scheduleRedraw();
-}
-
-export function clearOverviewData() {
-  globals.overviewStore.clear();
-  raf.scheduleRedraw();
-}
+import {globals, ThreadDesc} from './globals';
 
 export function publishTrackData(args: {id: string; data: {}}) {
   globals.setTrackData(args.id, args.data);
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index 484b834..ccc5ceb 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -14,7 +14,6 @@
 
 import {TrackContext} from '../public/track';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from './tracks/custom_sql_table_slice_track';
@@ -23,10 +22,7 @@
   SqlDataSource,
 } from '../public/lib/debug_tracks/debug_tracks';
 import {uuidv4Sql} from '../base/uuid';
-import {
-  ARG_PREFIX,
-  DebugSliceDetailsTab,
-} from '../public/lib/debug_tracks/details_tab';
+import {ARG_PREFIX} from '../public/lib/debug_tracks/details_tab';
 import {createPerfettoTable} from '../trace_processor/sql_utils';
 import {Trace} from '../public/trace';
 
@@ -38,7 +34,7 @@
 
 export class SimpleSliceTrack extends CustomSqlTableSliceTrack {
   private config: SimpleSliceTrackConfig;
-  private sqlTableName: string;
+  public readonly sqlTableName: string;
 
   constructor(trace: Trace, ctx: TrackContext, config: SimpleSliceTrackConfig) {
     super({
@@ -66,18 +62,6 @@
     };
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    // We currently borrow the debug slice details tab.
-    // TODO: Don't do this!
-    return {
-      kind: DebugSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.sqlTableName,
-        title: 'Debug Slice',
-      },
-    };
-  }
-
   private createTableQuery(
     data: SqlDataSource,
     sliceColumns: SliceColumns,
diff --git a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index f97142b..6b7fa59 100644
--- a/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -13,10 +13,7 @@
 // limitations under the License.
 
 import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
-import {LegacySelection} from '../../public/selection';
-import {OnSliceClickArgs} from '../base_slice_track';
 import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
-import {globals} from '../globals';
 import {NAMED_ROW, NamedRow, NamedSliceTrack} from '../named_slice_track';
 import {NewTrackArgs} from '../track';
 import {createView} from '../../trace_processor/sql_utils';
@@ -69,11 +66,6 @@
     | CustomSqlTableDefConfig
     | Promise<CustomSqlTableDefConfig>;
 
-  // Override by subclasses.
-  abstract getDetailsPanel(
-    args: OnSliceClickArgs<Slice>,
-  ): CustomSqlDetailsPanelConfig;
-
   getSqlImports(): CustomSqlImportConfig {
     return {
       modules: [] as string[],
@@ -109,32 +101,6 @@
     return `SELECT * FROM ${this.tableName}`;
   }
 
-  isSelectionHandled(selection: LegacySelection) {
-    if (selection.kind !== 'GENERIC_SLICE') {
-      return false;
-    }
-    return selection.trackUri === this.uri;
-  }
-
-  onSliceClick(args: OnSliceClickArgs<Slice>) {
-    if (this.getDetailsPanel(args) === undefined) {
-      return;
-    }
-
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    globals.selectionManager.selectGenericSlice({
-      id: args.slice.id,
-      sqlTableName: this.tableName,
-      start: args.slice.ts,
-      duration: args.slice.dur,
-      trackUri: this.uri,
-      detailsPanelConfig: {
-        kind: detailsPanelConfig.kind,
-        config: detailsPanelConfig.config,
-      },
-    });
-  }
-
   async loadImports() {
     for (const importModule of this.getSqlImports().modules) {
       await this.engine.query(`INCLUDE PERFETTO MODULE ${importModule};`);
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 46908d9..68645c4 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -91,7 +91,7 @@
   // Used to prevent global deselection if a pan/drag select occurred.
   private keepCurrentSelection = false;
 
-  private overviewTimelinePanel = new OverviewTimelinePanel();
+  private overviewTimelinePanel: OverviewTimelinePanel;
   private timeAxisPanel = new TimeAxisPanel();
   private timeSelectionPanel = new TimeSelectionPanel();
   private notesPanel = new NotesPanel();
@@ -102,6 +102,7 @@
 
   constructor(vnode: m.CVnode<PageWithTraceAttrs>) {
     this.tickmarkPanel = new TickmarkPanel(vnode.attrs.trace);
+    this.overviewTimelinePanel = new OverviewTimelinePanel(vnode.attrs.trace);
   }
 
   oncreate(vnode: m.CVnodeDOM<PageWithTraceAttrs>) {
diff --git a/ui/src/plugins/com.android.InputEvents/index.ts b/ui/src/plugins/com.android.InputEvents/index.ts
index 7a529d3..9116ad3 100644
--- a/ui/src/plugins/com.android.InputEvents/index.ts
+++ b/ui/src/plugins/com.android.InputEvents/index.ts
@@ -21,6 +21,7 @@
 } from '../../frontend/simple_slice_track';
 import {TrackNode} from '../../public/workspace';
 import {getOrCreateUserInteractionGroup} from '../../public/standard_groups';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
 
 class InputEvents implements PerfettoPlugin {
   private readonly SQL_SOURCE = `
@@ -33,12 +34,12 @@
     `;
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    const cnt = await(ctx.engine.query(`
+    const cnt = await ctx.engine.query(`
       SELECT
         count(*) as cnt
       FROM slice
       WHERE name GLOB 'UnwantedInteractionBlocker::notifyMotion*'
-    `));
+    `);
     if (cnt.firstRow({cnt: LONG}).cnt == 0n) {
       return;
     }
@@ -51,13 +52,16 @@
       columns: {ts: 'ts', dur: 'dur', name: 'name'},
       argColumns: [],
     };
-    await ctx.engine.query("INCLUDE PERFETTO MODULE android.input;");
+    await ctx.engine.query('INCLUDE PERFETTO MODULE android.input;');
     const uri = 'com.android.InputEvents#InputEventsTrack';
     const title = 'Input Events';
+    const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
     ctx.tracks.registerTrack({
       uri,
       title: title,
-      track: new SimpleSliceTrack(ctx, {trackUri: uri}, config)
+      track,
+      detailsPanel: ({eventId}) =>
+        new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
     });
     const node = new TrackNode({uri, title});
     const group = getOrCreateUserInteractionGroup(ctx.workspace);
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 6a2dd1e..0968acd 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -25,6 +25,7 @@
   SimpleCounterTrackConfig,
 } from '../../frontend/simple_counter_track';
 import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
 
 interface ContainedTrace {
   uuid: string;
@@ -1184,13 +1185,16 @@
     };
 
     const uri = `/long_battery_tracing_${name}`;
+    const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
     ctx.tracks.registerTrack({
       uri,
       title: name,
-      track: new SimpleSliceTrack(ctx, {trackUri: uri}, config),
+      track,
+      detailsPanel: ({eventId}) =>
+        new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
     });
-    const track = new TrackNode({uri, title: name});
-    this.addTrack(ctx, track, groupName);
+    const trackNode = new TrackNode({uri, title: name});
+    this.addTrack(ctx, trackNode, groupName);
   }
 
   addCounterTrack(
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index e734528..a53dd2a 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -20,6 +20,7 @@
   SimpleSliceTrackConfig,
 } from '../../frontend/simple_slice_track';
 import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
 class AndroidStartup implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
     const e = ctx.engine;
@@ -43,13 +44,16 @@
     };
     const uri = `/android_startups`;
     const title = 'Android App Startups';
+    const track = new SimpleSliceTrack(ctx, {trackUri: uri}, config);
     ctx.tracks.registerTrack({
       uri,
       title: 'Android App Startups',
-      track: new SimpleSliceTrack(ctx, {trackUri: uri}, config),
+      track,
+      detailsPanel: ({eventId}) =>
+        new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
     });
-    const track = new TrackNode({title, uri});
-    ctx.workspace.addChildInOrder(track);
+    const trackNode = new TrackNode({title, uri});
+    ctx.workspace.addChildInOrder(trackNode);
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 4199c44..fc660e2 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -17,6 +17,7 @@
 import {PerfettoPlugin, PluginDescriptor} from '../../public/plugin';
 import {SimpleSliceTrack} from '../../frontend/simple_slice_track';
 import {TrackNode} from '../../public/workspace';
+import {DebugSliceDetailsPanel} from '../../public/lib/debug_tracks/details_tab';
 class TraceMetadata implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
     const res = await ctx.engine.query(`
@@ -28,27 +29,30 @@
     }
     const uri = `/clock_snapshots`;
     const title = 'Clock Snapshots';
+    const track = new SimpleSliceTrack(
+      ctx,
+      {trackUri: uri},
+      {
+        data: {
+          sqlSource: `
+            select ts, 0 as dur, 'Snapshot' as name
+            from clock_snapshot
+          `,
+          columns: ['ts', 'dur', 'name'],
+        },
+        columns: {ts: 'ts', dur: 'dur', name: 'name'},
+        argColumns: [],
+      },
+    );
     ctx.tracks.registerTrack({
       uri,
       title,
-      track: new SimpleSliceTrack(
-        ctx,
-        {trackUri: uri},
-        {
-          data: {
-            sqlSource: `
-              select ts, 0 as dur, 'Snapshot' as name
-              from clock_snapshot
-            `,
-            columns: ['ts', 'dur', 'name'],
-          },
-          columns: {ts: 'ts', dur: 'dur', name: 'name'},
-          argColumns: [],
-        },
-      ),
+      track,
+      detailsPanel: ({eventId}) =>
+        new DebugSliceDetailsPanel(ctx, track.sqlTableName, eventId),
     });
-    const track = new TrackNode({uri, title});
-    ctx.workspace.addChildInOrder(track);
+    const trackNode = new TrackNode({uri, title});
+    ctx.workspace.addChildInOrder(trackNode);
   }
 }
 
diff --git a/ui/src/protos/index.ts b/ui/src/protos/index.ts
index 32f020b..6782599 100644
--- a/ui/src/protos/index.ts
+++ b/ui/src/protos/index.ts
@@ -71,8 +71,8 @@
 import QueryServiceStateResponse = protos.perfetto.protos.QueryServiceStateResponse;
 import ReadBuffersRequest = protos.perfetto.protos.ReadBuffersRequest;
 import ReadBuffersResponse = protos.perfetto.protos.ReadBuffersResponse;
-import RegisterSqlModuleArgs = protos.perfetto.protos.RegisterSqlModuleArgs;
-import RegisterSqlModuleResult = protos.perfetto.protos.RegisterSqlModuleResult;
+import RegisterSqlPackageArgs = protos.perfetto.protos.RegisterSqlPackageArgs;
+import RegisterSqlPackageResult = protos.perfetto.protos.RegisterSqlPackageResult;
 import ResetTraceProcessorArgs = protos.perfetto.protos.ResetTraceProcessorArgs;
 import StatCounters = protos.perfetto.protos.SysStatsConfig.StatCounters;
 import StatusResult = protos.perfetto.protos.StatusResult;
@@ -140,8 +140,8 @@
   QueryServiceStateResponse,
   ReadBuffersRequest,
   ReadBuffersResponse,
-  RegisterSqlModuleArgs,
-  RegisterSqlModuleResult,
+  RegisterSqlPackageArgs,
+  RegisterSqlPackageResult,
   ResetTraceProcessorArgs,
   StatCounters,
   StatusResult,
diff --git a/ui/src/public/lib/debug_tracks/debug_tracks.ts b/ui/src/public/lib/debug_tracks/debug_tracks.ts
index f85290f..387b8f2 100644
--- a/ui/src/public/lib/debug_tracks/debug_tracks.ts
+++ b/ui/src/public/lib/debug_tracks/debug_tracks.ts
@@ -19,9 +19,10 @@
   sqlValueToReadableString,
 } from '../../../trace_processor/sql_utils';
 import {DebugCounterTrack} from './counter_track';
-import {ARG_PREFIX} from './details_tab';
+import {ARG_PREFIX, DebugSliceDetailsPanel} from './details_tab';
 import {TrackNode} from '../../workspace';
 import {Trace} from '../../trace';
+import {TrackEventSelection} from '../../selection';
 
 let trackCounter = 0; // For reproducible ids.
 
@@ -123,6 +124,9 @@
     uri,
     title: trackName,
     track: new DebugSliceTrack(trace, {trackUri: uri}, tableName),
+    detailsPanel: (sel: TrackEventSelection) => {
+      return new DebugSliceDetailsPanel(trace, tableName, sel.eventId);
+    },
   });
 
   // Create the actions to add this track to the tracklist
diff --git a/ui/src/public/lib/debug_tracks/details_tab.ts b/ui/src/public/lib/debug_tracks/details_tab.ts
index 615f842..fe7c61e 100644
--- a/ui/src/public/lib/debug_tracks/details_tab.ts
+++ b/ui/src/public/lib/debug_tracks/details_tab.ts
@@ -14,8 +14,6 @@
 
 import m from 'mithril';
 import {duration, Time, time} from '../../../base/time';
-import {BottomTab, NewBottomTabArgs} from '../bottom_tab';
-import {GenericSliceDetailsTabConfig} from '../../../frontend/generic_slice_details_tab';
 import {hasArgs, renderArguments} from '../../../frontend/slice_args';
 import {getSlice, SliceDetails} from '../../../trace_processor/sql_utils/slice';
 import {
@@ -49,6 +47,8 @@
 import {getThreadName} from '../../../trace_processor/sql_utils/thread';
 import {getProcessName} from '../../../trace_processor/sql_utils/process';
 import {sliceRef} from '../../../frontend/widgets/slice';
+import {TrackEventDetailsPanel} from '../../details_panel';
+import {Trace} from '../../trace';
 
 export const ARG_PREFIX = 'arg_';
 
@@ -78,10 +78,8 @@
   return children;
 }
 
-export class DebugSliceDetailsTab extends BottomTab<GenericSliceDetailsTabConfig> {
-  static readonly kind = 'dev.perfetto.DebugSliceDetailsTab';
-
-  data?: {
+export class DebugSliceDetailsPanel implements TrackEventDetailsPanel {
+  private data?: {
     name: string;
     ts: time;
     dur: duration;
@@ -91,14 +89,14 @@
   // tables. These values will be set if the relevant columns exist and
   // are consistent (e.g. 'ts' and 'dur' for this slice correspond to values
   // in these well-known tables).
-  threadState?: ThreadState;
-  slice?: SliceDetails;
+  private threadState?: ThreadState;
+  private slice?: SliceDetails;
 
-  static create(
-    args: NewBottomTabArgs<GenericSliceDetailsTabConfig>,
-  ): DebugSliceDetailsTab {
-    return new DebugSliceDetailsTab(args);
-  }
+  constructor(
+    private readonly trace: Trace,
+    private readonly tableName: string,
+    private readonly eventId: number,
+  ) {}
 
   private async maybeLoadThreadState(
     id: number | undefined,
@@ -110,7 +108,7 @@
     if (id === undefined) return undefined;
     if (utid === undefined) return undefined;
 
-    const threadState = await getThreadState(this.engine, id);
+    const threadState = await getThreadState(this.trace.engine, id);
     if (threadState === undefined) return undefined;
     if (
       table === 'thread_state' ||
@@ -150,7 +148,7 @@
     if (id === undefined) return undefined;
     if (table !== 'slice' && trackId === undefined) return undefined;
 
-    const slice = await getSlice(this.engine, asSliceSqlId(id));
+    const slice = await getSlice(this.trace.engine, asSliceSqlId(id));
     if (slice === undefined) return undefined;
     if (
       table === 'slice' ||
@@ -193,9 +191,9 @@
     );
   }
 
-  private async loadData() {
-    const queryResult = await this.engine.query(
-      `select * from ${this.config.sqlTableName} where id = ${this.config.id}`,
+  async load() {
+    const queryResult = await this.trace.engine.query(
+      `select * from ${this.tableName} where id = ${this.eventId}`,
     );
     const row = queryResult.firstRow({
       ts: LONG,
@@ -237,12 +235,7 @@
     this.trace.scheduleRedraw();
   }
 
-  constructor(args: NewBottomTabArgs<GenericSliceDetailsTabConfig>) {
-    super(args);
-    this.loadData();
-  }
-
-  viewTab() {
+  render() {
     if (this.data === undefined) {
       return m('h2', 'Loading');
     }
@@ -250,7 +243,7 @@
       'Name': this.data['name'] as string,
       'Start time': m(Timestamp, {ts: timeFromSql(this.data['ts'])}),
       'Duration': m(DurationWidget, {dur: durationFromSql(this.data['dur'])}),
-      'Debug slice id': `${this.config.sqlTableName}[${this.config.id}]`,
+      'Debug slice id': `${this.tableName}[${this.eventId}]`,
     });
     details.push(this.renderThreadStateInfo());
     details.push(this.renderSliceInfo());
diff --git a/ui/src/public/lib/debug_tracks/slice_track.ts b/ui/src/public/lib/debug_tracks/slice_track.ts
index 4cfc74f..214f32b 100644
--- a/ui/src/public/lib/debug_tracks/slice_track.ts
+++ b/ui/src/public/lib/debug_tracks/slice_track.ts
@@ -14,12 +14,10 @@
 
 import m from 'mithril';
 import {
-  CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
 } from '../../../frontend/tracks/custom_sql_table_slice_track';
 import {TrackContext} from '../../track';
-import {DebugSliceDetailsTab} from './details_tab';
 import {Button} from '../../../widgets/button';
 import {Icons} from '../../../base/semantic_icons';
 import {Trace} from '../../trace';
@@ -41,16 +39,6 @@
     };
   }
 
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: DebugSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.sqlTableName,
-        title: 'Debug Slice',
-      },
-    };
-  }
-
   getTrackShellButtons(): m.Children {
     return m(Button, {
       onclick: () => {
diff --git a/ui/src/public/selection.ts b/ui/src/public/selection.ts
index 4965910..69a1a87 100644
--- a/ui/src/public/selection.ts
+++ b/ui/src/public/selection.ts
@@ -16,12 +16,10 @@
 import {Optional} from '../base/utils';
 import {Engine} from '../trace_processor/engine';
 import {ColumnDef, Sorting, ThreadStateExtra} from './aggregation';
-import {GenericSliceDetailsTabConfigBase} from './details_panel';
 import {TrackDescriptor} from './track';
 
 export interface SelectionManager {
   readonly selection: Selection;
-  readonly legacySelection: LegacySelection | null;
 
   findTimeRangeOfSelection(): Optional<TimeSpan>;
   clear(): void;
@@ -49,14 +47,6 @@
   selectSqlEvent(sqlTableName: string, id: number, opts?: SelectionOpts): void;
 
   /**
-   * Select a legacy selection.
-   *
-   * @param selection - The legacy selection to select.
-   * @param opts - Additional options.
-   */
-  selectLegacy(selection: LegacySelection, opts?: SelectionOpts): void;
-
-  /**
    * Create an area selection for the purposes of aggregation.
    *
    * @param args - The area to select.
@@ -67,20 +57,6 @@
   scrollToCurrentSelection(): void;
   registerAreaSelectionAggreagtor(aggr: AreaSelectionAggregator): void;
 
-  // TODO(primiano): I don't undertsand what this generic slice is, but now
-  // is exposed to plugins. For now i'm just carrying it forward.
-  selectGenericSlice(args: {
-    id: number;
-    sqlTableName: string;
-    start: time;
-    duration: duration;
-    trackUri: string;
-    detailsPanelConfig: {
-      kind: string;
-      config: GenericSliceDetailsTabConfigBase;
-    };
-  }): void;
-
   /**
    * Register a new SQL selection resolver.
    *
@@ -108,8 +84,7 @@
   | AreaSelection
   | NoteSelection
   | UnionSelection
-  | EmptySelection
-  | LegacySelectionWrapper;
+  | EmptySelection;
 
 /** Defines how changes to selection affect the rest of the UI state */
 export interface SelectionOpts {
@@ -118,32 +93,6 @@
   scrollToSelection?: boolean; // Default: false.
 }
 
-// LEGACY Selection types:
-
-export interface LegacySelectionWrapper {
-  readonly kind: 'legacy';
-  readonly legacySelection: LegacySelection;
-}
-
-export type LegacySelection = GenericSliceSelection & {
-  trackUri?: string;
-};
-
-export interface GenericSliceSelection {
-  readonly kind: 'GENERIC_SLICE';
-  readonly id: number;
-  readonly sqlTableName: string;
-  readonly start: time;
-  readonly duration: duration;
-  // NOTE: this config can be expanded for multiple details panel types.
-  readonly detailsPanelConfig: {
-    readonly kind: string;
-    readonly config: GenericSliceDetailsTabConfigBase;
-  };
-}
-
-// New Selection types:
-
 export interface TrackEventSelection extends TrackEventDetails {
   readonly kind: 'track_event';
   readonly trackUri: string;
@@ -165,6 +114,7 @@
   readonly utid?: number;
   readonly tableName?: string;
   readonly profileType?: ProfileType;
+  readonly interactionType?: string;
 }
 
 export interface Area {
diff --git a/ui/src/public/track.ts b/ui/src/public/track.ts
index 5f32e6b..d12faed 100644
--- a/ui/src/public/track.ts
+++ b/ui/src/public/track.ts
@@ -106,7 +106,7 @@
   // Optional: A factory that returns a details panel object. This is called
   // each time the selection is changed (and the selection is relevant to this
   // track).
-  readonly detailsPanel?: (id: TrackEventSelection) => TrackEventDetailsPanel;
+  readonly detailsPanel?: (sel: TrackEventSelection) => TrackEventDetailsPanel;
 }
 
 /**
diff --git a/ui/src/public/track_kinds.ts b/ui/src/public/track_kinds.ts
index 05671c9..e5df1ae 100644
--- a/ui/src/public/track_kinds.ts
+++ b/ui/src/public/track_kinds.ts
@@ -27,12 +27,4 @@
 export const CPUSS_ESTIMATE_TRACK_KIND = 'CpuSubsystemEstimateTrack';
 export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
 export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
-export const CHROME_TOPLEVEL_SCROLLS_KIND =
-  'org.chromium.TopLevelScrolls.scrolls';
-export const CHROME_EVENT_LATENCY_TRACK_KIND =
-  'org.chromium.ScrollJank.event_latencies';
-export const SCROLL_JANK_V3_TRACK_KIND =
-  'org.chromium.ScrollJank.scroll_jank_v3_track';
-export const CHROME_SCROLL_JANK_TRACK_KIND =
-  'org.chromium.ScrollJank.BrowserUIThreadLongTasks';
 export const ANDROID_LOGS_TRACK_KIND = 'AndroidLogTrack';
diff --git a/ui/src/public/utils.ts b/ui/src/public/utils.ts
index 6e6fe02..208961a 100644
--- a/ui/src/public/utils.ts
+++ b/ui/src/public/utils.ts
@@ -13,11 +13,9 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {LegacySelection, Selection} from '../public/selection';
 import {BottomTab} from './lib/bottom_tab';
 import {Tab} from './tab';
 import {exists} from '../base/utils';
-import {DetailsPanel} from './details_panel';
 import {Trace} from './trace';
 import {TimeSpan} from '../base/time';
 
@@ -98,10 +96,6 @@
   return 'Unknown';
 }
 
-export interface BottomTabAdapterAttrs {
-  tabFactory: (sel: LegacySelection) => BottomTab | undefined;
-}
-
 /**
  * This adapter wraps a BottomTab, converting it into a the new "current
  * selection" API.
@@ -130,34 +124,6 @@
       },
     })
  */
-export class BottomTabToSCSAdapter implements DetailsPanel {
-  private oldSelection?: Selection;
-  private bottomTab?: BottomTab;
-  private attrs: BottomTabAdapterAttrs;
-
-  constructor(attrs: BottomTabAdapterAttrs) {
-    this.attrs = attrs;
-  }
-
-  render(selection: Selection): m.Children {
-    // Detect selection changes, assuming selection is immutable
-    if (selection !== this.oldSelection) {
-      this.oldSelection = selection;
-      if (selection.kind === 'legacy') {
-        this.bottomTab = this.attrs.tabFactory(selection.legacySelection);
-      } else {
-        this.bottomTab = undefined;
-      }
-    }
-
-    return this.bottomTab?.renderPanel();
-  }
-
-  // Note: Must be called after render()
-  isLoading(): boolean {
-    return this.bottomTab?.isLoading() ?? false;
-  }
-}
 
 /**
  * This adapter wraps a BottomTab, converting it to work with the Tab API.
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 26197f1..7d4af6d 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -22,7 +22,7 @@
   MetatraceCategories,
   QueryArgs,
   QueryResult as ProtoQueryResult,
-  RegisterSqlModuleArgs,
+  RegisterSqlPackageArgs,
   ResetTraceProcessorArgs,
   TraceProcessorRpc,
   TraceProcessorRpcStream,
@@ -127,7 +127,7 @@
   private pendingRestoreTables = new Array<Deferred<void>>();
   private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
   private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
-  private pendingRegisterSqlModule?: Deferred<void>;
+  private pendingRegisterSqlPackage?: Deferred<void>;
   private _isMetatracingEnabled = false;
   private _numRequestsPending = 0;
   private _failed: Optional<string> = undefined;
@@ -265,9 +265,9 @@
         assertExists(this.pendingReadMetatrace).resolve(metatraceRes);
         this.pendingReadMetatrace = undefined;
         break;
-      case TPM.TPM_REGISTER_SQL_MODULE:
-        const registerResult = assertExists(rpc.registerSqlModuleResult);
-        const res = assertExists(this.pendingRegisterSqlModule);
+      case TPM.TPM_REGISTER_SQL_PACKAGE:
+        const registerResult = assertExists(rpc.registerSqlPackageResult);
+        const res = assertExists(this.pendingRegisterSqlPackage);
         if (exists(registerResult.error) && registerResult.error.length > 0) {
           res.reject(registerResult.error);
         } else {
@@ -476,23 +476,23 @@
     return result;
   }
 
-  registerSqlModules(p: {
+  registerSqlPackages(p: {
     name: string;
     modules: {name: string; sql: string}[];
   }): Promise<void> {
-    if (this.pendingRegisterSqlModule) {
+    if (this.pendingRegisterSqlPackage) {
       return Promise.reject(new Error('Already finalising a metatrace'));
     }
 
     const result = defer<void>();
 
     const rpc = TraceProcessorRpc.create();
-    rpc.request = TPM.TPM_REGISTER_SQL_MODULE;
-    const args = (rpc.registerSqlModuleArgs = new RegisterSqlModuleArgs());
-    args.topLevelPackageName = p.name;
+    rpc.request = TPM.TPM_REGISTER_SQL_PACKAGE;
+    const args = (rpc.registerSqlPackageArgs = new RegisterSqlPackageArgs());
+    args.packageName = p.name;
     args.modules = p.modules;
-    args.allowModuleOverride = true;
-    this.pendingRegisterSqlModule = result;
+    args.allowOverride = true;
+    this.pendingRegisterSqlPackage = result;
     this.rpcSendRequest(rpc);
     return result;
   }