Merge "Revert "tp: improve handling of !usable constraints in BestIndex"" into main
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..a35fb26
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+ui
\ No newline at end of file
diff --git a/Android.bp b/Android.bp
index ee7f589..c46b914 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1318,8 +1318,8 @@
 }
 
 // GN: [//protos/perfetto/config:source_set]
-java_library {
-    name: "perfetto_config_java_protos",
+filegroup {
+    name: "perfetto_config_filegroup_proto",
     srcs: [
         "protos/perfetto/common/android_energy_consumer_descriptor.proto",
         "protos/perfetto/common/android_log_constants.proto",
@@ -1375,10 +1375,6 @@
         "protos/perfetto/config/trace_config.proto",
         "protos/perfetto/config/track_event/track_event_config.proto",
     ],
-    proto: {
-        type: "lite",
-        canonical_path_from_root: false,
-    },
 }
 
 // GN: //test/cts:perfetto_cts_deps
@@ -17265,6 +17261,38 @@
     output_extension: "srcjar",
 }
 
+java_library {
+    name: "perfetto_config_java_protos",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+}
+
+java_library {
+    name: "perfetto_config_java_protos_system_server_current",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    sdk_version: "system_server_current",
+    apex_available: [
+        "com.android.profiling",
+    ],
+}
+
 prebuilt_etc {
     name: "perfetto_persistent_cfg.pbtxt",
     filename: "persistent_cfg.pbtxt",
diff --git a/Android.bp.extras b/Android.bp.extras
index 75d3a81..56cfcdd 100644
--- a/Android.bp.extras
+++ b/Android.bp.extras
@@ -198,6 +198,38 @@
     output_extension: "srcjar",
 }
 
+java_library {
+    name: "perfetto_config_java_protos",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+}
+
+java_library {
+    name: "perfetto_config_java_protos_system_server_current",
+    srcs: [
+        ":perfetto_config_filegroup_proto",
+    ],
+    static_libs: [
+        "libprotobuf-java-lite",
+    ],
+    proto: {
+        type: "lite",
+        canonical_path_from_root: false,
+    },
+    sdk_version: "system_server_current",
+    apex_available: [
+        "com.android.profiling",
+    ],
+}
+
 prebuilt_etc {
     name: "perfetto_persistent_cfg.pbtxt",
     filename: "persistent_cfg.pbtxt",
diff --git a/CHANGELOG b/CHANGELOG
index ae94f5f..70cb79c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,6 @@
 Unreleased:
   Tracing service and probes:
-    *
+    * 
   SQL Standard library:
     * Added megacycles support to CPU package. Added tables:
       `cpu_cycles_per_process`, `cpu_cycles_per_thread` and
@@ -12,7 +12,10 @@
   UI:
     *
   SDK:
-    *
+    * The TRACE_COUNTER macro and CounterTrack constructor no longer accept
+      `const char *` track names. In case your code fails to compile,
+      https://perfetto.dev/docs/instrumentation/track-events#dynamic-event-names
+      explains how to fix the problem.
 
 
 v45.0 - 2024-05-09:
diff --git a/include/perfetto/tracing/string_helpers.h b/include/perfetto/tracing/string_helpers.h
index 0d2819a..03fa9e4 100644
--- a/include/perfetto/tracing/string_helpers.h
+++ b/include/perfetto/tracing/string_helpers.h
@@ -38,6 +38,8 @@
 
   constexpr explicit StaticString(const char* str) : value(str) {}
 
+  operator bool() const { return !!value; }
+
   const char* value;
 };
 
@@ -52,6 +54,9 @@
     length = strlen(str);
   }
   DynamicString(const char* str, size_t len) : value(str), length(len) {}
+  constexpr DynamicString() : value(nullptr), length(0) {}
+
+  operator bool() const { return !!value; }
 
   const char* value;
   size_t length;
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index 367e10a..831c290 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -202,7 +202,7 @@
       perfetto::protos::gen::CounterDescriptor::BuiltinCounterType;
 
   // |name| must outlive this object.
-  constexpr explicit CounterTrack(const char* name,
+  constexpr explicit CounterTrack(StaticString name,
                                   Track parent = MakeProcessTrack())
       : CounterTrack(
             name,
@@ -255,33 +255,39 @@
   }
 
   constexpr CounterTrack set_unit(Unit unit) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit, unit_name_,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit, unit_name_, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_type(CounterType type) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier_, is_incremental_, type);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier_,
+                        is_incremental_, type);
   }
 
   constexpr CounterTrack set_unit_name(const char* unit_name) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_unit_multiplier(int64_t unit_multiplier) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_category(const char* category) const {
-    return CounterTrack(uuid, parent_uuid, name_, category, unit_, unit_name_,
-                        unit_multiplier_, is_incremental_, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category, unit_, unit_name_, unit_multiplier_,
+                        is_incremental_, type_);
   }
 
   constexpr CounterTrack set_is_incremental(bool is_incremental = true) const {
-    return CounterTrack(uuid, parent_uuid, name_, category_, unit_, unit_name_,
-                        unit_multiplier_, is_incremental, type_);
+    return CounterTrack(uuid, parent_uuid, static_name_, dynamic_name_,
+                        category_, unit_, unit_name_, unit_multiplier_,
+                        is_incremental, type_);
   }
 
   constexpr bool is_incremental() const { return is_incremental_; }
@@ -290,12 +296,12 @@
   protos::gen::TrackDescriptor Serialize() const;
 
  private:
-  constexpr CounterTrack(const char* name,
+  constexpr CounterTrack(StaticString name,
                          Unit unit,
                          const char* unit_name,
                          Track parent)
-      : Track(internal::Fnv1a(name) ^ kCounterMagic, parent),
-        name_(name),
+      : Track(internal::Fnv1a(name.value) ^ kCounterMagic, parent),
+        static_name_(name),
         category_(nullptr),
         unit_(unit),
         unit_name_(unit_name) {}
@@ -304,13 +310,15 @@
                const char* unit_name,
                Track parent)
       : Track(internal::Fnv1a(name.value, name.length) ^ kCounterMagic, parent),
-        name_(name.value),
+        static_name_(nullptr),
+        dynamic_name_(name),
         category_(nullptr),
         unit_(unit),
         unit_name_(unit_name) {}
   constexpr CounterTrack(uint64_t uuid_,
                          uint64_t parent_uuid_,
-                         const char* name,
+                         StaticString static_name,
+                         DynamicString dynamic_name,
                          const char* category,
                          Unit unit,
                          const char* unit_name,
@@ -318,7 +326,8 @@
                          bool is_incremental,
                          CounterType type)
       : Track(uuid_, parent_uuid_),
-        name_(name),
+        static_name_(static_name),
+        dynamic_name_(dynamic_name),
         category_(category),
         unit_(unit),
         unit_name_(unit_name),
@@ -326,7 +335,8 @@
         is_incremental_(is_incremental),
         type_(type) {}
 
-  const char* const name_;
+  StaticString static_name_;
+  DynamicString dynamic_name_;
   const char* const category_;
   Unit unit_ = perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED;
   const char* const unit_name_ = nullptr;
diff --git a/protos/perfetto/trace/perfetto_trace.proto b/protos/perfetto/trace/perfetto_trace.proto
index c296aea..20612e4 100644
--- a/protos/perfetto/trace/perfetto_trace.proto
+++ b/protos/perfetto/trace/perfetto_trace.proto
@@ -14676,7 +14676,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 10.
+// Next id: 11.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -14696,7 +14696,12 @@
   // Name of the track. Optional - if unspecified, it may be derived from the
   // process/thread name (process/thread tracks), the first event's name (async
   // tracks), or counter name (counter tracks).
-  optional string name = 2;
+  oneof static_or_dynamic_name {
+    string name = 2;
+    // This field is only set by the SDK when perfetto::StaticString is
+    // provided.
+    string static_name = 10;
+  }
 
   // Associate the track with a process, making it the process-global track.
   // There should only be one such track per process (usually for instant
diff --git a/protos/perfetto/trace/track_event/track_descriptor.proto b/protos/perfetto/trace/track_event/track_descriptor.proto
index d6db233..76890f2 100644
--- a/protos/perfetto/trace/track_event/track_descriptor.proto
+++ b/protos/perfetto/trace/track_event/track_descriptor.proto
@@ -37,7 +37,7 @@
 // |TrackEvent::track_uuid|. It is possible but not necessary to emit a
 // TrackDescriptor for this implicit track.
 //
-// Next id: 10.
+// Next id: 11.
 message TrackDescriptor {
   // Unique ID that identifies this track. This ID is global to the whole trace.
   // Producers should ensure that it is unlikely to clash with IDs emitted by
@@ -57,7 +57,12 @@
   // Name of the track. Optional - if unspecified, it may be derived from the
   // process/thread name (process/thread tracks), the first event's name (async
   // tracks), or counter name (counter tracks).
-  optional string name = 2;
+  oneof static_or_dynamic_name {
+    string name = 2;
+    // This field is only set by the SDK when perfetto::StaticString is
+    // provided.
+    string static_name = 10;
+  }
 
   // Associate the track with a process, making it the process-global track.
   // There should only be one such track per process (usually for instant
diff --git a/src/trace_processor/importers/perf/perf_data_tokenizer.cc b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
index 25cfb46..2dcc3e5 100644
--- a/src/trace_processor/importers/perf/perf_data_tokenizer.cc
+++ b/src/trace_processor/importers/perf/perf_data_tokenizer.cc
@@ -376,12 +376,14 @@
 base::Status PerfDataTokenizer::ParseFeature(uint8_t feature_id,
                                              TraceBlobView data) {
   switch (feature_id) {
-    case feature::ID_EVENT_DESC: {
-      RETURN_IF_ERROR(feature::EventDescription::Parse(
-          std::move(data),
-          [](feature::EventDescription) { return base::OkStatus(); }));
-      break;
-    }
+    case feature::ID_EVENT_DESC:
+      return feature::EventDescription::Parse(
+          std::move(data), [&](feature::EventDescription desc) {
+            for (auto id : desc.ids) {
+              perf_session_->SetEventName(id, std::move(desc.event_string));
+            }
+            return base::OkStatus();
+          });
 
     case feature::ID_BUILD_ID:
       return feature::BuildId::Parse(
@@ -399,6 +401,9 @@
       feature::SimpleperfMetaInfo meta_info;
       RETURN_IF_ERROR(
           feature::SimpleperfMetaInfo::Parse(std::move(data), meta_info));
+      for (auto it = meta_info.event_type_info.GetIterator(); it; ++it) {
+        perf_session_->SetEventName(it.key().type, it.key().config, it.value());
+      }
       break;
     }
     case feature::ID_SIMPLEPERF_FILE2: {
diff --git a/src/trace_processor/importers/perf/perf_event.h b/src/trace_processor/importers/perf/perf_event.h
index c45f4de..58077be 100644
--- a/src/trace_processor/importers/perf/perf_event.h
+++ b/src/trace_processor/importers/perf/perf_event.h
@@ -251,7 +251,7 @@
   PERF_FORMAT_MAX = 1U << 5, /* non-ABI */
 };
 
-enum perf_callchain_context {
+enum perf_callchain_context : uint64_t {
   PERF_CONTEXT_HV = static_cast<uint64_t>(-32),
   PERF_CONTEXT_KERNEL = static_cast<uint64_t>(-128),
   PERF_CONTEXT_USER = static_cast<uint64_t>(-512),
diff --git a/src/trace_processor/importers/perf/perf_event_attr.cc b/src/trace_processor/importers/perf/perf_event_attr.cc
index f00f4eb..03fe71f 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.cc
+++ b/src/trace_processor/importers/perf/perf_event_attr.cc
@@ -21,6 +21,7 @@
 #include <cstring>
 #include <optional>
 
+#include "perfetto/ext/base/string_view.h"
 #include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
 #include "src/trace_processor/storage/trace_storage.h"
@@ -115,14 +116,21 @@
 }
 
 PerfCounter PerfEventAttr::CreateCounter(uint32_t cpu) const {
-  return PerfCounter(context_->storage->mutable_counter_table(),
-                     context_->storage->mutable_perf_counter_track_table()
-                         ->Insert({context_->storage->InternString(""),
-                                   std::nullopt, std::nullopt, std::nullopt,
-                                   context_->storage->InternString(""),
-                                   context_->storage->InternString(""),
-                                   perf_session_id_, cpu, is_timebase()})
-                         .row_reference);
+  return PerfCounter(
+      context_->storage->mutable_counter_table(),
+      context_->storage->mutable_perf_counter_track_table()
+          ->Insert({/*in_name=*/context_->storage->InternString(
+                        base::StringView(event_name_)),
+                    /*in_parent_id=*/std::nullopt,
+                    /*in_source_arg_set_id=*/std::nullopt,
+                    /*in_machine_id=*/std::nullopt,
+                    /*in_unit=*/
+                    context_->storage->InternString(base::StringView("")),
+                    /*in_description=*/
+                    context_->storage->InternString(base::StringView("")),
+                    /*in_perf_session_id=*/perf_session_id_, /*in_cpu=*/cpu,
+                    /*in_is_timebase=*/is_timebase()})
+          .row_reference);
 }
 
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_event_attr.h b/src/trace_processor/importers/perf/perf_event_attr.h
index 77e52f4..f1af037 100644
--- a/src/trace_processor/importers/perf/perf_event_attr.h
+++ b/src/trace_processor/importers/perf/perf_event_attr.h
@@ -23,7 +23,6 @@
 #include <optional>
 #include <unordered_map>
 
-#include "perfetto/ext/base/string_view.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "src/trace_processor/importers/perf/perf_counter.h"
 #include "src/trace_processor/importers/perf/perf_event.h"
@@ -41,6 +40,8 @@
                 uint32_t perf_session_id_,
                 perf_event_attr attr);
   ~PerfEventAttr();
+  uint32_t type() const { return attr_.type; }
+  uint64_t config() const { return attr_.config; }
   uint64_t sample_type() const { return attr_.sample_type; }
   uint64_t read_format() const { return attr_.read_format; }
   bool sample_id_all() const { return !!attr_.sample_id_all; }
@@ -84,6 +85,10 @@
     return id_offset_from_end_;
   }
 
+  void set_event_name(std::string event_name) {
+    event_name_ = std::move(event_name);
+  }
+
   PerfCounter& GetOrCreateCounter(uint32_t cpu) const;
 
  private:
@@ -103,6 +108,7 @@
   std::optional<size_t> id_offset_from_start_;
   std::optional<size_t> id_offset_from_end_;
   mutable std::unordered_map<uint32_t, PerfCounter> counters_;
+  std::string event_name_;
 };
 
 }  // namespace perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.cc b/src/trace_processor/importers/perf/perf_session.cc
index 7619066..db42ae7 100644
--- a/src/trace_processor/importers/perf/perf_session.cc
+++ b/src/trace_processor/importers/perf/perf_session.cc
@@ -135,4 +135,22 @@
   return RefPtr<const PerfEventAttr>(it->get());
 }
 
+void PerfSession::SetEventName(uint64_t event_id, std::string name) {
+  auto it = attrs_by_id_.Find(event_id);
+  if (!it) {
+    return;
+  }
+  (*it)->set_event_name(std::move(name));
+}
+
+void PerfSession::SetEventName(uint32_t type,
+                               uint64_t config,
+                               const std::string& name) {
+  for (auto it = attrs_by_id_.GetIterator(); it; ++it) {
+    if (it.value()->type() == type && it.value()->config() == config) {
+      it.value()->set_event_name(name);
+    }
+  }
+}
+
 }  // namespace perfetto::trace_processor::perf_importer
diff --git a/src/trace_processor/importers/perf/perf_session.h b/src/trace_processor/importers/perf/perf_session.h
index eb509d0..c2a9e66 100644
--- a/src/trace_processor/importers/perf/perf_session.h
+++ b/src/trace_processor/importers/perf/perf_session.h
@@ -66,6 +66,9 @@
       const perf_event_header& header,
       const TraceBlobView& payload) const;
 
+  void SetEventName(uint64_t event_id, std::string name);
+  void SetEventName(uint32_t type, uint64_t config, const std::string& name);
+
  private:
   PerfSession(uint32_t perf_session_id,
               base::FlatHashMap<uint64_t, RefPtr<PerfEventAttr>> attrs_by_id,
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index 704a5a0..d958c0b 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -21,6 +21,7 @@
 
 #include "perfetto/base/build_config.h"
 #include "perfetto/base/logging.h"
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/ext/base/utils.h"
 #include "perfetto/protozero/proto_decoder.h"
@@ -119,6 +120,16 @@
     HandlePreviousPacketDropped(decoder);
   }
 
+  uint32_t sequence_id = decoder.trusted_packet_sequence_id();
+  if (sequence_id) {
+    auto [data_loss, inserted] =
+        packet_sequence_data_loss_.Insert(sequence_id, 0);
+
+    if (!inserted && decoder.previous_packet_dropped()) {
+      *data_loss += 1;
+    }
+  }
+
   // It is important that we parse defaults before parsing other fields such as
   // the timestamp, since the defaults could affect them.
   if (decoder.has_trace_packet_defaults()) {
@@ -132,8 +143,7 @@
   }
 
   if (decoder.has_clock_snapshot()) {
-    return ParseClockSnapshot(decoder.clock_snapshot(),
-                              decoder.trusted_packet_sequence_id());
+    return ParseClockSnapshot(decoder.clock_snapshot(), sequence_id);
   }
 
   if (decoder.has_trace_stats()) {
@@ -579,6 +589,23 @@
         stats::traced_buf_trace_writer_packet_loss, buf_num,
         static_cast<int64_t>(buf.trace_writer_packet_loss()));
   }
+
+  base::FlatHashMap<int32_t, int64_t> data_loss_per_buffer;
+
+  for (auto it = evt.writer_stats(); it; ++it) {
+    protos::pbzero::TraceStats::WriterStats::Decoder writer(*it);
+    auto* data_loss = packet_sequence_data_loss_.Find(
+        static_cast<uint32_t>(writer.sequence_id()));
+    if (data_loss) {
+      data_loss_per_buffer[static_cast<int32_t>(writer.buffer())] +=
+          static_cast<int64_t>(*data_loss);
+    }
+  }
+
+  for (auto it = data_loss_per_buffer.GetIterator(); it; ++it) {
+    storage->SetIndexedStats(stats::traced_buf_sequence_packet_loss, it.key(),
+                             it.value());
+  }
 }
 
 void ProtoTraceReader::NotifyEndOfFile() {}
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index c080837..4c4e2ca 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -105,6 +105,8 @@
   base::FlatHashMap<uint32_t, PacketSequenceStateBuilder>
       packet_sequence_state_builders_;
 
+  base::FlatHashMap<uint32_t, size_t> packet_sequence_data_loss_;
+
   StringId skipped_packet_key_id_;
   StringId invalid_incremental_state_key_id_;
 };
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index a242890..657e308 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -1527,9 +1527,10 @@
   }
 
   // Override the name with the most recent name seen (after sorting by ts).
-  if (decoder.has_name()) {
+  if (decoder.has_name() || decoder.has_static_name()) {
     auto* tracks = context_->storage->mutable_track_table();
-    const StringId raw_name_id = context_->storage->InternString(decoder.name());
+    const StringId raw_name_id = context_->storage->InternString(
+        decoder.has_name() ? decoder.name() : decoder.static_name());
     const StringId name_id =
       context_->process_track_translation_table->TranslateName(raw_name_id);
     tracks->mutable_name()->Set(*tracks->id().IndexOf(track_id), name_id);
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index c836052..238e247 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -92,6 +92,8 @@
   StringId name_id = kNullStringId;
   if (track.has_name())
     name_id = context_->storage->InternString(track.name());
+  else if (track.has_static_name())
+    name_id = context_->storage->InternString(track.static_name());
 
   if (packet.has_trusted_pid()) {
     context_->process_tracker->UpdateTrustedPid(
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
index 0d31b0d..1551ea6 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/suspend.sql
@@ -73,7 +73,9 @@
 FROM awake_slice
 UNION ALL
 SELECT ts, dur, 'suspended' AS power_state
-FROM suspend_slice;
+FROM suspend_slice
+ORDER BY ts; -- Order by will cause Perfetto table to index by ts.
+
 
 -- Extracts the duration without counting CPU suspended time from an event.
 -- This is the same as converting an event duration from wall clock to monotonic clock.
diff --git a/src/trace_processor/sqlite/db_sqlite_table.cc b/src/trace_processor/sqlite/db_sqlite_table.cc
index 736af92..cc4f545 100644
--- a/src/trace_processor/sqlite/db_sqlite_table.cc
+++ b/src/trace_processor/sqlite/db_sqlite_table.cc
@@ -54,9 +54,6 @@
 namespace perfetto::trace_processor {
 namespace {
 
-// TODO(b/341230981): Enable after it's fixed.
-static constexpr bool kEnableDistinct = false;
-
 std::optional<FilterOp> SqliteOpToFilterOp(int sqlite_op) {
   switch (sqlite_op) {
     case SQLITE_INDEX_CONSTRAINT_EQ:
@@ -574,7 +571,7 @@
 
   // Distinct:
   idx_str += "D";
-  if (ob_idxes.size() == 1 && kEnableDistinct) {
+  if (ob_idxes.size() == 1 && PERFETTO_POPCOUNT(info->colUsed) == 1) {
     switch (sqlite3_vtab_distinct(info)) {
       case 0:
       case 1:
diff --git a/src/trace_processor/storage/stats.h b/src/trace_processor/storage/stats.h
index 52f0c20..c8d58ca 100644
--- a/src/trace_processor/storage/stats.h
+++ b/src/trace_processor/storage/stats.h
@@ -155,7 +155,13 @@
   F(traced_buf_patches_succeeded,         kIndexed, kInfo,     kTrace,    ""), \
   F(traced_buf_readaheads_failed,         kIndexed, kInfo,     kTrace,    ""), \
   F(traced_buf_readaheads_succeeded,      kIndexed, kInfo,     kTrace,    ""), \
-  F(traced_buf_trace_writer_packet_loss,  kIndexed, kDataLoss, kTrace,    ""), \
+  F(traced_buf_trace_writer_packet_loss,  kIndexed, kDataLoss, kTrace,         \
+      "The tracing service observed packet loss for this buffer during this "  \
+      "tracing session. This also counts packet loss that happened before "    \
+      "the RING_BUFFER start or after the DISCARD buffer end."),               \
+  F(traced_buf_sequence_packet_loss,      kIndexed, kDataLoss, kAnalysis,      \
+      "The number of groups of consecutive packets lost in each sequence for " \
+      "this buffer"), \
   F(traced_buf_write_wrap_count,          kIndexed, kInfo,     kTrace,    ""), \
   F(traced_chunks_discarded,              kSingle,  kInfo,     kTrace,    ""), \
   F(traced_data_sources_registered,       kSingle,  kInfo,     kTrace,    ""), \
diff --git a/src/trace_redaction/main.cc b/src/trace_redaction/main.cc
index eec75fd..7014c0d 100644
--- a/src/trace_redaction/main.cc
+++ b/src/trace_redaction/main.cc
@@ -79,10 +79,11 @@
   redactor.emplace_transform<PrunePackageList>();
   redactor.emplace_transform<ScrubProcessStats>();
 
+  auto* comms_harness = redactor.emplace_transform<RedactSchedSwitchHarness>();
+  comms_harness->emplace_transform<ClearComms>();
+
   auto* redact_ftrace_events = redactor.emplace_transform<RedactFtraceEvent>();
   redact_ftrace_events
-      ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
-  redact_ftrace_events
       ->emplace_back<RedactTaskNewTask::kFieldId, RedactTaskNewTask>();
   redact_ftrace_events
       ->emplace_back<RedactProcessFree::kFieldId, RedactProcessFree>();
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
index 83cb36f..87412c1 100644
--- a/src/trace_redaction/redact_sched_switch.cc
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -16,6 +16,8 @@
 
 #include "src/trace_redaction/redact_sched_switch.h"
 
+#include "perfetto/protozero/scattered_heap_buffer.h"
+#include "src/trace_processor/util/status_macros.h"
 #include "src/trace_redaction/proto_util.h"
 
 #include "protos/perfetto/trace/ftrace/ftrace_event.pbzero.h"
@@ -25,19 +27,12 @@
 namespace perfetto::trace_redaction {
 
 namespace {
-
-// TODO(vaage): Merge with RedactComm in redact_task_newtask.cc.
-protozero::ConstChars RedactComm(const Context& context,
-                                 uint64_t ts,
-                                 int32_t pid,
-                                 protozero::ConstChars comm) {
-  if (context.timeline->PidConnectsToUid(ts, pid, *context.package_uid)) {
-    return comm;
-  }
-
-  return {};
+// TODO(vaage): While simple, this function saves us from declaring the sample
+// lambda each time we use the has_fields pattern. Once its usage increases, and
+// its value is obvious, remove this comment.
+bool IsTrue(bool value) {
+  return value;
 }
-
 }  // namespace
 
 // Redact sched switch trace events in an ftrace event bundle:
@@ -63,71 +58,170 @@
 // collection of ftrace event messages) because data in a sched_switch message
 // is needed in order to know if the event should be added to the bundle.
 
-base::Status RedactSchedSwitch::Redact(
-    const Context& context,
-    const protos::pbzero::FtraceEventBundle::Decoder&,
-    protozero::ProtoDecoder& event,
-    protos::pbzero::FtraceEvent* event_message) const {
-  if (!context.package_uid.has_value()) {
-    return base::ErrStatus("RedactSchedSwitch: missing package uid");
-  }
+SchedSwitchTransform::~SchedSwitchTransform() = default;
 
-  if (!context.timeline) {
-    return base::ErrStatus("RedactSchedSwitch: missing timeline");
-  }
+base::Status RedactSchedSwitchHarness::Transform(const Context& context,
+                                                 std::string* packet) const {
+  protozero::HeapBuffered<protos::pbzero::TracePacket> message;
+  protozero::ProtoDecoder decoder(*packet);
 
-  // The timestamp is needed to do the timeline look-up. If the packet has no
-  // timestamp, don't add the sched switch event. This is the safest option.
-  auto timestamp =
-      event.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
-  if (!timestamp.valid()) {
-    return base::OkStatus();
-  }
-
-  auto sched_switch =
-      event.FindField(protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber);
-  if (!sched_switch.valid()) {
-    return base::ErrStatus(
-        "RedactSchedSwitch: was used for unsupported field type");
-  }
-
-  protozero::ProtoDecoder sched_switch_decoder(sched_switch.as_bytes());
-
-  auto prev_pid = sched_switch_decoder.FindField(
-      protos::pbzero::SchedSwitchFtraceEvent::kPrevPidFieldNumber);
-  auto next_pid = sched_switch_decoder.FindField(
-      protos::pbzero::SchedSwitchFtraceEvent::kNextPidFieldNumber);
-
-  // There must be a prev pid and a next pid. Otherwise, the event is invalid.
-  // Dropping the event is the safest option.
-  if (!prev_pid.valid() || !next_pid.valid()) {
-    return base::OkStatus();
-  }
-
-  // Avoid making the message until we know that we have prev and next pids.
-  auto sched_switch_message = event_message->set_sched_switch();
-
-  for (auto field = sched_switch_decoder.ReadField(); field.valid();
-       field = sched_switch_decoder.ReadField()) {
-    switch (field.id()) {
-      case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber:
-        sched_switch_message->set_next_comm(
-            RedactComm(context, timestamp.as_uint64(), next_pid.as_int32(),
-                       field.as_string()));
-        break;
-
-      case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
-        sched_switch_message->set_prev_comm(
-            RedactComm(context, timestamp.as_uint64(), prev_pid.as_int32(),
-                       field.as_string()));
-        break;
-
-      default:
-        proto_util::AppendField(field, sched_switch_message);
-        break;
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::TracePacket::kFtraceEventsFieldNumber) {
+      RETURN_IF_ERROR(
+          TransformFtraceEvents(context, field, message->set_ftrace_events()));
+    } else {
+      proto_util::AppendField(field, message.get());
     }
   }
 
+  packet->assign(message.SerializeAsString());
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvents(
+    const Context& context,
+    protozero::Field ftrace_events,
+    protos::pbzero::FtraceEventBundle* message) const {
+  PERFETTO_DCHECK(ftrace_events.id() ==
+                  protos::pbzero::TracePacket::kFtraceEventsFieldNumber);
+
+  protozero::ProtoDecoder decoder(ftrace_events.as_bytes());
+
+  auto cpu =
+      decoder.FindField(protos::pbzero::FtraceEventBundle::kCpuFieldNumber);
+  if (!cpu.valid()) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing cpu in ftrace event bundle.");
+  }
+
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::FtraceEventBundle::kEventFieldNumber) {
+      RETURN_IF_ERROR(TransformFtraceEvent(context, cpu.as_int32(), field,
+                                           message->add_event()));
+      continue;
+    }
+
+    if (field.id() ==
+        protos::pbzero::FtraceEventBundle::kCompactSchedFieldNumber) {
+      // TODO(vaage): Replace this with logic specific to the comp sched data
+      // type.
+      proto_util::AppendField(field, message);
+      continue;
+    }
+
+    proto_util::AppendField(field, message);
+  }
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEvent(
+    const Context& context,
+    int32_t cpu,
+    protozero::Field ftrace_event,
+    protos::pbzero::FtraceEvent* message) const {
+  PERFETTO_DCHECK(ftrace_event.id() ==
+                  protos::pbzero::FtraceEventBundle::kEventFieldNumber);
+
+  protozero::ProtoDecoder decoder(ftrace_event.as_bytes());
+
+  auto ts =
+      decoder.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
+  if (!ts.valid()) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing timestamp in ftrace event.");
+  }
+
+  std::string scratch_str;
+
+  for (auto field = decoder.ReadField(); field.valid();
+       field = decoder.ReadField()) {
+    if (field.id() == protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber) {
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder sched_switch(
+          field.as_bytes());
+      RETURN_IF_ERROR(TransformFtraceEventSchedSwitch(
+          context, ts.as_uint64(), cpu, sched_switch, &scratch_str,
+          message->set_sched_switch()));
+    } else {
+      proto_util::AppendField(field, message);
+    }
+  }
+
+  return base::OkStatus();
+}
+
+base::Status RedactSchedSwitchHarness::TransformFtraceEventSchedSwitch(
+    const Context& context,
+    uint64_t ts,
+    int32_t cpu,
+    protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+    std::string* scratch_str,
+    protos::pbzero::SchedSwitchFtraceEvent* message) const {
+  auto has_fields = {
+      sched_switch.has_prev_comm(), sched_switch.has_prev_pid(),
+      sched_switch.has_prev_prio(), sched_switch.has_prev_state(),
+      sched_switch.has_next_comm(), sched_switch.has_next_pid(),
+      sched_switch.has_next_prio()};
+
+  if (!std::all_of(has_fields.begin(), has_fields.end(), IsTrue)) {
+    return base::ErrStatus(
+        "RedactSchedSwitchHarness: missing required SchedSwitchFtraceEvent "
+        "field.");
+  }
+
+  auto prev_pid = sched_switch.prev_pid();
+  auto prev_comm = sched_switch.prev_comm();
+
+  auto next_pid = sched_switch.next_pid();
+  auto next_comm = sched_switch.next_comm();
+
+  // There are 7 values in a sched switch message. Since 4 of the 7 can be
+  // replaced, it is easier/cleaner to go value-by-value. Go in proto-defined
+  // order.
+
+  scratch_str->assign(prev_comm.data, prev_comm.size);
+
+  for (const auto& transform : transforms_) {
+    RETURN_IF_ERROR(
+        transform->Transform(context, ts, cpu, &prev_pid, scratch_str));
+  }
+
+  message->set_prev_comm(*scratch_str);                // FieldNumber = 1
+  message->set_prev_pid(prev_pid);                     // FieldNumber = 2
+  message->set_prev_prio(sched_switch.prev_prio());    // FieldNumber = 3
+  message->set_prev_state(sched_switch.prev_state());  // FieldNumber = 4
+
+  scratch_str->assign(next_comm.data, next_comm.size);
+
+  for (const auto& transform : transforms_) {
+    RETURN_IF_ERROR(
+        transform->Transform(context, ts, cpu, &next_pid, scratch_str));
+  }
+
+  message->set_next_comm(*scratch_str);              // FieldNumber = 5
+  message->set_next_pid(next_pid);                   // FieldNumber = 6
+  message->set_next_prio(sched_switch.next_prio());  // FieldNumber = 7
+
+  return base::OkStatus();
+}
+
+// Switch event transformation: Clear the comm value if the thread/process is
+// not part of the target packet.
+base::Status ClearComms::Transform(const Context& context,
+                                   uint64_t ts,
+                                   int32_t,
+                                   int32_t* pid,
+                                   std::string* comm) const {
+  PERFETTO_DCHECK(pid);
+  PERFETTO_DCHECK(comm);
+
+  if (!context.timeline->PidConnectsToUid(ts, *pid, *context.package_uid)) {
+    comm->clear();
+  }
+
   return base::OkStatus();
 }
 
diff --git a/src/trace_redaction/redact_sched_switch.h b/src/trace_redaction/redact_sched_switch.h
index bcfa30d..55f9a75 100644
--- a/src/trace_redaction/redact_sched_switch.h
+++ b/src/trace_redaction/redact_sched_switch.h
@@ -17,23 +17,63 @@
 #ifndef SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
 #define SRC_TRACE_REDACTION_REDACT_SCHED_SWITCH_H_
 
-#include "src/trace_redaction/redact_ftrace_event.h"
+#include "protos/perfetto/trace/ftrace/ftrace_event_bundle.pbzero.h"
+#include "protos/perfetto/trace/ftrace/sched.pbzero.h"
 #include "src/trace_redaction/trace_redaction_framework.h"
 
 namespace perfetto::trace_redaction {
 
-// Goes through ftrace events and conditonally removes the comm values from
-// sched switch events.
-class RedactSchedSwitch : public FtraceEventRedaction {
+class SchedSwitchTransform {
  public:
-  static constexpr auto kFieldId =
-      protos::pbzero::FtraceEvent::kSchedSwitchFieldNumber;
+  virtual ~SchedSwitchTransform();
+  virtual base::Status Transform(const Context& context,
+                                 uint64_t ts,
+                                 int32_t cpu,
+                                 int32_t* pid,
+                                 std::string* comm) const = 0;
+};
 
-  base::Status Redact(
+// Goes through all sched switch events are modifies them.
+class RedactSchedSwitchHarness : public TransformPrimitive {
+ public:
+  base::Status Transform(const Context& context,
+                         std::string* packet) const override;
+
+  template <class Transform>
+  void emplace_transform() {
+    transforms_.emplace_back(new Transform());
+  }
+
+ private:
+  base::Status TransformFtraceEvents(
       const Context& context,
-      const protos::pbzero::FtraceEventBundle::Decoder& bundle,
-      protozero::ProtoDecoder& event,
-      protos::pbzero::FtraceEvent* event_message) const override;
+      protozero::Field ftrace_events,
+      protos::pbzero::FtraceEventBundle* message) const;
+
+  base::Status TransformFtraceEvent(const Context& context,
+                                    int32_t cpu,
+                                    protozero::Field ftrace_event,
+                                    protos::pbzero::FtraceEvent* message) const;
+
+  // scratch_str is a reusable string, allowing comm modifications to be done in
+  // a shared buffer, avoiding allocations when processing ftrace events.
+  base::Status TransformFtraceEventSchedSwitch(
+      const Context& context,
+      uint64_t ts,
+      int32_t cpu,
+      protos::pbzero::SchedSwitchFtraceEvent::Decoder& sched_switch,
+      std::string* scratch_str,
+      protos::pbzero::SchedSwitchFtraceEvent* message) const;
+
+  std::vector<std::unique_ptr<SchedSwitchTransform>> transforms_;
+};
+
+class ClearComms : public SchedSwitchTransform {
+  base::Status Transform(const Context& context,
+                         uint64_t ts,
+                         int32_t cpu,
+                         int32_t* pid,
+                         std::string* comm) const override;
 };
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch_integrationtest.cc b/src/trace_redaction/redact_sched_switch_integrationtest.cc
index f14d6e9..247d44b 100644
--- a/src/trace_redaction/redact_sched_switch_integrationtest.cc
+++ b/src/trace_redaction/redact_sched_switch_integrationtest.cc
@@ -16,9 +16,9 @@
 
 #include <cstdint>
 #include <string>
+#include <unordered_map>
 
 #include "perfetto/base/status.h"
-#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/base/test/status_matchers.h"
 #include "src/trace_redaction/collect_timeline_events.h"
 #include "src/trace_redaction/find_package_uid.h"
@@ -36,23 +36,6 @@
 
 namespace perfetto::trace_redaction {
 
-class RedactSchedSwitchIntegrationTest
-    : public testing::Test,
-      protected TraceRedactionIntegrationFixure {
- protected:
-  void SetUp() override {
-    trace_redactor()->emplace_collect<FindPackageUid>();
-    trace_redactor()->emplace_collect<CollectTimelineEvents>();
-
-    auto* ftrace_event_redactions =
-        trace_redactor()->emplace_transform<RedactFtraceEvent>();
-    ftrace_event_redactions
-        ->emplace_back<RedactSchedSwitch::kFieldId, RedactSchedSwitch>();
-
-    context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
-  }
-};
-
 // >>> SELECT uid
 // >>>   FROM package_list
 // >>>   WHERE package_name='com.Unity.com.unity.multiplayer.samples.coop'
@@ -103,6 +86,35 @@
 //     | 7950 | UnityGfxDeviceW |
 //     | 7969 | UnityGfxDeviceW |
 //     +------+-----------------+
+class RedactSchedSwitchIntegrationTest
+    : public testing::Test,
+      protected TraceRedactionIntegrationFixure {
+ protected:
+  void SetUp() override {
+    trace_redactor()->emplace_collect<FindPackageUid>();
+    trace_redactor()->emplace_collect<CollectTimelineEvents>();
+
+    auto* harness =
+        trace_redactor()->emplace_transform<RedactSchedSwitchHarness>();
+    harness->emplace_transform<ClearComms>();
+
+    context()->package_name = "com.Unity.com.unity.multiplayer.samples.coop";
+  }
+
+  std::unordered_map<int32_t, std::string> expected_names_ = {
+      {7120, "Binder:7105_2"},   {7127, "UnityMain"},
+      {7142, "Job.worker 0"},    {7143, "Job.worker 1"},
+      {7144, "Job.worker 2"},    {7145, "Job.worker 3"},
+      {7146, "Job.worker 4"},    {7147, "Job.worker 5"},
+      {7148, "Job.worker 6"},    {7150, "Background Job."},
+      {7151, "Background Job."}, {7167, "UnityGfxDeviceW"},
+      {7172, "AudioTrack"},      {7174, "FMOD stream thr"},
+      {7180, "Binder:7105_3"},   {7184, "UnityChoreograp"},
+      {7945, "Filter0"},         {7946, "Filter1"},
+      {7947, "Thread-7"},        {7948, "FMOD mixer thre"},
+      {7950, "UnityGfxDeviceW"}, {7969, "UnityGfxDeviceW"},
+  };
+};
 
 TEST_F(RedactSchedSwitchIntegrationTest, ClearsNonTargetSwitchComms) {
   auto result = Redact();
@@ -114,30 +126,6 @@
   auto redacted = LoadRedacted();
   ASSERT_OK(redacted) << redacted.status().c_message();
 
-  base::FlatHashMap<int32_t, std::string> expected_names;
-  expected_names.Insert(7120, "Binder:7105_2");
-  expected_names.Insert(7127, "UnityMain");
-  expected_names.Insert(7142, "Job.worker 0");
-  expected_names.Insert(7143, "Job.worker 1");
-  expected_names.Insert(7144, "Job.worker 2");
-  expected_names.Insert(7145, "Job.worker 3");
-  expected_names.Insert(7146, "Job.worker 4");
-  expected_names.Insert(7147, "Job.worker 5");
-  expected_names.Insert(7148, "Job.worker 6");
-  expected_names.Insert(7150, "Background Job.");
-  expected_names.Insert(7151, "Background Job.");
-  expected_names.Insert(7167, "UnityGfxDeviceW");
-  expected_names.Insert(7172, "AudioTrack");
-  expected_names.Insert(7174, "FMOD stream thr");
-  expected_names.Insert(7180, "Binder:7105_3");
-  expected_names.Insert(7184, "UnityChoreograp");
-  expected_names.Insert(7945, "Filter0");
-  expected_names.Insert(7946, "Filter1");
-  expected_names.Insert(7947, "Thread-7");
-  expected_names.Insert(7948, "FMOD mixer thre");
-  expected_names.Insert(7950, "UnityGfxDeviceW");
-  expected_names.Insert(7969, "UnityGfxDeviceW");
-
   auto redacted_trace_data = LoadRedacted();
   ASSERT_OK(redacted_trace_data) << redacted.status().c_message();
 
@@ -164,29 +152,29 @@
           event_decoder.sched_switch());
 
       ASSERT_TRUE(sched_decoder.has_next_pid());
-      ASSERT_TRUE(sched_decoder.has_prev_pid());
-
-      auto next_pid = sched_decoder.next_pid();
-      auto prev_pid = sched_decoder.prev_pid();
+      ASSERT_TRUE(sched_decoder.has_next_comm());
 
       // If the pid is expected, make sure it has the right now. If it is not
       // expected, it should be missing.
-      const auto* next_comm = expected_names.Find(next_pid);
-      const auto* prev_comm = expected_names.Find(prev_pid);
+      auto next_pid = sched_decoder.next_pid();
+      auto next_comm = expected_names_.find(next_pid);
 
-      EXPECT_TRUE(sched_decoder.has_next_comm());
-      EXPECT_TRUE(sched_decoder.has_prev_comm());
-
-      if (next_comm) {
-        EXPECT_EQ(sched_decoder.next_comm().ToStdString(), *next_comm);
+      if (next_comm == expected_names_.end()) {
+        ASSERT_EQ(sched_decoder.next_comm().size, 0u);
       } else {
-        EXPECT_EQ(sched_decoder.next_comm().size, 0u);
+        ASSERT_EQ(sched_decoder.next_comm().ToStdString(), next_comm->second);
       }
 
-      if (prev_comm) {
-        EXPECT_EQ(sched_decoder.prev_comm().ToStdString(), *prev_comm);
+      ASSERT_TRUE(sched_decoder.has_prev_pid());
+      ASSERT_TRUE(sched_decoder.has_prev_comm());
+
+      auto prev_pid = sched_decoder.prev_pid();
+      auto prev_comm = expected_names_.find(prev_pid);
+
+      if (prev_comm == expected_names_.end()) {
+        ASSERT_EQ(sched_decoder.prev_comm().size, 0u);
       } else {
-        EXPECT_EQ(sched_decoder.prev_comm().size, 0u);
+        ASSERT_EQ(sched_decoder.prev_comm().ToStdString(), prev_comm->second);
       }
     }
   }
diff --git a/src/trace_redaction/redact_sched_switch_unittest.cc b/src/trace_redaction/redact_sched_switch_unittest.cc
index 72b65f4..88280d7 100644
--- a/src/trace_redaction/redact_sched_switch_unittest.cc
+++ b/src/trace_redaction/redact_sched_switch_unittest.cc
@@ -15,7 +15,6 @@
  */
 
 #include "src/trace_redaction/redact_sched_switch.h"
-#include "perfetto/protozero/scattered_heap_buffer.h"
 #include "src/base/test/status_matchers.h"
 #include "test/gtest_and_gmock.h"
 
@@ -28,6 +27,7 @@
 namespace perfetto::trace_redaction {
 
 namespace {
+
 constexpr uint64_t kUidA = 1;
 constexpr uint64_t kUidB = 2;
 constexpr uint64_t kUidC = 3;
@@ -36,167 +36,130 @@
 constexpr int32_t kPidA = 11;
 constexpr int32_t kPidB = 12;
 
-constexpr std::string_view kCommA = "comm-a";
-constexpr std::string_view kCommB = "comm-b";
+constexpr int32_t kCpuA = 0;
+
+constexpr uint64_t kFullStep = 1000;
+constexpr uint64_t kTimeA = 0;
+constexpr uint64_t kTimeB = kFullStep;
+
+constexpr auto kCommA = "comm-a";
+constexpr auto kCommB = "comm-b";
+constexpr auto kCommNone = "";
 
 }  // namespace
 
-// Tests which nested messages and fields are removed.
 class RedactSchedSwitchTest : public testing::Test {
  protected:
   void SetUp() override {
-    auto* event = bundle_.add_event();
+    // Create a packet where two pids are swapping back-and-forth.
+    auto* bundle = packet_.mutable_ftrace_events();
+    bundle->set_cpu(kCpuA);
 
-    event->set_timestamp(123456789);
-    event->set_pid(kPidA);
+    {
+      auto* event = bundle->add_event();
 
-    auto* sched_switch = event->mutable_sched_switch();
-    sched_switch->set_prev_comm(std::string(kCommA));
-    sched_switch->set_prev_pid(kPidA);
-    sched_switch->set_next_comm(std::string(kCommB));
-    sched_switch->set_next_pid(kPidB);
+      event->set_timestamp(kTimeA);
+      event->set_pid(kPidA);
+
+      auto* sched_switch = event->mutable_sched_switch();
+      sched_switch->set_prev_comm(kCommA);
+      sched_switch->set_prev_pid(kPidA);
+      sched_switch->set_prev_prio(0);
+      sched_switch->set_prev_state(0);
+      sched_switch->set_next_comm(kCommB);
+      sched_switch->set_next_pid(kPidB);
+      sched_switch->set_next_prio(0);
+    }
+
+    {
+      auto* event = bundle->add_event();
+
+      event->set_timestamp(kTimeB);
+      event->set_pid(kPidB);
+
+      auto* sched_switch = event->mutable_sched_switch();
+      sched_switch->set_prev_comm(kCommB);
+      sched_switch->set_prev_pid(kPidB);
+      sched_switch->set_prev_prio(0);
+      sched_switch->set_prev_state(0);
+      sched_switch->set_next_comm(kCommA);
+      sched_switch->set_next_pid(kPidA);
+      sched_switch->set_next_prio(0);
+    }
+
+    // PID A and PID B need to be attached to different packages (UID) so that
+    // its possible to include one but not the other.
+    context_.timeline = std::make_unique<ProcessThreadTimeline>();
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(kTimeA, kPidA, kNoParent, kUidA));
+    context_.timeline->Append(
+        ProcessThreadTimeline::Event::Open(kTimeA, kPidB, kNoParent, kUidB));
+    context_.timeline->Sort();
   }
 
-  base::Status Redact(const Context& context,
-                      protos::pbzero::FtraceEvent* event_message) {
-    RedactSchedSwitch redact;
-
-    auto bundle_str = bundle_.SerializeAsString();
-    protos::pbzero::FtraceEventBundle::Decoder bundle_decoder(bundle_str);
-
-    auto event_str = bundle_.event().back().SerializeAsString();
-    protos::pbzero::FtraceEvent::Decoder event_decoder(event_str);
-
-    return redact.Redact(context, bundle_decoder, event_decoder, event_message);
-  }
-
-  const std::string& event_string() const { return event_string_; }
-
-  // This test breaks the rules for task_newtask and the timeline. The
-  // timeline will report the task existing before the new task event. This
-  // should not happen in the field, but it makes the test more robust.
-  std::unique_ptr<ProcessThreadTimeline> CreatePopulatedTimeline() {
-    auto timeline = std::make_unique<ProcessThreadTimeline>();
-
-    timeline->Append(
-        ProcessThreadTimeline::Event::Open(0, kPidA, kNoParent, kUidA));
-    timeline->Append(
-        ProcessThreadTimeline::Event::Open(0, kPidB, kNoParent, kUidB));
-    timeline->Sort();
-
-    return timeline;
-  }
-
- private:
-  std::string event_string_;
-
-  std::unique_ptr<ProcessThreadTimeline> timeline_;
-
-  protos::gen::FtraceEventBundle bundle_;
+  protos::gen::TracePacket packet_;
+  Context context_;
 };
 
-TEST_F(RedactSchedSwitchTest, RejectMissingPackageUid) {
-  RedactSchedSwitch redact;
+// In this case, the target uid will be UID A. That means the comm values for
+// PID B should be removed, and the comm values for PID A should remain.
+TEST_F(RedactSchedSwitchTest, KeepsTargetCommValues) {
+  RedactSchedSwitchHarness redact;
+  redact.emplace_transform<ClearComms>();
 
-  Context context;
-  context.timeline = std::make_unique<ProcessThreadTimeline>();
+  context_.package_uid = kUidA;
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_FALSE(result.ok());
+  auto packet_buffer = packet_.SerializeAsString();
+
+  ASSERT_OK(redact.Transform(context_, &packet_buffer));
+
+  protos::gen::TracePacket packet;
+  ASSERT_TRUE(packet.ParseFromString(packet_buffer));
+
+  const auto& bundle = packet.ftrace_events();
+  const auto& events = bundle.event();
+
+  ASSERT_EQ(events.size(), 2u);
+
+  ASSERT_EQ(events[0].sched_switch().prev_pid(), kPidA);
+  ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommA);
+
+  ASSERT_EQ(events[0].sched_switch().next_pid(), kPidB);
+  ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
+
+  ASSERT_EQ(events[1].sched_switch().prev_pid(), kPidB);
+  ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+
+  ASSERT_EQ(events[1].sched_switch().next_pid(), kPidA);
+  ASSERT_EQ(events[1].sched_switch().next_comm(), kCommA);
 }
 
-TEST_F(RedactSchedSwitchTest, RejectMissingTimeline) {
-  RedactSchedSwitch redact;
+// This case is very similar to the "some are connected", expect that it
+// verifies all comm values will be removed when testing against an unused
+// uid.
+TEST_F(RedactSchedSwitchTest, RemovesAllCommsIfPackageDoesntExist) {
+  RedactSchedSwitchHarness redact;
+  redact.emplace_transform<ClearComms>();
 
-  Context context;
-  context.package_uid = kUidA;
+  context_.package_uid = kUidC;
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_FALSE(result.ok());
-}
+  auto packet_buffer = packet_.SerializeAsString();
 
-TEST_F(RedactSchedSwitchTest, ReplacePrevAndNextWithEmptyStrings) {
-  RedactSchedSwitch redact;
+  ASSERT_OK(redact.Transform(context_, &packet_buffer));
 
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
+  protos::gen::TracePacket packet;
+  ASSERT_TRUE(packet.ParseFromString(packet_buffer));
 
-  // Neither pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidC;
+  const auto& bundle = packet.ftrace_events();
+  const auto& events = bundle.event();
 
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_OK(result) << result.c_message();
+  ASSERT_EQ(events.size(), 2u);
 
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
+  ASSERT_EQ(events[0].sched_switch().prev_comm(), kCommNone);
+  ASSERT_EQ(events[0].sched_switch().next_comm(), kCommNone);
 
-  ASSERT_TRUE(event.has_sched_switch());
-
-  // Cleared prev and next comm.
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_TRUE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplacePrevWithEmptyStrings) {
-  RedactSchedSwitch redact;
-
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
-
-  // Only next pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidB;
-
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-
-  ASSERT_OK(result) << result.c_message();
-
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
-
-  ASSERT_TRUE(event.has_sched_switch());
-
-  // Only cleared the prev comm.
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_TRUE(event.sched_switch().prev_comm().empty());
-
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_FALSE(event.sched_switch().next_comm().empty());
-}
-
-TEST_F(RedactSchedSwitchTest, ReplaceNextWithEmptyStrings) {
-  RedactSchedSwitch redact;
-
-  Context context;
-  context.timeline = CreatePopulatedTimeline();
-
-  // Only prev pid is connected to the target package (see timeline
-  // initialization).
-  context.package_uid = kUidA;
-
-  protozero::HeapBuffered<protos::pbzero::FtraceEvent> event_message;
-  auto result = Redact(context, event_message.get());
-  ASSERT_OK(result) << result.c_message();
-
-  protos::gen::FtraceEvent event;
-  event.ParseFromString(event_message.SerializeAsString());
-
-  ASSERT_TRUE(event.has_sched_switch());
-
-  ASSERT_TRUE(event.sched_switch().has_prev_comm());
-  ASSERT_FALSE(event.sched_switch().prev_comm().empty());
-
-  // Only cleared the next comm.
-  ASSERT_TRUE(event.sched_switch().has_next_comm());
-  ASSERT_TRUE(event.sched_switch().next_comm().empty());
+  ASSERT_EQ(events[1].sched_switch().prev_comm(), kCommNone);
+  ASSERT_EQ(events[1].sched_switch().next_comm(), kCommNone);
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/tracing/test/api_integrationtest.cc b/src/tracing/test/api_integrationtest.cc
index 5e1e277..5072c3d 100644
--- a/src/tracing/test/api_integrationtest.cc
+++ b/src/tracing/test/api_integrationtest.cc
@@ -2485,7 +2485,7 @@
         EXPECT_FALSE(found_counter_track_descriptor);
         found_counter_track_descriptor = true;
         thread_time_counter_uuid = packet.track_descriptor().uuid();
-        EXPECT_EQ("thread_time", packet.track_descriptor().name());
+        EXPECT_EQ("thread_time", packet.track_descriptor().static_name());
         auto counter = packet.track_descriptor().counter();
         EXPECT_EQ(
             perfetto::protos::gen::
@@ -5795,8 +5795,10 @@
       auto& desc = packet.track_descriptor();
       if (!desc.has_counter())
         continue;
-      counter_names[desc.uuid()] = desc.name();
-      EXPECT_EQ((desc.name() != "Framerate3"), desc.counter().is_incremental());
+      counter_names[desc.uuid()] =
+          desc.has_name() ? desc.name() : desc.static_name();
+      EXPECT_EQ((desc.static_name() != "Framerate3"),
+                desc.counter().is_incremental());
     }
     if (packet.has_track_event()) {
       auto event = packet.track_event();
@@ -5869,7 +5871,8 @@
       continue;
     }
     auto desc = packet.track_descriptor();
-    counter_names[desc.uuid()] = desc.name();
+    counter_names[desc.uuid()] =
+        desc.has_name() ? desc.name() : desc.static_name();
     if (desc.name() == "Framerate") {
       EXPECT_EQ("fps", desc.counter().unit_name());
     } else if (desc.name() == "Goats teleported") {
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index 02e6e03..dc02609 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -118,8 +118,13 @@
 
 protos::gen::TrackDescriptor CounterTrack::Serialize() const {
   auto desc = Track::Serialize();
-  desc.set_name(name_);
   auto* counter = desc.mutable_counter();
+  if (static_name_) {
+    desc.set_static_name(static_name_.value);
+  } else {
+    desc.set_name(dynamic_name_.value);
+  }
+
   if (category_)
     counter->add_categories(category_);
   if (unit_ != perfetto::protos::pbzero::CounterDescriptor::UNIT_UNSPECIFIED)
diff --git a/src/tracing/track_event_state_tracker.cc b/src/tracing/track_event_state_tracker.cc
index 1574292..e65d86f 100644
--- a/src/tracing/track_event_state_tracker.cc
+++ b/src/tracing/track_event_state_tracker.cc
@@ -246,7 +246,11 @@
       track.index = static_cast<uint32_t>(session_state->tracks.size() + 1);
     track.uuid = track_descriptor.uuid();
 
-    track.name = track_descriptor.name().ToStdString();
+    if (track_descriptor.has_name()) {
+      track.name = track_descriptor.name().ToStdString();
+    } else if (track_descriptor.has_static_name()) {
+      track.name = track_descriptor.static_name().ToStdString();
+    }
     track.pid = 0;
     track.tid = 0;
     if (track_descriptor.has_process()) {
diff --git a/test/trace_processor/diff_tests/include_index.py b/test/trace_processor/diff_tests/include_index.py
index 38e3ba2..45c972f 100644
--- a/test/trace_processor/diff_tests/include_index.py
+++ b/test/trace_processor/diff_tests/include_index.py
@@ -75,6 +75,7 @@
 from diff_tests.parser.parsing.tests import Parsing
 from diff_tests.parser.parsing.tests_debug_annotation import ParsingDebugAnnotation
 from diff_tests.parser.parsing.tests_memory_counters import ParsingMemoryCounters
+from diff_tests.parser.parsing.tests_traced_stats import ParsingTracedStats
 from diff_tests.parser.parsing.tests_rss_stats import ParsingRssStats
 from diff_tests.parser.power.tests_energy_breakdown import PowerEnergyBreakdown
 from diff_tests.parser.power.tests_entity_state_residency import EntityStateResidency
@@ -137,6 +138,7 @@
 
 sys.path.pop()
 
+
 def fetch_all_diff_tests(index_path: str) -> List['testing.TestCase']:
   parser_tests = [
       *AndroidBugreport(index_path, 'parser/android',
@@ -187,11 +189,11 @@
       *SmokeJson(index_path, 'parser/smoke', 'SmokeJson').fetch(),
       *SmokeSchedEvents(index_path, 'parser/smoke', 'SmokeSchedEvents').fetch(),
       *InputMethodClients(index_path, 'parser/android',
-                            'InputMethodClients').fetch(),
+                          'InputMethodClients').fetch(),
       *InputMethodManagerService(index_path, 'parser/android',
-                            'InputMethodManagerService').fetch(),
+                                 'InputMethodManagerService').fetch(),
       *InputMethodService(index_path, 'parser/android',
-                            'InputMethodService').fetch(),
+                          'InputMethodService').fetch(),
       *SurfaceFlingerLayers(index_path, 'parser/android',
                             'SurfaceFlingerLayers').fetch(),
       *SurfaceFlingerTransactions(index_path, 'parser/android',
@@ -212,6 +214,8 @@
       *ParsingMemoryCounters(index_path, 'parser/parsing',
                              'ParsingMemoryCounters').fetch(),
       *FtraceCrop(index_path, 'parser/ftrace', 'FtraceCrop').fetch(),
+      *ParsingTracedStats(index_path, 'parser/parsing',
+                          'ParsingTracedStats').fetch(),
   ]
 
   metrics_tests = [
diff --git a/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
new file mode 100644
index 0000000..af2f362
--- /dev/null
+++ b/test/trace_processor/diff_tests/parser/parsing/tests_traced_stats.py
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+# Copyright (C) 2024 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License a
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from python.generators.diff_tests.testing import Csv, TextProto
+from python.generators.diff_tests.testing import DiffTestBlueprint
+from python.generators.diff_tests.testing import TestSuite
+
+
+class ParsingTracedStats(TestSuite):
+  # Check that `previous_packed_dropped: true` maps to
+  # `traced_buf_sequence_packet_loss`.
+  def test_sequence_packet_loss(self):
+    return DiffTestBlueprint(
+        trace=TextProto(r"""
+        packet {
+          trusted_packet_sequence_id: 2
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          trusted_packet_sequence_id: 2
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 3
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 4
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+          previous_packet_dropped: true
+        }
+        packet {
+          trusted_packet_sequence_id: 5
+        }
+        packet {
+          trusted_uid: 9999
+          trusted_packet_sequence_id: 1
+          trace_stats {
+            writer_stats {
+              sequence_id: 2
+              buffer: 0
+            }
+            writer_stats {
+              sequence_id: 3
+              buffer: 1
+            }
+            writer_stats {
+              sequence_id: 4
+              buffer: 2
+            }
+            writer_stats {
+              sequence_id: 5
+              buffer: 2
+            }
+          }
+        }
+        """),
+        query="""
+          SELECT idx, value
+          FROM stats
+          WHERE name = 'traced_buf_sequence_packet_loss'
+          ORDER BY idx;
+        """,
+        out=Csv("""
+        "idx","value"
+        0,0
+        1,1
+        2,2
+        """))
diff --git a/test/trace_processor/diff_tests/syntax/table_tests.py b/test/trace_processor/diff_tests/syntax/table_tests.py
index 7de63e9..5785b6f 100644
--- a/test/trace_processor/diff_tests/syntax/table_tests.py
+++ b/test/trace_processor/diff_tests/syntax/table_tests.py
@@ -178,6 +178,37 @@
         3073,8,4529,8
         """))
 
+  def test_distinct_multi_column(self):
+    return DiffTestBlueprint(
+        trace=TextProto(''),
+        query="""
+        CREATE PERFETTO TABLE foo AS
+        WITH data(a, b) AS (
+          VALUES
+            -- Needed to defeat any id/sorted detection.
+            (2, 3),
+            (0, 2),
+            (0, 1)
+        )
+        SELECT * FROM data;
+
+        CREATE TABLE bar AS
+        SELECT 1 AS b;
+
+        WITH multi_col_distinct AS (
+          SELECT DISTINCT a FROM foo CROSS JOIN bar USING (b)
+        ), multi_col_group_by AS (
+          SELECT a FROM foo CROSS JOIN bar USING (b) GROUP BY a
+        )
+        SELECT
+          (SELECT COUNT(*) FROM multi_col_distinct) AS cnt_distinct,
+          (SELECT COUNT(*) FROM multi_col_group_by) AS cnt_group_by
+        """,
+        out=Csv("""
+        "cnt_distinct","cnt_group_by"
+        1,1
+        """))
+
   def test_limit(self):
     return DiffTestBlueprint(
         trace=TextProto(''),
diff --git a/tools/gen_android_bp b/tools/gen_android_bp
index a0d92ed..2aa0b7f 100755
--- a/tools/gen_android_bp
+++ b/tools/gen_android_bp
@@ -137,7 +137,7 @@
         ]
     },
     'config': {
-        'types': ['lite'],
+        'types': ['filegroup'],
         'targets': [
             '//protos/perfetto/config:source_set',
         ]
diff --git a/ui/src/assets/brand.png b/ui/src/assets/brand.png
index dc6f8b6..63fe6f3 100644
--- a/ui/src/assets/brand.png
+++ b/ui/src/assets/brand.png
Binary files differ
diff --git a/ui/src/assets/favicon.png b/ui/src/assets/favicon.png
index 837b75b..2844520 100644
--- a/ui/src/assets/favicon.png
+++ b/ui/src/assets/favicon.png
Binary files differ
diff --git a/ui/src/assets/logo-128.png b/ui/src/assets/logo-128.png
index 43a2cd1..6d13da5 100644
--- a/ui/src/assets/logo-128.png
+++ b/ui/src/assets/logo-128.png
Binary files differ
diff --git a/ui/src/assets/logo-3d.png b/ui/src/assets/logo-3d.png
index b832ae6..9528c48 100644
--- a/ui/src/assets/logo-3d.png
+++ b/ui/src/assets/logo-3d.png
Binary files differ
diff --git a/ui/src/assets/rec_atrace.png b/ui/src/assets/rec_atrace.png
index d63f2a2..9a6baa2 100644
--- a/ui/src/assets/rec_atrace.png
+++ b/ui/src/assets/rec_atrace.png
Binary files differ
diff --git a/ui/src/assets/rec_battery_counters.png b/ui/src/assets/rec_battery_counters.png
index b73603d..3f1557b 100644
--- a/ui/src/assets/rec_battery_counters.png
+++ b/ui/src/assets/rec_battery_counters.png
Binary files differ
diff --git a/ui/src/assets/rec_board_voltage.png b/ui/src/assets/rec_board_voltage.png
index bc4dd12..d5f5b42 100644
--- a/ui/src/assets/rec_board_voltage.png
+++ b/ui/src/assets/rec_board_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_coarse.png b/ui/src/assets/rec_cpu_coarse.png
index b7241bf..9296a19 100644
--- a/ui/src/assets/rec_cpu_coarse.png
+++ b/ui/src/assets/rec_cpu_coarse.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_fine.png b/ui/src/assets/rec_cpu_fine.png
index 3c8df8b..6d069c2 100644
--- a/ui/src/assets/rec_cpu_fine.png
+++ b/ui/src/assets/rec_cpu_fine.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_freq.png b/ui/src/assets/rec_cpu_freq.png
index 23d86f8..5bd9e7b 100644
--- a/ui/src/assets/rec_cpu_freq.png
+++ b/ui/src/assets/rec_cpu_freq.png
Binary files differ
diff --git a/ui/src/assets/rec_cpu_voltage.png b/ui/src/assets/rec_cpu_voltage.png
index d82d5f3..7e31de9 100644
--- a/ui/src/assets/rec_cpu_voltage.png
+++ b/ui/src/assets/rec_cpu_voltage.png
Binary files differ
diff --git a/ui/src/assets/rec_frame_timeline.png b/ui/src/assets/rec_frame_timeline.png
index 2c83762..da98c40 100644
--- a/ui/src/assets/rec_frame_timeline.png
+++ b/ui/src/assets/rec_frame_timeline.png
Binary files differ
diff --git a/ui/src/assets/rec_ftrace.png b/ui/src/assets/rec_ftrace.png
index a907f9e..e956d05 100644
--- a/ui/src/assets/rec_ftrace.png
+++ b/ui/src/assets/rec_ftrace.png
Binary files differ
diff --git a/ui/src/assets/rec_gpu_mem_total.png b/ui/src/assets/rec_gpu_mem_total.png
index 4b5a44a..537ce9f 100644
--- a/ui/src/assets/rec_gpu_mem_total.png
+++ b/ui/src/assets/rec_gpu_mem_total.png
Binary files differ
diff --git a/ui/src/assets/rec_java_heap_dump.png b/ui/src/assets/rec_java_heap_dump.png
index 229ebe0..e3ee7c4 100644
--- a/ui/src/assets/rec_java_heap_dump.png
+++ b/ui/src/assets/rec_java_heap_dump.png
Binary files differ
diff --git a/ui/src/assets/rec_lmk.png b/ui/src/assets/rec_lmk.png
index 7324cf9..3a42b2d 100644
--- a/ui/src/assets/rec_lmk.png
+++ b/ui/src/assets/rec_lmk.png
Binary files differ
diff --git a/ui/src/assets/rec_logcat.png b/ui/src/assets/rec_logcat.png
index b3b4905..dda6526 100644
--- a/ui/src/assets/rec_logcat.png
+++ b/ui/src/assets/rec_logcat.png
Binary files differ
diff --git a/ui/src/assets/rec_long_trace.png b/ui/src/assets/rec_long_trace.png
index 23aad0b..f801756 100644
--- a/ui/src/assets/rec_long_trace.png
+++ b/ui/src/assets/rec_long_trace.png
Binary files differ
diff --git a/ui/src/assets/rec_mem_hifreq.png b/ui/src/assets/rec_mem_hifreq.png
index f36ef3e..f2909f8 100644
--- a/ui/src/assets/rec_mem_hifreq.png
+++ b/ui/src/assets/rec_mem_hifreq.png
Binary files differ
diff --git a/ui/src/assets/rec_meminfo.png b/ui/src/assets/rec_meminfo.png
index 4280675..43f968e 100644
--- a/ui/src/assets/rec_meminfo.png
+++ b/ui/src/assets/rec_meminfo.png
Binary files differ
diff --git a/ui/src/assets/rec_native_heap_profiler.png b/ui/src/assets/rec_native_heap_profiler.png
index bb3b010..4915c2b 100644
--- a/ui/src/assets/rec_native_heap_profiler.png
+++ b/ui/src/assets/rec_native_heap_profiler.png
Binary files differ
diff --git a/ui/src/assets/rec_one_shot.png b/ui/src/assets/rec_one_shot.png
index 7539c72..8981075 100644
--- a/ui/src/assets/rec_one_shot.png
+++ b/ui/src/assets/rec_one_shot.png
Binary files differ
diff --git a/ui/src/assets/rec_profiling.png b/ui/src/assets/rec_profiling.png
index 385670c..024d9f5 100644
--- a/ui/src/assets/rec_profiling.png
+++ b/ui/src/assets/rec_profiling.png
Binary files differ
diff --git a/ui/src/assets/rec_ps_stats.png b/ui/src/assets/rec_ps_stats.png
index e37bab1..df02761 100644
--- a/ui/src/assets/rec_ps_stats.png
+++ b/ui/src/assets/rec_ps_stats.png
Binary files differ
diff --git a/ui/src/assets/rec_ring_buf.png b/ui/src/assets/rec_ring_buf.png
index a2490fa..9bbe231 100644
--- a/ui/src/assets/rec_ring_buf.png
+++ b/ui/src/assets/rec_ring_buf.png
Binary files differ
diff --git a/ui/src/assets/rec_syscalls.png b/ui/src/assets/rec_syscalls.png
index 734854a..90b4ad8 100644
--- a/ui/src/assets/rec_syscalls.png
+++ b/ui/src/assets/rec_syscalls.png
Binary files differ
diff --git a/ui/src/assets/rec_vmstat.png b/ui/src/assets/rec_vmstat.png
index 58a4e71..46a1006 100644
--- a/ui/src/assets/rec_vmstat.png
+++ b/ui/src/assets/rec_vmstat.png
Binary files differ
diff --git a/ui/src/assets/scheduling_latency.png b/ui/src/assets/scheduling_latency.png
index 36bcfb8..8b5a2e1 100644
--- a/ui/src/assets/scheduling_latency.png
+++ b/ui/src/assets/scheduling_latency.png
Binary files differ
diff --git a/ui/src/base/utils.ts b/ui/src/base/utils.ts
index 41a201f..d3c5f4b 100644
--- a/ui/src/base/utils.ts
+++ b/ui/src/base/utils.ts
@@ -19,3 +19,8 @@
 export function exists<T>(value: T): value is NonNullable<T> {
   return value !== undefined && value !== null;
 }
+
+// Generic result type - similar to Rust's Result<T, E>
+export type Result<T, E = {}> =
+  | {success: true; result: T}
+  | {success: false; error: E};
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 4cebecb..15d8f6c 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -253,15 +253,15 @@
     // the reducer.
     args: {
       name: string;
-      id: string;
+      key: string;
       summaryTrackKey?: string;
       collapsed: boolean;
       fixedOrdering?: boolean;
     },
   ): void {
-    state.trackGroups[args.id] = {
+    state.trackGroups[args.key] = {
       name: args.name,
-      id: args.id,
+      key: args.key,
       collapsed: args.collapsed,
       tracks: [],
       summaryTrack: args.summaryTrackKey,
@@ -394,12 +394,8 @@
     }
   },
 
-  toggleTrackGroupCollapsed(
-    state: StateDraft,
-    args: {trackGroupId: string},
-  ): void {
-    const id = args.trackGroupId;
-    const trackGroup = assertExists(state.trackGroups[id]);
+  toggleTrackGroupCollapsed(state: StateDraft, args: {groupKey: string}): void {
+    const trackGroup = assertExists(state.trackGroups[args.groupKey]);
     trackGroup.collapsed = !trackGroup.collapsed;
   },
 
@@ -448,31 +444,6 @@
     }
   },
 
-  createPermalink(state: StateDraft, args: {isRecordingConfig: boolean}): void {
-    state.permalink = {
-      requestId: generateNextId(state),
-      hash: undefined,
-      isRecordingConfig: args.isRecordingConfig,
-    };
-  },
-
-  setPermalink(
-    state: StateDraft,
-    args: {requestId: string; hash: string},
-  ): void {
-    // Drop any links for old requests.
-    if (state.permalink.requestId !== args.requestId) return;
-    state.permalink = args;
-  },
-
-  loadPermalink(state: StateDraft, args: {hash: string}): void {
-    state.permalink = {requestId: generateNextId(state), hash: args.hash};
-  },
-
-  clearPermalink(state: StateDraft, _: {}): void {
-    state.permalink = {};
-  },
-
   updateStatus(state: StateDraft, args: Status): void {
     if (statusTraceEvent) {
       traceEventEnd(statusTraceEvent);
@@ -920,7 +891,7 @@
 
   toggleTrackSelection(
     state: StateDraft,
-    args: {id: string; isTrackGroup: boolean},
+    args: {key: string; isTrackGroup: boolean},
   ) {
     const selection = state.selection;
     if (
@@ -930,12 +901,12 @@
       return;
     }
     const areaId = selection.legacySelection.areaId;
-    const index = state.areas[areaId].tracks.indexOf(args.id);
+    const index = state.areas[areaId].tracks.indexOf(args.key);
     if (index > -1) {
       state.areas[areaId].tracks.splice(index, 1);
       if (args.isTrackGroup) {
         // Also remove all child tracks.
-        for (const childTrack of state.trackGroups[args.id].tracks) {
+        for (const childTrack of state.trackGroups[args.key].tracks) {
           const childIndex = state.areas[areaId].tracks.indexOf(childTrack);
           if (childIndex > -1) {
             state.areas[areaId].tracks.splice(childIndex, 1);
@@ -943,10 +914,10 @@
         }
       }
     } else {
-      state.areas[areaId].tracks.push(args.id);
+      state.areas[areaId].tracks.push(args.key);
       if (args.isTrackGroup) {
         // Also add all child tracks.
-        for (const childTrack of state.trackGroups[args.id].tracks) {
+        for (const childTrack of state.trackGroups[args.key].tracks) {
           if (!state.areas[areaId].tracks.includes(childTrack)) {
             state.areas[areaId].tracks.push(childTrack);
           }
diff --git a/ui/src/common/actions_unittest.ts b/ui/src/common/actions_unittest.ts
index de497d7..734ac91 100644
--- a/ui/src/common/actions_unittest.ts
+++ b/ui/src/common/actions_unittest.ts
@@ -60,14 +60,14 @@
 
 function fakeTrackGroup(
   state: State,
-  args: {id: string; summaryTrackId: string},
+  args: {key: string; summaryTrackKey: string},
 ): State {
   return produce(state, (draft) => {
     StateActions.addTrackGroup(draft, {
       name: 'A group',
-      id: args.id,
+      key: args.key,
       collapsed: false,
-      summaryTrackKey: args.summaryTrackId,
+      summaryTrackKey: args.summaryTrackKey,
     });
   });
 }
@@ -117,7 +117,7 @@
   const afterGroup = produce(state, (draft) => {
     StateActions.addTrackGroup(draft, {
       name: 'A track group',
-      id: '123-123-123',
+      key: '123-123-123',
       summaryTrackKey: 's',
       collapsed: false,
     });
@@ -151,19 +151,19 @@
     });
   });
 
-  const firstTrackId = once.scrollingTracks[0];
-  const secondTrackId = once.scrollingTracks[1];
+  const firstTrackKey = once.scrollingTracks[0];
+  const secondTrackKey = once.scrollingTracks[1];
 
   const twice = produce(once, (draft) => {
     StateActions.moveTrack(draft, {
-      srcId: `${firstTrackId}`,
+      srcId: `${firstTrackKey}`,
       op: 'after',
-      dstId: `${secondTrackId}`,
+      dstId: `${secondTrackKey}`,
     });
   });
 
-  expect(twice.scrollingTracks[0]).toBe(secondTrackId);
-  expect(twice.scrollingTracks[1]).toBe(firstTrackId);
+  expect(twice.scrollingTracks[0]).toBe(secondTrackKey);
+  expect(twice.scrollingTracks[1]).toBe(firstTrackKey);
 });
 
 test('reorder pinned to scrolling', () => {
@@ -326,7 +326,7 @@
 
 test('sortTracksByPriority', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
   state = fakeTrack(state, {
     key: 'b',
     uri: HEAP_PROFILE_TRACK_KIND,
@@ -351,7 +351,7 @@
 
 test('sortTracksByPriorityAndKindAndName', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'b'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'b'});
   state = fakeTrack(state, {
     key: 'a',
     uri: PROCESS_SCHEDULING_TRACK_KIND,
@@ -416,7 +416,7 @@
 
 test('sortTracksByTidThenName', () => {
   let state = createEmptyState();
-  state = fakeTrackGroup(state, {id: 'g', summaryTrackId: 'a'});
+  state = fakeTrackGroup(state, {key: 'g', summaryTrackKey: 'a'});
   state = fakeTrack(state, {
     key: 'a',
     uri: SLICE_TRACK_KIND,
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index e3ed2b7..c513d73 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -95,7 +95,6 @@
     scrollingTracks: [],
     areas: {},
     queries: {},
-    permalink: {},
     notes: {},
 
     recordConfig: AUTOLOAD_STARTED_CONFIG_FLAG.get()
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 3a5899e..f729597 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -17,7 +17,7 @@
 import {Disposable, Trash} from '../base/disposable';
 import {Registry} from '../base/registry';
 import {Span, duration, time} from '../base/time';
-import {globals} from '../frontend/globals';
+import {TraceContext, globals} from '../frontend/globals';
 import {
   Command,
   DetailsPanel,
@@ -45,6 +45,7 @@
 import {raf} from '../core/raf_scheduler';
 import {defaultPlugins} from '../core/default_plugins';
 import {HighPrecisionTimeSpan} from './high_precision_time';
+import {PromptOption} from '../frontend/omnibox_manager';
 
 // Every plugin gets its own PluginContext. This is how we keep track
 // what each plugin is doing and how we can blame issues on particular
@@ -275,10 +276,10 @@
           };
           return predicate(ref);
         })
-        .map((group) => group.id);
+        .map((group) => group.key);
 
-      for (const trackGroupId of groupsToExpand) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+      for (const groupKey of groupsToExpand) {
+        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
       }
     },
 
@@ -293,10 +294,10 @@
           };
           return predicate(ref);
         })
-        .map((group) => group.id);
+        .map((group) => group.key);
 
-      for (const trackGroupId of groupsToCollapse) {
-        globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+      for (const groupKey of groupsToCollapse) {
+        globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
       }
     },
 
@@ -342,11 +343,16 @@
     return globals.store.createSubStore(['plugins', this.pluginId], migrate);
   }
 
-  readonly trace = {
-    get span(): Span<time, duration> {
-      return globals.stateTraceTimeTP();
-    },
-  };
+  get trace(): TraceContext {
+    return globals.traceContext;
+  }
+
+  async prompt(
+    text: string,
+    options?: PromptOption[] | undefined,
+  ): Promise<string> {
+    return globals.omnibox.prompt(text, options);
+  }
 }
 
 function isPinned(trackId: string): boolean {
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index a6c461b..227b9cd 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -40,48 +40,59 @@
   params?: QueryRunParams,
 ): Promise<QueryResponse> {
   const startMs = performance.now();
-  const queryRes = engine.execute(sqlQuery);
 
   // TODO(primiano): once the controller thread is gone we should pass down
   // the result objects directly to the frontend, iterate over the result
   // and deal with pagination there. For now we keep the old behavior and
   // truncate to 10k rows.
 
-  try {
-    await queryRes.waitAllRows();
-  } catch {
+  const maybeResult = await engine.tryQuery(sqlQuery);
+
+  if (maybeResult.success) {
+    const queryRes = maybeResult.result;
+    const convertNullsToString = params?.convertNullsToString ?? true;
+
+    const durationMs = performance.now() - startMs;
+    const rows: Row[] = [];
+    const columns = queryRes.columns();
+    let numRows = 0;
+    for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
+      const row: Row = {};
+      for (const colName of columns) {
+        const value = iter.get(colName);
+        row[colName] = value === null && convertNullsToString ? 'NULL' : value;
+      }
+      rows.push(row);
+      if (++numRows >= MAX_DISPLAY_ROWS) break;
+    }
+
+    const result: QueryResponse = {
+      query: sqlQuery,
+      durationMs,
+      error: queryRes.error(),
+      totalRowCount: queryRes.numRows(),
+      columns,
+      rows,
+      statementCount: queryRes.statementCount(),
+      statementWithOutputCount: queryRes.statementWithOutputCount(),
+      lastStatementSql: queryRes.lastStatementSql(),
+    };
+    return result;
+  } else {
     // In the case of a query error we don't want the exception to bubble up
     // as a crash. The |queryRes| object will be populated anyways.
     // queryRes.error() is used to tell if the query errored or not. If it
     // errored, the frontend will show a graceful message instead.
+    return {
+      query: sqlQuery,
+      durationMs: performance.now() - startMs,
+      error: maybeResult.error.message,
+      totalRowCount: 0,
+      columns: [],
+      rows: [],
+      statementCount: 0,
+      statementWithOutputCount: 0,
+      lastStatementSql: '',
+    };
   }
-
-  const convertNullsToString = params?.convertNullsToString ?? true;
-
-  const durationMs = performance.now() - startMs;
-  const rows: Row[] = [];
-  const columns = queryRes.columns();
-  let numRows = 0;
-  for (const iter = queryRes.iter({}); iter.valid(); iter.next()) {
-    const row: Row = {};
-    for (const colName of columns) {
-      const value = iter.get(colName);
-      row[colName] = value === null && convertNullsToString ? 'NULL' : value;
-    }
-    rows.push(row);
-    if (++numRows >= MAX_DISPLAY_ROWS) break;
-  }
-
-  const result: QueryResponse = {
-    query: sqlQuery,
-    durationMs,
-    error: queryRes.error(),
-    totalRowCount: queryRes.numRows(),
-    columns,
-    rows,
-    statementCount: queryRes.statementCount(),
-    statementWithOutputCount: queryRes.statementWithOutputCount(),
-    lastStatementSql: queryRes.lastStatementSql(),
-  };
-  return result;
 }
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index fc3018e..94eb429 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -151,7 +151,8 @@
 // 52. Update track group state - don't make the summary track the first track.
 // 53. Remove android log state.
 // 54. Remove traceTime.
-export const STATE_VERSION = 54;
+// 55. Rename TrackGroupState.id -> TrackGroupState.key.
+export const STATE_VERSION = 55;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -288,7 +289,7 @@
 }
 
 export interface TrackGroupState {
-  id: string;
+  key: string;
   name: string;
   collapsed: boolean;
   tracks: string[]; // Child track ids.
@@ -310,12 +311,6 @@
   query: string;
 }
 
-export interface PermalinkConfig {
-  requestId?: string; // Set by the frontend to request a new permalink.
-  hash?: string; // Set by the controller when the link has been created.
-  isRecordingConfig?: boolean; // this permalink request is for a recording config only
-}
-
 export interface FrontendLocalState {
   visibleState: VisibleState;
 }
@@ -476,7 +471,7 @@
   newEngineMode: NewEngineMode;
   engine?: EngineConfig;
   traceUuid?: string;
-  trackGroups: ObjectById<TrackGroupState>;
+  trackGroups: ObjectByKey<TrackGroupState>;
   tracks: ObjectByKey<TrackState>;
   utidToThreadSortKey: UtidToTrackSortKey;
   areas: ObjectById<AreaById>;
@@ -486,7 +481,6 @@
   debugTrackId?: string;
   lastTrackReloadRequest?: number;
   queries: ObjectById<QueryConfig>;
-  permalink: PermalinkConfig;
   notes: ObjectById<Note | AreaNote>;
   status: Status;
   selection: Selection;
@@ -915,20 +909,19 @@
   ];
 }
 
-export function getContainingTrackId(
+export function getContainingGroupKey(
   state: State,
   trackKey: string,
 ): null | string {
   const track = state.tracks[trackKey];
-  // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-  if (!track) {
+  if (track === undefined) {
     return null;
   }
-  const parentId = track.trackGroup;
-  if (!parentId) {
+  const parentGroupKey = track.trackGroup;
+  if (!parentGroupKey) {
     return null;
   }
-  return parentId;
+  return parentGroupKey;
 }
 
 export function getLegacySelection(state: State): LegacySelection | null {
diff --git a/ui/src/common/state_unittest.ts b/ui/src/common/state_unittest.ts
index be66340..ff111de 100644
--- a/ui/src/common/state_unittest.ts
+++ b/ui/src/common/state_unittest.ts
@@ -15,7 +15,7 @@
 import {PrimaryTrackSortKey} from '../public';
 
 import {createEmptyState} from './empty_state';
-import {getContainingTrackId, State} from './state';
+import {getContainingGroupKey, State} from './state';
 import {deserializeStateObject, serializeStateObject} from './upload_utils';
 
 test('createEmptyState', () => {
@@ -40,9 +40,9 @@
     trackGroup: 'containsB',
   };
 
-  expect(getContainingTrackId(state, 'z')).toEqual(null);
-  expect(getContainingTrackId(state, 'a')).toEqual(null);
-  expect(getContainingTrackId(state, 'b')).toEqual('containsB');
+  expect(getContainingGroupKey(state, 'z')).toEqual(null);
+  expect(getContainingGroupKey(state, 'a')).toEqual(null);
+  expect(getContainingGroupKey(state, 'b')).toEqual('containsB');
 });
 
 test('state is serializable', () => {
diff --git a/ui/src/controller/app_controller.ts b/ui/src/controller/app_controller.ts
index 4c8ad85..7d4d7cd 100644
--- a/ui/src/controller/app_controller.ts
+++ b/ui/src/controller/app_controller.ts
@@ -16,7 +16,6 @@
 import {globals} from '../frontend/globals';
 
 import {Child, Controller, ControllerInitializerAny} from './controller';
-import {PermalinkController} from './permalink_controller';
 import {RecordController} from './record_controller';
 import {TraceController} from './trace_controller';
 
@@ -40,9 +39,7 @@
   // - An internal promise of a nested controller being resolved and manually
   //   re-triggering the controllers.
   run() {
-    const childControllers: ControllerInitializerAny[] = [
-      Child('permalink', PermalinkController, {}),
-    ];
+    const childControllers: ControllerInitializerAny[] = [];
     if (!RECORDING_V2_FLAG.get()) {
       childControllers.push(
         Child('record', RecordController, {extensionPort: this.extensionPort}),
diff --git a/ui/src/controller/permalink_controller.ts b/ui/src/controller/permalink_controller.ts
deleted file mode 100644
index 1380ebf..0000000
--- a/ui/src/controller/permalink_controller.ts
+++ /dev/null
@@ -1,269 +0,0 @@
-// Copyright (C) 2018 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//      http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-import {produce} from 'immer';
-
-import {assertExists} from '../base/logging';
-import {runValidator} from '../base/validators';
-import {Actions} from '../common/actions';
-import {ConversionJobStatus} from '../common/conversion_jobs';
-import {
-  createEmptyNonSerializableState,
-  createEmptyState,
-} from '../common/empty_state';
-import {EngineConfig, ObjectById, State, STATE_VERSION} from '../common/state';
-import {
-  BUCKET_NAME,
-  buggyToSha256,
-  deserializeStateObject,
-  saveState,
-  toSha256,
-  TraceGcsUploader,
-} from '../common/upload_utils';
-import {globals} from '../frontend/globals';
-import {publishConversionJobStatusUpdate} from '../frontend/publish';
-import {Router} from '../frontend/router';
-
-import {Controller} from './controller';
-import {RecordConfig, recordConfigValidator} from './record_config_types';
-import {showModal} from '../widgets/modal';
-
-interface MultiEngineState {
-  currentEngineId?: string;
-  engines: ObjectById<EngineConfig>;
-}
-
-function isMultiEngineState(
-  state: State | MultiEngineState,
-): state is MultiEngineState {
-  if ((state as MultiEngineState).engines !== undefined) {
-    return true;
-  }
-  return false;
-}
-
-export class PermalinkController extends Controller<'main'> {
-  private lastRequestId?: string;
-  constructor() {
-    super('main');
-  }
-
-  run() {
-    if (
-      globals.state.permalink.requestId === undefined ||
-      globals.state.permalink.requestId === this.lastRequestId
-    ) {
-      return;
-    }
-    const requestId = assertExists(globals.state.permalink.requestId);
-    this.lastRequestId = requestId;
-
-    // if the |hash| is not set, this is a request to create a permalink.
-    if (globals.state.permalink.hash === undefined) {
-      const isRecordingConfig = assertExists(
-        globals.state.permalink.isRecordingConfig,
-      );
-
-      const jobName = 'create_permalink';
-      publishConversionJobStatusUpdate({
-        jobName,
-        jobStatus: ConversionJobStatus.InProgress,
-      });
-
-      PermalinkController.createPermalink(isRecordingConfig)
-        .then((hash) => {
-          globals.dispatch(Actions.setPermalink({requestId, hash}));
-        })
-        .finally(() => {
-          publishConversionJobStatusUpdate({
-            jobName,
-            jobStatus: ConversionJobStatus.NotRunning,
-          });
-        });
-      return;
-    }
-
-    // Otherwise, this is a request to load the permalink.
-    PermalinkController.loadState(globals.state.permalink.hash).then(
-      (stateOrConfig) => {
-        if (PermalinkController.isRecordConfig(stateOrConfig)) {
-          // This permalink state only contains a RecordConfig. Show the
-          // recording page with the config, but keep other state as-is.
-          const validConfig = runValidator(
-            recordConfigValidator,
-            stateOrConfig as unknown,
-          ).result;
-          globals.dispatch(Actions.setRecordConfig({config: validConfig}));
-          Router.navigate('#!/record');
-          return;
-        }
-        globals.dispatch(Actions.setState({newState: stateOrConfig}));
-        this.lastRequestId = stateOrConfig.permalink.requestId;
-      },
-    );
-  }
-
-  private static upgradeState(state: State): State {
-    if (state.engine !== undefined && state.engine.source.type !== 'URL') {
-      // All permalink traces should be modified to have a source.type=URL
-      // pointing to the uploaded trace. Due to a bug in some older version
-      // of the UI (b/327049372), an upload failure can end up with a state that
-      // has type=FILE but a null file object. If this happens, invalidate the
-      // trace and show a message.
-      showModal({
-        title: 'Cannot load trace permalink',
-        content: m(
-          'div',
-          'The permalink stored on the server is corrupted ' +
-            'and cannot be loaded.',
-        ),
-      });
-      return createEmptyState();
-    }
-
-    if (state.version !== STATE_VERSION) {
-      const newState = createEmptyState();
-      // Old permalinks from state versions prior to version 24
-      // have multiple engines of which only one is identified as the
-      // current engine via currentEngineId. Handle this case:
-      if (isMultiEngineState(state)) {
-        const engineId = state.currentEngineId;
-        if (engineId !== undefined) {
-          newState.engine = state.engines[engineId];
-        }
-      } else {
-        newState.engine = state.engine;
-      }
-
-      if (newState.engine !== undefined) {
-        newState.engine.ready = false;
-      }
-      const message =
-        `Unable to parse old state version. Discarding state ` +
-        `and loading trace.`;
-      console.warn(message);
-      PermalinkController.updateStatus(message);
-      return newState;
-    } else {
-      // Loaded state is presumed to be compatible with the State type
-      // definition in the app. However, a non-serializable part has to be
-      // recreated.
-      state.nonSerializableState = createEmptyNonSerializableState();
-    }
-    return state;
-  }
-
-  private static isRecordConfig(
-    stateOrConfig: State | RecordConfig,
-  ): stateOrConfig is RecordConfig {
-    const mode = (stateOrConfig as {mode?: string}).mode;
-    return (
-      mode !== undefined &&
-      ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
-    );
-  }
-
-  private static async createPermalink(
-    isRecordingConfig: boolean,
-  ): Promise<string> {
-    let uploadState: State | RecordConfig = globals.state;
-
-    if (isRecordingConfig) {
-      uploadState = globals.state.recordConfig;
-    } else {
-      const engine = assertExists(globals.getCurrentEngine());
-      let dataToUpload: File | ArrayBuffer | undefined = undefined;
-      let traceName = `trace ${engine.id}`;
-      if (engine.source.type === 'FILE') {
-        dataToUpload = engine.source.file;
-        traceName = dataToUpload.name;
-      } else if (engine.source.type === 'ARRAY_BUFFER') {
-        dataToUpload = engine.source.buffer;
-      } else if (engine.source.type !== 'URL') {
-        throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
-      }
-
-      if (dataToUpload !== undefined) {
-        PermalinkController.updateStatus(`Uploading ${traceName}`);
-        const uploader = new TraceGcsUploader(dataToUpload, () => {
-          switch (uploader.state) {
-            case 'UPLOADING':
-              const statusTxt = `Uploading ${uploader.getEtaString()}`;
-              PermalinkController.updateStatus(statusTxt);
-              break;
-            case 'UPLOADED':
-              // Convert state to use URLs and remove permalink.
-              const url = uploader.uploadedUrl;
-              uploadState = produce(globals.state, (draft) => {
-                assertExists(draft.engine).source = {type: 'URL', url};
-                draft.permalink = {};
-              });
-              break;
-            case 'ERROR':
-              PermalinkController.updateStatus(
-                `Upload failed ${uploader.error}`,
-              );
-              break;
-          } // switch (state)
-        }); // onProgress
-        await uploader.waitForCompletion();
-      }
-    }
-
-    // Upload state.
-    PermalinkController.updateStatus(`Creating permalink...`);
-    const hash = await saveState(uploadState);
-    PermalinkController.updateStatus(`Permalink ready`);
-    return hash;
-  }
-
-  private static async loadState(id: string): Promise<State | RecordConfig> {
-    const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
-    const response = await fetch(url);
-    if (!response.ok) {
-      throw new Error(
-        `Could not fetch permalink.\n` +
-          `Are you sure the id (${id}) is correct?\n` +
-          `URL: ${url}`,
-      );
-    }
-    const text = await response.text();
-    const stateHash = await toSha256(text);
-    const state = deserializeStateObject<State>(text);
-    if (stateHash !== id) {
-      // Old permalinks incorrectly dropped some digits from the
-      // hexdigest of the SHA256. We don't want to invalidate those
-      // links so we also compute the old string and try that here
-      // also.
-      const buggyStateHash = await buggyToSha256(text);
-      if (buggyStateHash !== id) {
-        throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
-      }
-    }
-    if (!this.isRecordConfig(state)) {
-      return this.upgradeState(state);
-    }
-    return state;
-  }
-
-  private static updateStatus(msg: string): void {
-    // TODO(hjd): Unify loading updates.
-    globals.dispatch(
-      Actions.updateStatus({
-        msg,
-        timestamp: Date.now() / 1000,
-      }),
-    );
-  }
-}
diff --git a/ui/src/controller/search_controller.ts b/ui/src/controller/search_controller.ts
index ba2d354..bd6a3b1 100644
--- a/ui/src/controller/search_controller.ts
+++ b/ui/src/controller/search_controller.ts
@@ -163,7 +163,7 @@
       utids.push(it.utid);
     }
 
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
     const maxCpu = Math.max(...cpus, -1);
 
     const res = await this.query(`
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index 1b6dd0f..0a58c43 100644
--- a/ui/src/controller/selection_controller.ts
+++ b/ui/src/controller/selection_controller.ts
@@ -441,7 +441,7 @@
           IFNULL(value, 0) as value
         FROM counter WHERE ts < ${ts} and track_id = ${trackId}`);
     const previousValue = previous.firstRow({value: NUM}).value;
-    const endTs = rightTs !== -1n ? rightTs : globals.traceTime.end;
+    const endTs = rightTs !== -1n ? rightTs : globals.traceContext.end;
     const delta = value - previousValue;
     const duration = endTs - ts;
     const trackKey = globals.trackManager.trackKeyByTrackId.get(trackId);
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 445bd4a..a3e6a59 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -29,11 +29,11 @@
 import {EngineMode, PendingDeeplinkState, ProfileType} from '../common/state';
 import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
 import {
-  defaultTraceTime,
+  defaultTraceContext,
   globals,
   QuantizedLoad,
   ThreadDesc,
-  TraceTime,
+  TraceContext,
 } from '../frontend/globals';
 import {
   clearOverviewData,
@@ -41,7 +41,7 @@
   publishMetricError,
   publishOverviewData,
   publishThreads,
-  publishTraceDetails,
+  publishTraceContext,
 } from '../frontend/publish';
 import {addQueryResultsTab} from '../frontend/query_result_tab';
 import {Router} from '../frontend/router';
@@ -452,7 +452,7 @@
     const traceUuid = await this.cacheCurrentTrace();
 
     const traceDetails = await getTraceTimeDetails(this.engine);
-    publishTraceDetails(traceDetails);
+    publishTraceContext(traceDetails);
 
     const shownJsonWarning =
       window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
@@ -1100,8 +1100,8 @@
   // if we have non-default visible state, update the visible time to it
   const previousVisibleState = globals.stateVisibleTime();
   const defaultTraceSpan = new TimeSpan(
-    defaultTraceTime.start,
-    defaultTraceTime.end,
+    defaultTraceContext.start,
+    defaultTraceContext.end,
   );
   if (
     !(
@@ -1122,7 +1122,7 @@
   let visibleEnd = traceEnd;
 
   // compare start and end with metadata computed by the trace processor
-  const mdTime = await engine.getTracingMetadataTimeBounds();
+  const mdTime = await getTracingMetadataTimeBounds(engine);
   // make sure the bounds hold
   if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
     visibleStart = Time.max(visibleStart, mdTime.start);
@@ -1145,8 +1145,8 @@
   return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd);
 }
 
-async function getTraceTimeDetails(engine: EngineBase): Promise<TraceTime> {
-  const traceTime = await engine.getTraceTimeBounds();
+async function getTraceTimeDetails(engine: Engine): Promise<TraceContext> {
+  const traceTime = await getTraceTimeBounds(engine);
 
   // Find the first REALTIME or REALTIME_COARSE clock snapshot.
   // Prioritize REALTIME over REALTIME_COARSE.
@@ -1216,5 +1216,67 @@
     realtimeOffset,
     utcOffset,
     traceTzOffset,
+    cpus: await getCpus(engine),
+    gpuCount: await getNumberOfGpus(engine),
   };
 }
+
+async function getTraceTimeBounds(
+  engine: Engine,
+): Promise<Span<time, duration>> {
+  const result = await engine.query(
+    `select start_ts as startTs, end_ts as endTs from trace_bounds`,
+  );
+  const bounds = result.firstRow({
+    startTs: LONG,
+    endTs: LONG,
+  });
+  return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
+}
+
+// TODO(hjd): When streaming must invalidate this somehow.
+async function getCpus(engine: Engine): Promise<number[]> {
+  const cpus = [];
+  const queryRes = await engine.query(
+    'select distinct(cpu) as cpu from sched order by cpu;',
+  );
+  for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
+    cpus.push(it.cpu);
+  }
+  return cpus;
+}
+
+async function getNumberOfGpus(engine: Engine): Promise<number> {
+  const result = await engine.query(`
+    select count(distinct(gpu_id)) as gpuCount
+    from gpu_counter_track
+    where name = 'gpufreq';
+  `);
+  return result.firstRow({gpuCount: NUM}).gpuCount;
+}
+
+async function getTracingMetadataTimeBounds(
+  engine: Engine,
+): Promise<Span<time, duration>> {
+  const queryRes = await engine.query(`select
+       name,
+       int_value as intValue
+       from metadata
+       where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
+       or name = 'all_data_source_started_ns'`);
+  let startBound = Time.MIN;
+  let endBound = Time.MAX;
+  const it = queryRes.iter({name: STR, intValue: LONG_NULL});
+  for (; it.valid(); it.next()) {
+    const columnName = it.name;
+    const timestamp = it.intValue;
+    if (timestamp === null) continue;
+    if (columnName === 'tracing_disabled_ns') {
+      endBound = Time.min(endBound, Time.fromRaw(timestamp));
+    } else {
+      startBound = Time.max(startBound, Time.fromRaw(timestamp));
+    }
+  }
+
+  return new TimeSpan(startBound, endBound);
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index e1b907c..a9b186d 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -118,7 +118,7 @@
   }
 
   async addCpuSchedulingTracks(): Promise<void> {
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
     const cpuToSize = await this.guessCpuSizes();
 
     for (const cpu of cpus) {
@@ -134,7 +134,7 @@
   }
 
   async addCpuFreqTracks(engine: Engine): Promise<void> {
-    const cpus = await this.engine.getCpus();
+    const cpus = globals.traceContext.cpus;
 
     for (const cpu of cpus) {
       // Only add a cpu freq track if we have
@@ -189,38 +189,38 @@
       parentName: STR_NULL,
     });
 
-    const parentIdToGroupId = new Map<number, string>();
+    const parentIdToGroupKey = new Map<number, string>();
     for (; it.valid(); it.next()) {
       const kind = ASYNC_SLICE_TRACK_KIND;
       const rawName = it.name === null ? undefined : it.name;
       const rawParentName = it.parentName === null ? undefined : it.parentName;
       const name = getTrackName({name: rawName, kind});
       const parentTrackId = it.parentId;
-      let trackGroup = SCROLLING_TRACK_GROUP;
+      let groupKey = SCROLLING_TRACK_GROUP;
 
       if (parentTrackId !== null) {
-        const groupId = parentIdToGroupId.get(parentTrackId);
-        if (groupId === undefined) {
-          trackGroup = uuidv4();
-          parentIdToGroupId.set(parentTrackId, trackGroup);
+        const maybeGroupKey = parentIdToGroupKey.get(parentTrackId);
+        if (maybeGroupKey === undefined) {
+          groupKey = uuidv4();
+          parentIdToGroupKey.set(parentTrackId, groupKey);
 
           const parentName = getTrackName({name: rawParentName, kind});
           this.addTrackGroupActions.push(
             Actions.addTrackGroup({
               name: parentName,
-              id: trackGroup,
+              key: groupKey,
               collapsed: true,
             }),
           );
         } else {
-          trackGroup = groupId;
+          groupKey = maybeGroupKey;
         }
       }
 
       const track: AddTrackArgs = {
         uri: `perfetto.AsyncSlices#${rawName}.${it.parentId}`,
         trackSortKey: PrimaryTrackSortKey.ASYNC_SLICE_TRACK,
-        trackGroup,
+        trackGroup: groupKey,
         name,
       };
 
@@ -229,7 +229,7 @@
   }
 
   async addGpuFreqTracks(engine: Engine): Promise<void> {
-    const numGpus = await this.engine.getNumberOfGpus();
+    const numGpus = globals.traceContext.gpuCount;
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
       // gpu freq data.
@@ -315,7 +315,7 @@
       return;
     }
 
-    const id = uuidv4();
+    const groupUuid = uuidv4();
     const summaryTrackKey = uuidv4();
     let foundSummary = false;
 
@@ -328,14 +328,14 @@
         track.key = summaryTrackKey;
         track.trackGroup = undefined;
       } else {
-        track.trackGroup = id;
+        track.trackGroup = groupUuid;
       }
     }
 
     const addGroup = Actions.addTrackGroup({
       summaryTrackKey,
       name: MEM_DMA_COUNTER_NAME,
-      id,
+      key: groupUuid,
       collapsed: true,
     });
     this.addTrackGroupActions.push(addGroup);
@@ -369,7 +369,7 @@
       const groupName = group + key;
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: value,
+        key: value,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -408,7 +408,7 @@
       const groupName = key;
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: value,
+        key: value,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -441,7 +441,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -483,7 +483,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -511,7 +511,7 @@
     if (groupUuid !== undefined) {
       const addGroup = Actions.addTrackGroup({
         name: groupName,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -537,7 +537,7 @@
       summaryTrackKey: string;
     }
 
-    const groupNameToIds = new Map<string, GroupIds>();
+    const groupNameToKeys = new Map<string, GroupIds>();
 
     for (; sliceIt.valid(); sliceIt.next()) {
       const id = sliceIt.id;
@@ -553,13 +553,13 @@
         // If this is the first track encountered for a certain group,
         // create an id for the group and use this track as the group's
         // summary track.
-        const groupIds = groupNameToIds.get(groupName);
-        if (groupIds) {
-          trackGroupId = groupIds.id;
+        const groupKeys = groupNameToKeys.get(groupName);
+        if (groupKeys) {
+          trackGroupId = groupKeys.id;
         } else {
           trackGroupId = uuidv4();
           summaryTrackKey = uuidv4();
-          groupNameToIds.set(groupName, {
+          groupNameToKeys.set(groupName, {
             id: trackGroupId,
             summaryTrackKey,
           });
@@ -575,11 +575,11 @@
       });
     }
 
-    for (const [groupName, groupIds] of groupNameToIds) {
+    for (const [groupName, groupKeys] of groupNameToKeys) {
       const addGroup = Actions.addTrackGroup({
-        summaryTrackKey: groupIds.summaryTrackKey,
+        summaryTrackKey: groupKeys.summaryTrackKey,
         name: groupName,
-        id: groupIds.id,
+        key: groupKeys.id,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -843,7 +843,7 @@
     for (const [name, groupUuid] of groupMap) {
       const addGroup = Actions.addTrackGroup({
         name: name,
-        id: groupUuid,
+        key: groupUuid,
         collapsed: true,
       });
       this.addTrackGroupActions.push(addGroup);
@@ -1153,7 +1153,7 @@
     const addTrackGroup = Actions.addTrackGroup({
       summaryTrackKey,
       name: `Kernel threads`,
-      id: kthreadGroupUuid,
+      key: kthreadGroupUuid,
       collapsed: true,
     });
     this.addTrackGroupActions.push(addTrackGroup);
@@ -1320,7 +1320,7 @@
       const addTrackGroup = Actions.addTrackGroup({
         summaryTrackKey,
         name,
-        id: this.getOrCreateUuid(utid, upid),
+        key: this.getOrCreateUuid(utid, upid),
         // Perf profiling tracks remain collapsed, otherwise we would have too
         // many expanded process tracks for some perf traces, leading to
         // jankyness.
@@ -1373,7 +1373,7 @@
           groupUuid = uuidv4();
           const addGroup = Actions.addTrackGroup({
             name: groupName,
-            id: groupUuid,
+            key: groupUuid,
             collapsed: true,
             fixedOrdering: true,
           });
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 15e5d2c..4f5460e 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -44,7 +44,6 @@
   'perfetto.CpuProfile',
   'perfetto.CpuSlices',
   'perfetto.CriticalUserInteraction',
-  'perfetto.CustomSqlTrack',
   'perfetto.DebugSlices',
   'perfetto.Flows',
   'perfetto.Frames',
@@ -58,4 +57,5 @@
   'perfetto.ThreadState',
   'perfetto.VisualisedArgs',
   'org.kernel.LinuxKernelDevices',
+  'perfetto.TrackUtils',
 ];
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index bda66f3..43ccb9d 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -18,7 +18,8 @@
   SLICE_TRACK_KIND,
 } from '../chrome_slices/chrome_slice_track';
 import {NUM, NUM_NULL, STR} from '../../trace_processor/query_result';
-import {COUNTER_TRACK_KIND, TraceProcessorCounterTrack} from '../counter';
+import {COUNTER_TRACK_KIND} from '../counter';
+import {TraceProcessorCounterTrack} from '../counter/trace_processor_counter_track';
 
 class AnnotationPlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
diff --git a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts b/ui/src/core_plugins/async_slices/async_slice_track.ts
similarity index 95%
rename from ui/src/core_plugins/async_slices/async_slice_track_v2.ts
rename to ui/src/core_plugins/async_slices/async_slice_track.ts
index 659a89a..275c418 100644
--- a/ui/src/core_plugins/async_slices/async_slice_track_v2.ts
+++ b/ui/src/core_plugins/async_slices/async_slice_track.ts
@@ -17,7 +17,7 @@
 import {NewTrackArgs} from '../../frontend/track';
 import {Slice} from '../../public';
 
-export class AsyncSliceTrackV2 extends NamedSliceTrack {
+export class AsyncSliceTrack extends NamedSliceTrack {
   constructor(
     args: NewTrackArgs,
     maxDepth: number,
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index 644d22f..b01cb38 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -16,7 +16,7 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
-import {AsyncSliceTrackV2} from './async_slice_track_v2';
+import {AsyncSliceTrack} from './async_slice_track';
 
 export const ASYNC_SLICE_TRACK_KIND = 'AsyncSliceTrack';
 
@@ -71,7 +71,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
         },
       });
     }
@@ -123,7 +123,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2(
+          return new AsyncSliceTrack(
             {engine: ctx.engine, trackKey},
             maxDepth,
             trackIds,
@@ -190,7 +190,7 @@
         trackIds,
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2({engine, trackKey}, maxDepth, trackIds);
+          return new AsyncSliceTrack({engine, trackKey}, maxDepth, trackIds);
         },
       });
     }
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
new file mode 100644
index 0000000..9268750
--- /dev/null
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/critical_user_interaction_track.ts
@@ -0,0 +1,180 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {OnSliceClickArgs} from '../../frontend/base_slice_track';
+import {GenericSliceDetailsTab} from '../../frontend/generic_slice_details_tab';
+import {globals} from '../../frontend/globals';
+import {
+  NAMED_ROW,
+  NamedSliceTrackTypes,
+} from '../../frontend/named_slice_track';
+import {NUM, Slice, STR} from '../../public';
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlImportConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+
+import {PageLoadDetailsPanel} from './page_load_details_panel';
+import {StartupDetailsPanel} from './startup_details_panel';
+import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+
+export const CRITICAL_USER_INTERACTIONS_KIND =
+  'org.chromium.CriticalUserInteraction.track';
+
+export const CRITICAL_USER_INTERACTIONS_ROW = {
+  ...NAMED_ROW,
+  scopedId: NUM,
+  type: STR,
+};
+export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
+
+export interface CriticalUserInteractionSlice extends Slice {
+  scopedId: number;
+  type: string;
+}
+
+export interface CriticalUserInteractionSliceTrackTypes
+  extends NamedSliceTrackTypes {
+  slice: CriticalUserInteractionSlice;
+  row: CriticalUserInteractionRow;
+}
+
+enum CriticalUserInteractionType {
+  UNKNOWN = 'Unknown',
+  PAGE_LOAD = 'chrome_page_loads',
+  STARTUP = 'chrome_startups',
+  WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
+}
+
+function convertToCriticalUserInteractionType(
+  cujType: string,
+): CriticalUserInteractionType {
+  switch (cujType) {
+    case CriticalUserInteractionType.PAGE_LOAD:
+      return CriticalUserInteractionType.PAGE_LOAD;
+    case CriticalUserInteractionType.STARTUP:
+      return CriticalUserInteractionType.STARTUP;
+    case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+      return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
+    default:
+      return CriticalUserInteractionType.UNKNOWN;
+  }
+}
+
+export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
+  static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      columns: [
+        // The scoped_id is not a unique identifier within the table; generate
+        // a unique id from type and scoped_id on the fly to use for slice
+        // selection.
+        'hash(type, scoped_id) AS id',
+        'scoped_id AS scopedId',
+        'name',
+        'ts',
+        'dur',
+        'type',
+      ],
+      sqlTableName: 'chrome_interactions',
+    };
+  }
+
+  getDetailsPanel(
+    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+  ): CustomSqlDetailsPanelConfig {
+    let detailsPanel = {
+      kind: GenericSliceDetailsTab.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Chrome Interaction',
+      },
+    };
+
+    switch (convertToCriticalUserInteractionType(args.slice.type)) {
+      case CriticalUserInteractionType.PAGE_LOAD:
+        detailsPanel = {
+          kind: PageLoadDetailsPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Page Load',
+          },
+        };
+        break;
+      case CriticalUserInteractionType.STARTUP:
+        detailsPanel = {
+          kind: StartupDetailsPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Startup',
+          },
+        };
+        break;
+      case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
+        detailsPanel = {
+          kind: WebContentInteractionPanel.kind,
+          config: {
+            sqlTableName: this.tableName,
+            title: 'Chrome Web Content Interaction',
+          },
+        };
+        break;
+      default:
+        break;
+    }
+    return detailsPanel;
+  }
+
+  onSliceClick(
+    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
+  ) {
+    const detailsPanelConfig = this.getDetailsPanel(args);
+    globals.makeSelection(
+      Actions.selectGenericSlice({
+        id: args.slice.scopedId,
+        sqlTableName: this.tableName,
+        start: args.slice.ts,
+        duration: args.slice.dur,
+        trackKey: this.trackKey,
+        detailsPanelConfig: {
+          kind: detailsPanelConfig.kind,
+          config: detailsPanelConfig.config,
+        },
+      }),
+    );
+  }
+
+  getSqlImports(): CustomSqlImportConfig {
+    return {
+      modules: ['chrome.interactions'],
+    };
+  }
+
+  getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
+    return CRITICAL_USER_INTERACTIONS_ROW;
+  }
+
+  rowToSlice(
+    row: CriticalUserInteractionSliceTrackTypes['row'],
+  ): CriticalUserInteractionSliceTrackTypes['slice'] {
+    const baseSlice = super.rowToSlice(row);
+    const scopedId = row.scopedId;
+    const type = row.type;
+    return {...baseSlice, scopedId, type};
+  }
+}
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index faa863b..b8d7050 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -16,186 +16,23 @@
 
 import {Actions} from '../../common/actions';
 import {SCROLLING_TRACK_GROUP} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {
-  GenericSliceDetailsTab,
-  GenericSliceDetailsTabConfig,
-} from '../../frontend/generic_slice_details_tab';
+import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {globals} from '../../frontend/globals';
 import {
-  NAMED_ROW,
-  NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {
   BottomTabToSCSAdapter,
-  NUM,
   Plugin,
   PluginContext,
   PluginContextTrace,
   PluginDescriptor,
   PrimaryTrackSortKey,
-  Slice,
-  STR,
 } from '../../public';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlImportConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
 
 import {PageLoadDetailsPanel} from './page_load_details_panel';
 import {StartupDetailsPanel} from './startup_details_panel';
 import {WebContentInteractionPanel} from './web_content_interaction_details_panel';
+import {CriticalUserInteractionTrack} from './critical_user_interaction_track';
 
-export const CRITICAL_USER_INTERACTIONS_KIND =
-  'org.chromium.CriticalUserInteraction.track';
-
-export const CRITICAL_USER_INTERACTIONS_ROW = {
-  ...NAMED_ROW,
-  scopedId: NUM,
-  type: STR,
-};
-export type CriticalUserInteractionRow = typeof CRITICAL_USER_INTERACTIONS_ROW;
-
-export interface CriticalUserInteractionSlice extends Slice {
-  scopedId: number;
-  type: string;
-}
-
-export interface CriticalUserInteractionSliceTrackTypes
-  extends NamedSliceTrackTypes {
-  slice: CriticalUserInteractionSlice;
-  row: CriticalUserInteractionRow;
-}
-
-enum CriticalUserInteractionType {
-  UNKNOWN = 'Unknown',
-  PAGE_LOAD = 'chrome_page_loads',
-  STARTUP = 'chrome_startups',
-  WEB_CONTENT_INTERACTION = 'chrome_web_content_interactions',
-}
-
-function convertToCriticalUserInteractionType(
-  cujType: string,
-): CriticalUserInteractionType {
-  switch (cujType) {
-    case CriticalUserInteractionType.PAGE_LOAD:
-      return CriticalUserInteractionType.PAGE_LOAD;
-    case CriticalUserInteractionType.STARTUP:
-      return CriticalUserInteractionType.STARTUP;
-    case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-      return CriticalUserInteractionType.WEB_CONTENT_INTERACTION;
-    default:
-      return CriticalUserInteractionType.UNKNOWN;
-  }
-}
-
-export class CriticalUserInteractionTrack extends CustomSqlTableSliceTrack<CriticalUserInteractionSliceTrackTypes> {
-  static readonly kind = CRITICAL_USER_INTERACTIONS_KIND;
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      columns: [
-        // The scoped_id is not a unique identifier within the table; generate
-        // a unique id from type and scoped_id on the fly to use for slice
-        // selection.
-        'hash(type, scoped_id) AS id',
-        'scoped_id AS scopedId',
-        'name',
-        'ts',
-        'dur',
-        'type',
-      ],
-      sqlTableName: 'chrome_interactions',
-    };
-  }
-
-  getDetailsPanel(
-    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
-  ): CustomSqlDetailsPanelConfig {
-    let detailsPanel = {
-      kind: GenericSliceDetailsTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Chrome Interaction',
-      },
-    };
-
-    switch (convertToCriticalUserInteractionType(args.slice.type)) {
-      case CriticalUserInteractionType.PAGE_LOAD:
-        detailsPanel = {
-          kind: PageLoadDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Page Load',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.STARTUP:
-        detailsPanel = {
-          kind: StartupDetailsPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Startup',
-          },
-        };
-        break;
-      case CriticalUserInteractionType.WEB_CONTENT_INTERACTION:
-        detailsPanel = {
-          kind: WebContentInteractionPanel.kind,
-          config: {
-            sqlTableName: this.tableName,
-            title: 'Chrome Web Content Interaction',
-          },
-        };
-        break;
-      default:
-        break;
-    }
-    return detailsPanel;
-  }
-
-  onSliceClick(
-    args: OnSliceClickArgs<CriticalUserInteractionSliceTrackTypes['slice']>,
-  ) {
-    const detailsPanelConfig = this.getDetailsPanel(args);
-    globals.makeSelection(
-      Actions.selectGenericSlice({
-        id: args.slice.scopedId,
-        sqlTableName: this.tableName,
-        start: args.slice.ts,
-        duration: args.slice.dur,
-        trackKey: this.trackKey,
-        detailsPanelConfig: {
-          kind: detailsPanelConfig.kind,
-          config: detailsPanelConfig.config,
-        },
-      }),
-    );
-  }
-
-  getSqlImports(): CustomSqlImportConfig {
-    return {
-      modules: ['chrome.interactions'],
-    };
-  }
-
-  getRowSpec(): CriticalUserInteractionSliceTrackTypes['row'] {
-    return CRITICAL_USER_INTERACTIONS_ROW;
-  }
-
-  rowToSlice(
-    row: CriticalUserInteractionSliceTrackTypes['row'],
-  ): CriticalUserInteractionSliceTrackTypes['slice'] {
-    const baseSlice = super.rowToSlice(row);
-    const scopedId = row.scopedId;
-    const type = row.type;
-    return {...baseSlice, scopedId, type};
-  }
-}
-
-export function addCriticalUserInteractionTrack() {
+function addCriticalUserInteractionTrack() {
   const trackKey = uuidv4();
   globals.dispatchMultiple([
     Actions.addTrack({
diff --git a/ui/src/core_plugins/chrome_scroll_jank/common.ts b/ui/src/core_plugins/chrome_scroll_jank/common.ts
index 8c20cc4..523880d 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/common.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/common.ts
@@ -15,7 +15,7 @@
 import {AddTrackArgs} from '../../common/actions';
 import {ObjectByKey} from '../../common/state';
 import {featureFlags} from '../../core/feature_flags';
-import {CustomSqlDetailsPanelConfig} from '../custom_sql_table_slices';
+import {CustomSqlDetailsPanelConfig} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 export const SCROLL_JANK_GROUP_ID = 'chrome-scroll-jank-track-group';
 
diff --git a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
index 6d89daa..98f3dd3 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/event_latency_track.ts
@@ -20,7 +20,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
 import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 4267f4c..aae51c4 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -84,7 +84,7 @@
 
   const addTrackGroup = Actions.addTrackGroup({
     name: 'Chrome Scroll Jank',
-    id: SCROLL_JANK_GROUP_ID,
+    key: SCROLL_JANK_GROUP_ID,
     collapsed: false,
     fixedOrdering: true,
   });
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
index f775740..11d6f1f 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_track.ts
@@ -20,7 +20,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {EventLatencyTrackTypes} from './event_latency_track';
 import {JANK_COLOR} from './jank_colors';
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
index 123d8f9..2d69139 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_track.ts
@@ -19,7 +19,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 import {
   DecideTracksResult,
   SCROLL_JANK_GROUP_ID,
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 55773cd..7ac31d0 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -14,14 +14,10 @@
 
 import m from 'mithril';
 
-import {Time} from '../../base/time';
-import {Actions} from '../../common/actions';
 import {CounterDetailsPanel} from '../../frontend/counter_panel';
-import {globals} from '../../frontend/globals';
 import {
   NUM_NULL,
   STR_NULL,
-  LONG,
   LONG_NULL,
   NUM,
   Plugin,
@@ -31,11 +27,8 @@
   STR,
 } from '../../public';
 import {getTrackName} from '../../public/utils';
-import {
-  BaseCounterTrack,
-  BaseCounterTrackArgs,
-  CounterOptions,
-} from '../../frontend/base_counter_track';
+import {CounterOptions} from '../../frontend/base_counter_track';
+import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
 
 export const COUNTER_TRACK_KIND = 'CounterTrack';
 
@@ -112,82 +105,6 @@
   return options;
 }
 
-interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
-  trackId: number;
-  rootTable?: string;
-}
-
-export class TraceProcessorCounterTrack extends BaseCounterTrack {
-  private trackId: number;
-  private rootTable: string;
-
-  constructor(args: TraceProcessorCounterTrackArgs) {
-    super(args);
-    this.trackId = args.trackId;
-    this.rootTable = args.rootTable ?? 'counter';
-  }
-
-  getSqlSource() {
-    return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
-  }
-
-  onMouseClick({x}: {x: number}): boolean {
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
-
-    const query = `
-      select
-        id,
-        ts as leftTs,
-        (
-          select ts
-          from ${this.rootTable}
-          where
-            track_id = ${this.trackId}
-            and ts >= ${time}
-          order by ts
-          limit 1
-        ) as rightTs
-      from ${this.rootTable}
-      where
-        track_id = ${this.trackId}
-        and ts < ${time}
-      order by ts DESC
-      limit 1
-    `;
-
-    this.engine.query(query).then((result) => {
-      const it = result.iter({
-        id: NUM,
-        leftTs: LONG,
-        rightTs: LONG_NULL,
-      });
-      if (!it.valid()) {
-        return;
-      }
-      const trackKey = this.trackKey;
-      const id = it.id;
-      const leftTs = Time.fromRaw(it.leftTs);
-
-      // TODO(stevegolton): Don't try to guess times and durations here, make it
-      // obvious to the user that this counter sample has no duration as it's
-      // the last one in the series
-      const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
-
-      globals.makeSelection(
-        Actions.selectCounter({
-          leftTs,
-          rightTs,
-          id,
-          trackKey,
-        }),
-      );
-    });
-
-    return true;
-  }
-}
-
 class CounterPlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     await this.addCounterTracks(ctx);
@@ -421,7 +338,7 @@
 
   private async addGpuFrequencyTracks(ctx: PluginContextTrace) {
     const engine = ctx.engine;
-    const numGpus = await engine.getNumberOfGpus();
+    const numGpus = ctx.trace.gpuCount;
 
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
diff --git a/ui/src/core_plugins/counter/trace_processor_counter_track.ts b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
new file mode 100644
index 0000000..a80ac4b
--- /dev/null
+++ b/ui/src/core_plugins/counter/trace_processor_counter_track.ts
@@ -0,0 +1,98 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {globals} from '../../frontend/globals';
+import {LONG, LONG_NULL, NUM} from '../../public';
+import {
+  BaseCounterTrack,
+  BaseCounterTrackArgs,
+} from '../../frontend/base_counter_track';
+
+interface TraceProcessorCounterTrackArgs extends BaseCounterTrackArgs {
+  trackId: number;
+  rootTable?: string;
+}
+
+export class TraceProcessorCounterTrack extends BaseCounterTrack {
+  private trackId: number;
+  private rootTable: string;
+
+  constructor(args: TraceProcessorCounterTrackArgs) {
+    super(args);
+    this.trackId = args.trackId;
+    this.rootTable = args.rootTable ?? 'counter';
+  }
+
+  getSqlSource() {
+    return `select ts, value from ${this.rootTable} where track_id = ${this.trackId}`;
+  }
+
+  onMouseClick({x}: {x: number}): boolean {
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x).toTime('floor');
+
+    const query = `
+      select
+        id,
+        ts as leftTs,
+        (
+          select ts
+          from ${this.rootTable}
+          where
+            track_id = ${this.trackId}
+            and ts >= ${time}
+          order by ts
+          limit 1
+        ) as rightTs
+      from ${this.rootTable}
+      where
+        track_id = ${this.trackId}
+        and ts < ${time}
+      order by ts DESC
+      limit 1
+    `;
+
+    this.engine.query(query).then((result) => {
+      const it = result.iter({
+        id: NUM,
+        leftTs: LONG,
+        rightTs: LONG_NULL,
+      });
+      if (!it.valid()) {
+        return;
+      }
+      const trackKey = this.trackKey;
+      const id = it.id;
+      const leftTs = Time.fromRaw(it.leftTs);
+
+      // TODO(stevegolton): Don't try to guess times and durations here, make it
+      // obvious to the user that this counter sample has no duration as it's
+      // the last one in the series
+      const rightTs = Time.fromRaw(it.rightTs ?? leftTs);
+
+      globals.makeSelection(
+        Actions.selectCounter({
+          leftTs,
+          rightTs,
+          id,
+          trackKey,
+        }),
+      );
+    });
+
+    return true;
+  }
+}
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index 07b9b1d..a1d3947 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -73,14 +73,14 @@
 
   async onCreate() {
     if (this.config.idleTrackId === undefined) {
-      await this.engine.execute(`
+      await this.engine.query(`
         create view raw_freq_idle_${this.trackUuid} as
         select ts, dur, value as freqValue, -1 as idleValue
         from experimental_counter_dur c
         where track_id = ${this.config.freqTrackId}
       `);
     } else {
-      await this.engine.execute(`
+      await this.engine.query(`
         create view raw_freq_${this.trackUuid} as
         select ts, dur, value as freqValue
         from experimental_counter_dur c
@@ -99,7 +99,7 @@
       `);
     }
 
-    await this.engine.execute(`
+    await this.engine.query(`
       create virtual table cpu_freq_${this.trackUuid}
       using __intrinsic_counter_mipmap((
         select ts, freqValue as value
@@ -119,13 +119,15 @@
   }
 
   async onDestroy(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table cpu_freq_${this.trackUuid}`);
-      await this.engine.query(`drop table cpu_idle_${this.trackUuid}`);
-      await this.engine.query(`drop table raw_freq_idle_${this.trackUuid}`);
-      await this.engine.query(`drop view if exists raw_freq_${this.trackUuid}`);
-      await this.engine.query(`drop view if exists raw_idle_${this.trackUuid}`);
-    }
+    await this.engine.tryQuery(`drop table cpu_freq_${this.trackUuid}`);
+    await this.engine.tryQuery(`drop table cpu_idle_${this.trackUuid}`);
+    await this.engine.tryQuery(`drop table raw_freq_idle_${this.trackUuid}`);
+    await this.engine.tryQuery(
+      `drop view if exists raw_freq_${this.trackUuid}`,
+    );
+    await this.engine.tryQuery(
+      `drop view if exists raw_idle_${this.trackUuid}`,
+    );
   }
 
   async onBoundsChange(
@@ -403,7 +405,7 @@
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const {engine} = ctx;
 
-    const cpus = await engine.getCpus();
+    const cpus = ctx.trace.cpus;
 
     const maxCpuFreqResult = await engine.query(`
       select ifnull(max(value), 0) as freq
diff --git a/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
new file mode 100644
index 0000000..802e341
--- /dev/null
+++ b/ui/src/core_plugins/cpu_profile/cpu_profile_track.ts
@@ -0,0 +1,248 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {getLegacySelection} from '../../common/state';
+import {Actions} from '../../common/actions';
+import {colorForSample} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+
+const BAR_HEIGHT = 3;
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+interface Data extends TrackData {
+  ids: Float64Array;
+  tsStarts: BigInt64Array;
+  callsiteId: Uint32Array;
+}
+
+export class CpuProfileTrack implements Track {
+  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
+  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
+  private hoveredTs: time | undefined = undefined;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+  private engine: Engine;
+  private utid: number;
+
+  constructor(engine: Engine, utid: number) {
+    this.engine = engine;
+    this.utid = utid;
+  }
+
+  async onUpdate(): Promise<void> {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    const query = `select
+        id,
+        ts,
+        callsite_id as callsiteId
+      from cpu_profile_stack_sample
+      where utid = ${this.utid}
+      order by ts`;
+
+    const result = await this.engine.query(query);
+    const numRows = result.numRows();
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      ids: new Float64Array(numRows),
+      tsStarts: new BigInt64Array(numRows),
+      callsiteId: new Uint32Array(numRows),
+    };
+
+    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
+    for (let row = 0; it.valid(); it.next(), ++row) {
+      data.ids[row] = it.id;
+      data.tsStarts[row] = it.ts;
+      data.callsiteId[row] = it.callsiteId;
+    }
+
+    return data;
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher.dispose();
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT - 1;
+  }
+
+  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+    const {visibleTimeScale: timeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return;
+
+    for (let i = 0; i < data.tsStarts.length; i++) {
+      const centerX = Time.fromRaw(data.tsStarts[i]);
+      const selection = getLegacySelection(globals.state);
+      const isHovered = this.hoveredTs === centerX;
+      const isSelected =
+        selection !== null &&
+        selection.kind === 'CPU_PROFILE_SAMPLE' &&
+        selection.ts === centerX;
+      const strokeWidth = isSelected ? 3 : 0;
+      this.drawMarker(
+        ctx,
+        timeScale.timeToPx(centerX),
+        this.centerY,
+        isHovered,
+        strokeWidth,
+        data.callsiteId[i],
+      );
+    }
+
+    // Group together identical identical CPU profile samples by connecting them
+    // with an horizontal bar.
+    let clusterStartIndex = 0;
+    while (clusterStartIndex < data.tsStarts.length) {
+      const callsiteId = data.callsiteId[clusterStartIndex];
+
+      // Find the end of the cluster by searching for the next different CPU
+      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
+      // inclusive and within array bounds.
+      let clusterEndIndex = clusterStartIndex;
+      while (
+        clusterEndIndex + 1 < data.tsStarts.length &&
+        data.callsiteId[clusterEndIndex + 1] === callsiteId
+      ) {
+        clusterEndIndex++;
+      }
+
+      // If there are multiple CPU samples in the cluster, draw a line.
+      if (clusterStartIndex !== clusterEndIndex) {
+        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
+        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
+        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
+        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
+        const width = rightPx - leftPx;
+        ctx.fillStyle = colorForSample(callsiteId, false);
+        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
+      }
+
+      // Move to the next cluster.
+      clusterStartIndex = clusterEndIndex + 1;
+    }
+  }
+
+  drawMarker(
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    isHovered: boolean,
+    strokeWidth: number,
+    callsiteId: number,
+  ): void {
+    ctx.beginPath();
+    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
+    ctx.lineTo(x, y + this.markerWidth);
+    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
+    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
+    ctx.closePath();
+    ctx.fillStyle = colorForSample(callsiteId, isHovered);
+    ctx.fill();
+    if (strokeWidth > 0) {
+      ctx.strokeStyle = colorForSample(callsiteId, false);
+      ctx.lineWidth = strokeWidth;
+      ctx.stroke();
+    }
+  }
+
+  onMouseMove({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    const {visibleTimeScale: timeScale} = globals.timeline;
+    const time = timeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+    this.hoveredTs =
+      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+  }
+
+  onMouseOut() {
+    this.hoveredTs = undefined;
+  }
+
+  onMouseClick({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale: timeScale} = globals.timeline;
+
+    const time = timeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
+
+    if (index !== -1) {
+      const id = data.ids[index];
+      const ts = Time.fromRaw(data.tsStarts[index]);
+
+      globals.makeSelection(
+        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
+      );
+      return true;
+    }
+    return false;
+  }
+
+  // If the markers overlap the rightmost one will be selected.
+  findTimestampIndex(
+    left: number,
+    timeScale: TimeScale,
+    data: Data,
+    x: number,
+    y: number,
+    right: number,
+  ): number {
+    let index = -1;
+    if (left !== -1) {
+      const start = Time.fromRaw(data.tsStarts[left]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = left;
+      }
+    }
+    if (right !== -1) {
+      const start = Time.fromRaw(data.tsStarts[right]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = right;
+      }
+    }
+    return index;
+  }
+
+  isInMarker(x: number, y: number, centerX: number) {
+    return (
+      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+    );
+  }
+}
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 364225d..9e8fe84 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -12,255 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {getLegacySelection} from '../../common/state';
-import {Actions} from '../../common/actions';
-import {colorForSample} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
 import {CpuProfileDetailsPanel} from '../../frontend/cpu_profile_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Track,
-} from '../../public';
-import {
-  LONG,
-  NUM,
-  NUM_NULL,
-  STR_NULL,
-} from '../../trace_processor/query_result';
-
-const BAR_HEIGHT = 3;
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {CpuProfileTrack} from './cpu_profile_track';
 
 export const CPU_PROFILE_TRACK_KIND = 'CpuProfileTrack';
 
-interface Data extends TrackData {
-  ids: Float64Array;
-  tsStarts: BigInt64Array;
-  callsiteId: Uint32Array;
-}
-
-class CpuProfileTrack implements Track {
-  private centerY = this.getHeight() / 2 + BAR_HEIGHT;
-  private markerWidth = (this.getHeight() - MARGIN_TOP - BAR_HEIGHT) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: Engine;
-  private utid: number;
-
-  constructor(engine: Engine, utid: number) {
-    this.engine = engine;
-    this.utid = utid;
-  }
-
-  async onUpdate(): Promise<void> {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    const query = `select
-        id,
-        ts,
-        callsite_id as callsiteId
-      from cpu_profile_stack_sample
-      where utid = ${this.utid}
-      order by ts`;
-
-    const result = await this.engine.query(query);
-    const numRows = result.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      ids: new Float64Array(numRows),
-      tsStarts: new BigInt64Array(numRows),
-      callsiteId: new Uint32Array(numRows),
-    };
-
-    const it = result.iter({id: NUM, ts: LONG, callsiteId: NUM});
-    for (let row = 0; it.valid(); it.next(), ++row) {
-      data.ids[row] = it.id;
-      data.tsStarts[row] = it.ts;
-      data.callsiteId[row] = it.callsiteId;
-    }
-
-    return data;
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher.dispose();
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
-    const {visibleTimeScale: timeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'CPU_PROFILE_SAMPLE' &&
-        selection.ts === centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        timeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-        data.callsiteId[i],
-      );
-    }
-
-    // Group together identical identical CPU profile samples by connecting them
-    // with an horizontal bar.
-    let clusterStartIndex = 0;
-    while (clusterStartIndex < data.tsStarts.length) {
-      const callsiteId = data.callsiteId[clusterStartIndex];
-
-      // Find the end of the cluster by searching for the next different CPU
-      // sample. The resulting range [clusterStartIndex, clusterEndIndex] is
-      // inclusive and within array bounds.
-      let clusterEndIndex = clusterStartIndex;
-      while (
-        clusterEndIndex + 1 < data.tsStarts.length &&
-        data.callsiteId[clusterEndIndex + 1] === callsiteId
-      ) {
-        clusterEndIndex++;
-      }
-
-      // If there are multiple CPU samples in the cluster, draw a line.
-      if (clusterStartIndex !== clusterEndIndex) {
-        const startX = Time.fromRaw(data.tsStarts[clusterStartIndex]);
-        const endX = Time.fromRaw(data.tsStarts[clusterEndIndex]);
-        const leftPx = timeScale.timeToPx(startX) - this.markerWidth;
-        const rightPx = timeScale.timeToPx(endX) + this.markerWidth;
-        const width = rightPx - leftPx;
-        ctx.fillStyle = colorForSample(callsiteId, false);
-        ctx.fillRect(leftPx, MARGIN_TOP, width, BAR_HEIGHT);
-      }
-
-      // Move to the next cluster.
-      clusterStartIndex = clusterEndIndex + 1;
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-    callsiteId: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = colorForSample(callsiteId, isHovered);
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = colorForSample(callsiteId, false);
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const {visibleTimeScale: timeScale} = globals.timeline;
-    const time = timeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale: timeScale} = globals.timeline;
-
-    const time = timeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(left, timeScale, data, x, y, right);
-
-    if (index !== -1) {
-      const id = data.ids[index];
-      const ts = Time.fromRaw(data.tsStarts[index]);
-
-      globals.makeSelection(
-        Actions.selectCpuProfileSample({id, utid: this.utid, ts}),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
-    );
-  }
-}
-
 class CpuProfile implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
new file mode 100644
index 0000000..70acb78
--- /dev/null
+++ b/ui/src/core_plugins/cpu_slices/cpu_slice_track.ts
@@ -0,0 +1,474 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {BigintMath as BIMath} from '../../base/bigint_math';
+import {search, searchEq, searchSegment} from '../../base/binary_search';
+import {assertExists, assertTrue} from '../../base/logging';
+import {Duration, duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {getLegacySelection} from '../../common/state';
+import {
+  cropText,
+  drawDoubleHeadedArrow,
+  drawIncompleteSlice,
+  drawTrackHoverTooltip,
+} from '../../common/canvas_utils';
+import {Color} from '../../core/color';
+import {colorForThread} from '../../core/colorizer';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {checkerboardExcept} from '../../frontend/checkerboard';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {Engine, Track} from '../../public';
+import {LONG, NUM} from '../../trace_processor/query_result';
+import {uuidv4Sql} from '../../base/uuid';
+
+export interface Data extends TrackData {
+  // Slices are stored in a columnar fashion. All fields have the same length.
+  ids: Float64Array;
+  startQs: BigInt64Array;
+  endQs: BigInt64Array;
+  utids: Uint32Array;
+  flags: Uint8Array;
+  lastRowId: number;
+}
+
+const MARGIN_TOP = 3;
+const RECT_HEIGHT = 24;
+const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
+
+const CPU_SLICE_FLAGS_INCOMPLETE = 1;
+const CPU_SLICE_FLAGS_REALTIME = 2;
+
+export class CpuSliceTrack implements Track {
+  private mousePos?: {x: number; y: number};
+  private utidHoveredInThisTrack = -1;
+  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
+
+  private lastRowId = -1;
+  private engine: Engine;
+  private cpu: number;
+  private trackKey: string;
+  private trackUuid = uuidv4Sql();
+
+  constructor(engine: Engine, trackKey: string, cpu: number) {
+    this.engine = engine;
+    this.trackKey = trackKey;
+    this.cpu = cpu;
+  }
+
+  async onCreate() {
+    await this.engine.query(`
+      create virtual table cpu_slice_${this.trackUuid}
+      using __intrinsic_slice_mipmap((
+        select
+          id,
+          ts,
+          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
+          0 as depth
+        from sched
+        where cpu = ${this.cpu} and utid != 0
+      ));
+    `);
+    const it = await this.engine.query(`
+      select coalesce(max(id), -1) as lastRowId
+      from sched
+      where cpu = ${this.cpu} and utid != 0
+    `);
+    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
+  }
+
+  async onUpdate() {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
+
+    const queryRes = await this.engine.query(`
+      select
+        (z.ts / ${resolution}) * ${resolution} as tsQ,
+        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
+        s.utid,
+        s.id,
+        s.dur = -1 as isIncomplete,
+        ifnull(s.priority < 100, 0) as isRealtime
+      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
+      cross join sched s using (id)
+    `);
+
+    const numRows = queryRes.numRows();
+    const slices: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      lastRowId: this.lastRowId,
+      ids: new Float64Array(numRows),
+      startQs: new BigInt64Array(numRows),
+      endQs: new BigInt64Array(numRows),
+      utids: new Uint32Array(numRows),
+      flags: new Uint8Array(numRows),
+    };
+
+    const it = queryRes.iter({
+      tsQ: LONG,
+      tsEndQ: LONG,
+      utid: NUM,
+      id: NUM,
+      isIncomplete: NUM,
+      isRealtime: NUM,
+    });
+    for (let row = 0; it.valid(); it.next(), row++) {
+      slices.startQs[row] = it.tsQ;
+      slices.endQs[row] = it.tsEndQ;
+      slices.utids[row] = it.utid;
+      slices.ids[row] = it.id;
+
+      slices.flags[row] = 0;
+      if (it.isIncomplete) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
+      }
+      if (it.isRealtime) {
+        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
+      }
+    }
+    return slices;
+  }
+
+  async onDestroy() {
+    await this.engine.tryQuery(
+      `drop table if exists cpu_slice_${this.trackUuid}`,
+    );
+    this.fetcher.dispose();
+  }
+
+  getHeight(): number {
+    return TRACK_HEIGHT;
+  }
+
+  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
+    // TODO: fonts and colors should come from the CSS and not hardcoded here.
+    const {visibleTimeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return; // Can't possibly draw anything.
+
+    // If the cached trace slices don't fully cover the visible time range,
+    // show a gray rectangle with a "Loading..." label.
+    checkerboardExcept(
+      ctx,
+      this.getHeight(),
+      0,
+      size.width,
+      visibleTimeScale.timeToPx(data.start),
+      visibleTimeScale.timeToPx(data.end),
+    );
+
+    this.renderSlices(ctx, data);
+  }
+
+  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
+    const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
+      globals.timeline;
+    assertTrue(data.startQs.length === data.endQs.length);
+    assertTrue(data.startQs.length === data.utids.length);
+
+    const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
+
+    ctx.textAlign = 'center';
+    ctx.font = '12px Roboto Condensed';
+    const charWidth = ctx.measureText('dbpqaouk').width / 8;
+
+    const startTime = visibleTimeSpan.start;
+    const endTime = visibleTimeSpan.end;
+
+    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
+    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
+
+    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
+    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
+
+    for (let i = startIdx; i < endIdx; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      let tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+
+      // If the last slice is incomplete, it should end with the end of the
+      // window, else it might spill over the window and the end would not be
+      // visible as a zigzag line.
+      if (
+        data.ids[i] === data.lastRowId &&
+        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
+      ) {
+        tEnd = endTime;
+      }
+      const rectStart = visibleTimeScale.timeToPx(tStart);
+      const rectEnd = visibleTimeScale.timeToPx(tEnd);
+      const rectWidth = Math.max(1, rectEnd - rectStart);
+
+      const threadInfo = globals.threads.get(utid);
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
+
+      const isHovering = globals.state.hoveredUtid !== -1;
+      const isThreadHovered = globals.state.hoveredUtid === utid;
+      const isProcessHovered = globals.state.hoveredPid === pid;
+      const colorScheme = colorForThread(threadInfo);
+      let color: Color;
+      let textColor: Color;
+      if (isHovering && !isThreadHovered) {
+        if (!isProcessHovered) {
+          color = colorScheme.disabled;
+          textColor = colorScheme.textDisabled;
+        } else {
+          color = colorScheme.variant;
+          textColor = colorScheme.textVariant;
+        }
+      } else {
+        color = colorScheme.base;
+        textColor = colorScheme.textBase;
+      }
+      ctx.fillStyle = color.cssString;
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
+        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      } else {
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // Don't render text when we have less than 5px to play with.
+      if (rectWidth < 5) continue;
+
+      // Stylize real-time threads. We don't do it when zoomed out as the
+      // fillRect is expensive.
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        ctx.fillStyle = getHatchedPattern(ctx);
+        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
+      }
+
+      // TODO: consider de-duplicating this code with the copied one from
+      // chrome_slices/frontend.ts.
+      let title = `[utid:${utid}]`;
+      let subTitle = '';
+      if (threadInfo) {
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
+        if (threadInfo.pid) {
+          /* eslint-enable */
+          let procName = threadInfo.procName || '';
+          if (procName.startsWith('/')) {
+            // Remove folder paths from name
+            procName = procName.substring(procName.lastIndexOf('/') + 1);
+          }
+          title = `${procName} [${threadInfo.pid}]`;
+          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        } else {
+          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
+        }
+      }
+
+      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
+        subTitle = subTitle + ' (RT)';
+      }
+
+      const right = Math.min(visWindowEndPx, rectEnd);
+      const left = Math.max(rectStart, 0);
+      const visibleWidth = Math.max(right - left, 1);
+      title = cropText(title, charWidth, visibleWidth);
+      subTitle = cropText(subTitle, charWidth, visibleWidth);
+      const rectXCenter = left + visibleWidth / 2;
+      ctx.fillStyle = textColor.cssString;
+      ctx.font = '12px Roboto Condensed';
+      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
+      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
+      ctx.font = '10px Roboto Condensed';
+      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
+    }
+
+    const selection = getLegacySelection(globals.state);
+    const details = globals.sliceDetails;
+    if (selection !== null && selection.kind === 'SLICE') {
+      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
+      if (startIndex !== endIndex) {
+        const tStart = Time.fromRaw(data.startQs[startIndex]);
+        const tEnd = Time.fromRaw(data.endQs[startIndex]);
+        const utid = data.utids[startIndex];
+        const color = colorForThread(globals.threads.get(utid));
+        const rectStart = visibleTimeScale.timeToPx(tStart);
+        const rectEnd = visibleTimeScale.timeToPx(tEnd);
+        const rectWidth = Math.max(1, rectEnd - rectStart);
+
+        // Draw a rectangle around the slice that is currently selected.
+        ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
+        ctx.beginPath();
+        ctx.lineWidth = 3;
+        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
+        ctx.closePath();
+        // Draw arrow from wakeup time of current slice.
+        if (details.wakeupTs) {
+          const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
+          const latencyWidth = rectStart - wakeupPos;
+          drawDoubleHeadedArrow(
+            ctx,
+            wakeupPos,
+            MARGIN_TOP + RECT_HEIGHT,
+            latencyWidth,
+            latencyWidth >= 20,
+          );
+          // Latency time with a white semi-transparent background.
+          const latency = tStart - details.wakeupTs;
+          const displayText = Duration.humanise(latency);
+          const measured = ctx.measureText(displayText);
+          if (latencyWidth >= measured.width + 2) {
+            ctx.fillStyle = 'rgba(255,255,255,0.7)';
+            ctx.fillRect(
+              wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
+              MARGIN_TOP + RECT_HEIGHT - 12,
+              measured.width + 2,
+              11,
+            );
+            ctx.textBaseline = 'bottom';
+            ctx.fillStyle = 'black';
+            ctx.fillText(
+              displayText,
+              wakeupPos + latencyWidth / 2,
+              MARGIN_TOP + RECT_HEIGHT - 1,
+            );
+          }
+        }
+      }
+
+      // Draw diamond if the track being drawn is the cpu of the waker.
+      if (this.cpu === details.wakerCpu && details.wakeupTs) {
+        const wakeupPos = Math.floor(
+          visibleTimeScale.timeToPx(details.wakeupTs),
+        );
+        ctx.beginPath();
+        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
+        ctx.fillStyle = 'black';
+        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
+        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
+        ctx.fill();
+        ctx.closePath();
+      }
+    }
+
+    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
+    const maxHeight = this.getHeight();
+    if (hoveredThread !== undefined && this.mousePos !== undefined) {
+      const tidText = `T: ${hoveredThread.threadName}
+      [${hoveredThread.tid}]`;
+      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+      if (hoveredThread.pid) {
+        const pidText = `P: ${hoveredThread.procName}
+        [${hoveredThread.pid}]`;
+        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
+      } else {
+        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
+      }
+    }
+  }
+
+  onMouseMove(pos: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    this.mousePos = pos;
+    if (data === undefined) return;
+    const {visibleTimeScale} = globals.timeline;
+    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
+      this.utidHoveredInThisTrack = -1;
+      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+      return;
+    }
+    const t = visibleTimeScale.pxToHpTime(pos.x);
+    let hoveredUtid = -1;
+
+    for (let i = 0; i < data.startQs.length; i++) {
+      const tStart = Time.fromRaw(data.startQs[i]);
+      const tEnd = Time.fromRaw(data.endQs[i]);
+      const utid = data.utids[i];
+      if (t.gte(tStart) && t.lt(tEnd)) {
+        hoveredUtid = utid;
+        break;
+      }
+    }
+    this.utidHoveredInThisTrack = hoveredUtid;
+    const threadInfo = globals.threads.get(hoveredUtid);
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
+    globals.dispatch(
+      Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
+    );
+  }
+
+  onMouseOut() {
+    this.utidHoveredInThisTrack = -1;
+    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
+    this.mousePos = undefined;
+  }
+
+  onMouseClick({x}: {x: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x);
+    const index = search(data.startQs, time.toTime());
+    const id = index === -1 ? undefined : data.ids[index];
+    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
+    if (!id || this.utidHoveredInThisTrack === -1) return false;
+
+    globals.setLegacySelection(
+      {
+        kind: 'SLICE',
+        id,
+        trackKey: this.trackKey,
+      },
+      {
+        clearSearch: true,
+        pendingScrollId: undefined,
+        switchToCurrentSelectionTab: true,
+      },
+    );
+
+    return true;
+  }
+}
+
+// Creates a diagonal hatched pattern to be used for distinguishing slices with
+// real-time priorities. The pattern is created once as an offscreen canvas and
+// is kept cached inside the Context2D of the main canvas, without making
+// assumptions on the lifetime of the main canvas.
+function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
+  const mctx = mainCtx as CanvasRenderingContext2D & {
+    sliceHatchedPattern?: CanvasPattern;
+  };
+  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
+  const canvas = document.createElement('canvas');
+  const SIZE = 8;
+  canvas.width = canvas.height = SIZE;
+  const ctx = assertExists(canvas.getContext('2d'));
+  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
+  ctx.beginPath();
+  ctx.lineWidth = 1;
+  ctx.moveTo(0, SIZE);
+  ctx.lineTo(SIZE, 0);
+  ctx.stroke();
+  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
+  return mctx.sliceHatchedPattern;
+}
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index 1fc1e67..3f456a3 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -12,458 +12,21 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {BigintMath as BIMath} from '../../base/bigint_math';
-import {search, searchEq, searchSegment} from '../../base/binary_search';
-import {assertExists, assertTrue} from '../../base/logging';
-import {Duration, duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {getLegacySelection} from '../../common/state';
-import {
-  cropText,
-  drawDoubleHeadedArrow,
-  drawIncompleteSlice,
-  drawTrackHoverTooltip,
-} from '../../common/canvas_utils';
-import {Color} from '../../core/color';
-import {colorForThread} from '../../core/colorizer';
-import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {checkerboardExcept} from '../../frontend/checkerboard';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
 import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
 import {
   Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
-  Track,
 } from '../../public';
-import {LONG, NUM, STR_NULL} from '../../trace_processor/query_result';
-import {uuidv4Sql} from '../../base/uuid';
+import {NUM, STR_NULL} from '../../trace_processor/query_result';
+import {CpuSliceTrack} from './cpu_slice_track';
 
 export const CPU_SLICE_TRACK_KIND = 'CpuSliceTrack';
 
-export interface Data extends TrackData {
-  // Slices are stored in a columnar fashion. All fields have the same length.
-  ids: Float64Array;
-  startQs: BigInt64Array;
-  endQs: BigInt64Array;
-  utids: Uint32Array;
-  flags: Uint8Array;
-  lastRowId: number;
-}
-
-const MARGIN_TOP = 3;
-const RECT_HEIGHT = 24;
-const TRACK_HEIGHT = MARGIN_TOP * 2 + RECT_HEIGHT;
-
-const CPU_SLICE_FLAGS_INCOMPLETE = 1;
-const CPU_SLICE_FLAGS_REALTIME = 2;
-
-class CpuSliceTrack implements Track {
-  private mousePos?: {x: number; y: number};
-  private utidHoveredInThisTrack = -1;
-  private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-
-  private lastRowId = -1;
-  private engine: Engine;
-  private cpu: number;
-  private trackKey: string;
-  private trackUuid = uuidv4Sql();
-
-  constructor(engine: Engine, trackKey: string, cpu: number) {
-    this.engine = engine;
-    this.trackKey = trackKey;
-    this.cpu = cpu;
-  }
-
-  async onCreate() {
-    await this.engine.query(`
-      create virtual table cpu_slice_${this.trackUuid}
-      using __intrinsic_slice_mipmap((
-        select
-          id,
-          ts,
-          iif(dur = -1, lead(ts, 1, trace_end()) over (order by ts) - ts, dur),
-          0 as depth
-        from sched
-        where cpu = ${this.cpu} and utid != 0
-      ));
-    `);
-    const it = await this.engine.query(`
-      select coalesce(max(id), -1) as lastRowId
-      from sched
-      where cpu = ${this.cpu} and utid != 0
-    `);
-    this.lastRowId = it.firstRow({lastRowId: NUM}).lastRowId;
-  }
-
-  async onUpdate() {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    assertTrue(BIMath.popcount(resolution) === 1, `${resolution} not pow of 2`);
-
-    const queryRes = await this.engine.query(`
-      select
-        (z.ts / ${resolution}) * ${resolution} as tsQ,
-        (((z.ts + z.dur) / ${resolution}) + 1) * ${resolution} as tsEndQ,
-        s.utid,
-        s.id,
-        s.dur = -1 as isIncomplete,
-        ifnull(s.priority < 100, 0) as isRealtime
-      from cpu_slice_${this.trackUuid}(${start}, ${end}, ${resolution}) z
-      cross join sched s using (id)
-    `);
-
-    const numRows = queryRes.numRows();
-    const slices: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      lastRowId: this.lastRowId,
-      ids: new Float64Array(numRows),
-      startQs: new BigInt64Array(numRows),
-      endQs: new BigInt64Array(numRows),
-      utids: new Uint32Array(numRows),
-      flags: new Uint8Array(numRows),
-    };
-
-    const it = queryRes.iter({
-      tsQ: LONG,
-      tsEndQ: LONG,
-      utid: NUM,
-      id: NUM,
-      isIncomplete: NUM,
-      isRealtime: NUM,
-    });
-    for (let row = 0; it.valid(); it.next(), row++) {
-      slices.startQs[row] = it.tsQ;
-      slices.endQs[row] = it.tsEndQ;
-      slices.utids[row] = it.utid;
-      slices.ids[row] = it.id;
-
-      slices.flags[row] = 0;
-      if (it.isIncomplete) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_INCOMPLETE;
-      }
-      if (it.isRealtime) {
-        slices.flags[row] |= CPU_SLICE_FLAGS_REALTIME;
-      }
-    }
-    return slices;
-  }
-
-  async onDestroy() {
-    if (this.engine.isAlive) {
-      await this.engine.query(
-        `drop table if exists cpu_slice_${this.trackUuid}`,
-      );
-    }
-    this.fetcher.dispose();
-  }
-
-  getHeight(): number {
-    return TRACK_HEIGHT;
-  }
-
-  render(ctx: CanvasRenderingContext2D, size: PanelSize): void {
-    // TODO: fonts and colors should come from the CSS and not hardcoded here.
-    const {visibleTimeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return; // Can't possibly draw anything.
-
-    // If the cached trace slices don't fully cover the visible time range,
-    // show a gray rectangle with a "Loading..." label.
-    checkerboardExcept(
-      ctx,
-      this.getHeight(),
-      0,
-      size.width,
-      visibleTimeScale.timeToPx(data.start),
-      visibleTimeScale.timeToPx(data.end),
-    );
-
-    this.renderSlices(ctx, data);
-  }
-
-  renderSlices(ctx: CanvasRenderingContext2D, data: Data): void {
-    const {visibleTimeScale, visibleTimeSpan, visibleWindowTime} =
-      globals.timeline;
-    assertTrue(data.startQs.length === data.endQs.length);
-    assertTrue(data.startQs.length === data.utids.length);
-
-    const visWindowEndPx = visibleTimeScale.hpTimeToPx(visibleWindowTime.end);
-
-    ctx.textAlign = 'center';
-    ctx.font = '12px Roboto Condensed';
-    const charWidth = ctx.measureText('dbpqaouk').width / 8;
-
-    const startTime = visibleTimeSpan.start;
-    const endTime = visibleTimeSpan.end;
-
-    const rawStartIdx = data.endQs.findIndex((end) => end >= startTime);
-    const startIdx = rawStartIdx === -1 ? 0 : rawStartIdx;
-
-    const [, rawEndIdx] = searchSegment(data.startQs, endTime);
-    const endIdx = rawEndIdx === -1 ? data.startQs.length : rawEndIdx;
-
-    for (let i = startIdx; i < endIdx; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      let tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-
-      // If the last slice is incomplete, it should end with the end of the
-      // window, else it might spill over the window and the end would not be
-      // visible as a zigzag line.
-      if (
-        data.ids[i] === data.lastRowId &&
-        data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE
-      ) {
-        tEnd = endTime;
-      }
-      const rectStart = visibleTimeScale.timeToPx(tStart);
-      const rectEnd = visibleTimeScale.timeToPx(tEnd);
-      const rectWidth = Math.max(1, rectEnd - rectStart);
-
-      const threadInfo = globals.threads.get(utid);
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      const pid = threadInfo && threadInfo.pid ? threadInfo.pid : -1;
-
-      const isHovering = globals.state.hoveredUtid !== -1;
-      const isThreadHovered = globals.state.hoveredUtid === utid;
-      const isProcessHovered = globals.state.hoveredPid === pid;
-      const colorScheme = colorForThread(threadInfo);
-      let color: Color;
-      let textColor: Color;
-      if (isHovering && !isThreadHovered) {
-        if (!isProcessHovered) {
-          color = colorScheme.disabled;
-          textColor = colorScheme.textDisabled;
-        } else {
-          color = colorScheme.variant;
-          textColor = colorScheme.textVariant;
-        }
-      } else {
-        color = colorScheme.base;
-        textColor = colorScheme.textBase;
-      }
-      ctx.fillStyle = color.cssString;
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_INCOMPLETE) {
-        drawIncompleteSlice(ctx, rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      } else {
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // Don't render text when we have less than 5px to play with.
-      if (rectWidth < 5) continue;
-
-      // Stylize real-time threads. We don't do it when zoomed out as the
-      // fillRect is expensive.
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        ctx.fillStyle = getHatchedPattern(ctx);
-        ctx.fillRect(rectStart, MARGIN_TOP, rectWidth, RECT_HEIGHT);
-      }
-
-      // TODO: consider de-duplicating this code with the copied one from
-      // chrome_slices/frontend.ts.
-      let title = `[utid:${utid}]`;
-      let subTitle = '';
-      if (threadInfo) {
-        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-        if (threadInfo.pid) {
-          /* eslint-enable */
-          let procName = threadInfo.procName || '';
-          if (procName.startsWith('/')) {
-            // Remove folder paths from name
-            procName = procName.substring(procName.lastIndexOf('/') + 1);
-          }
-          title = `${procName} [${threadInfo.pid}]`;
-          subTitle = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        } else {
-          title = `${threadInfo.threadName} [${threadInfo.tid}]`;
-        }
-      }
-
-      if (data.flags[i] & CPU_SLICE_FLAGS_REALTIME) {
-        subTitle = subTitle + ' (RT)';
-      }
-
-      const right = Math.min(visWindowEndPx, rectEnd);
-      const left = Math.max(rectStart, 0);
-      const visibleWidth = Math.max(right - left, 1);
-      title = cropText(title, charWidth, visibleWidth);
-      subTitle = cropText(subTitle, charWidth, visibleWidth);
-      const rectXCenter = left + visibleWidth / 2;
-      ctx.fillStyle = textColor.cssString;
-      ctx.font = '12px Roboto Condensed';
-      ctx.fillText(title, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 - 1);
-      ctx.fillStyle = textColor.setAlpha(0.6).cssString;
-      ctx.font = '10px Roboto Condensed';
-      ctx.fillText(subTitle, rectXCenter, MARGIN_TOP + RECT_HEIGHT / 2 + 9);
-    }
-
-    const selection = getLegacySelection(globals.state);
-    const details = globals.sliceDetails;
-    if (selection !== null && selection.kind === 'SLICE') {
-      const [startIndex, endIndex] = searchEq(data.ids, selection.id);
-      if (startIndex !== endIndex) {
-        const tStart = Time.fromRaw(data.startQs[startIndex]);
-        const tEnd = Time.fromRaw(data.endQs[startIndex]);
-        const utid = data.utids[startIndex];
-        const color = colorForThread(globals.threads.get(utid));
-        const rectStart = visibleTimeScale.timeToPx(tStart);
-        const rectEnd = visibleTimeScale.timeToPx(tEnd);
-        const rectWidth = Math.max(1, rectEnd - rectStart);
-
-        // Draw a rectangle around the slice that is currently selected.
-        ctx.strokeStyle = color.base.setHSL({l: 30}).cssString;
-        ctx.beginPath();
-        ctx.lineWidth = 3;
-        ctx.strokeRect(rectStart, MARGIN_TOP - 1.5, rectWidth, RECT_HEIGHT + 3);
-        ctx.closePath();
-        // Draw arrow from wakeup time of current slice.
-        if (details.wakeupTs) {
-          const wakeupPos = visibleTimeScale.timeToPx(details.wakeupTs);
-          const latencyWidth = rectStart - wakeupPos;
-          drawDoubleHeadedArrow(
-            ctx,
-            wakeupPos,
-            MARGIN_TOP + RECT_HEIGHT,
-            latencyWidth,
-            latencyWidth >= 20,
-          );
-          // Latency time with a white semi-transparent background.
-          const latency = tStart - details.wakeupTs;
-          const displayText = Duration.humanise(latency);
-          const measured = ctx.measureText(displayText);
-          if (latencyWidth >= measured.width + 2) {
-            ctx.fillStyle = 'rgba(255,255,255,0.7)';
-            ctx.fillRect(
-              wakeupPos + latencyWidth / 2 - measured.width / 2 - 1,
-              MARGIN_TOP + RECT_HEIGHT - 12,
-              measured.width + 2,
-              11,
-            );
-            ctx.textBaseline = 'bottom';
-            ctx.fillStyle = 'black';
-            ctx.fillText(
-              displayText,
-              wakeupPos + latencyWidth / 2,
-              MARGIN_TOP + RECT_HEIGHT - 1,
-            );
-          }
-        }
-      }
-
-      // Draw diamond if the track being drawn is the cpu of the waker.
-      if (this.cpu === details.wakerCpu && details.wakeupTs) {
-        const wakeupPos = Math.floor(
-          visibleTimeScale.timeToPx(details.wakeupTs),
-        );
-        ctx.beginPath();
-        ctx.moveTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 + 8);
-        ctx.fillStyle = 'black';
-        ctx.lineTo(wakeupPos + 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.lineTo(wakeupPos, MARGIN_TOP + RECT_HEIGHT / 2 - 8);
-        ctx.lineTo(wakeupPos - 6, MARGIN_TOP + RECT_HEIGHT / 2);
-        ctx.fill();
-        ctx.closePath();
-      }
-    }
-
-    const hoveredThread = globals.threads.get(this.utidHoveredInThisTrack);
-    const maxHeight = this.getHeight();
-    if (hoveredThread !== undefined && this.mousePos !== undefined) {
-      const tidText = `T: ${hoveredThread.threadName}
-      [${hoveredThread.tid}]`;
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (hoveredThread.pid) {
-        const pidText = `P: ${hoveredThread.procName}
-        [${hoveredThread.pid}]`;
-        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, pidText, tidText);
-      } else {
-        drawTrackHoverTooltip(ctx, this.mousePos, maxHeight, tidText);
-      }
-    }
-  }
-
-  onMouseMove(pos: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    this.mousePos = pos;
-    if (data === undefined) return;
-    const {visibleTimeScale} = globals.timeline;
-    if (pos.y < MARGIN_TOP || pos.y > MARGIN_TOP + RECT_HEIGHT) {
-      this.utidHoveredInThisTrack = -1;
-      globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-      return;
-    }
-    const t = visibleTimeScale.pxToHpTime(pos.x);
-    let hoveredUtid = -1;
-
-    for (let i = 0; i < data.startQs.length; i++) {
-      const tStart = Time.fromRaw(data.startQs[i]);
-      const tEnd = Time.fromRaw(data.endQs[i]);
-      const utid = data.utids[i];
-      if (t.gte(tStart) && t.lt(tEnd)) {
-        hoveredUtid = utid;
-        break;
-      }
-    }
-    this.utidHoveredInThisTrack = hoveredUtid;
-    const threadInfo = globals.threads.get(hoveredUtid);
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    const hoveredPid = threadInfo ? (threadInfo.pid ? threadInfo.pid : -1) : -1;
-    globals.dispatch(
-      Actions.setHoveredUtidAndPid({utid: hoveredUtid, pid: hoveredPid}),
-    );
-  }
-
-  onMouseOut() {
-    this.utidHoveredInThisTrack = -1;
-    globals.dispatch(Actions.setHoveredUtidAndPid({utid: -1, pid: -1}));
-    this.mousePos = undefined;
-  }
-
-  onMouseClick({x}: {x: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x);
-    const index = search(data.startQs, time.toTime());
-    const id = index === -1 ? undefined : data.ids[index];
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!id || this.utidHoveredInThisTrack === -1) return false;
-
-    globals.setLegacySelection(
-      {
-        kind: 'SLICE',
-        id,
-        trackKey: this.trackKey,
-      },
-      {
-        clearSearch: true,
-        pendingScrollId: undefined,
-        switchToCurrentSelectionTab: true,
-      },
-    );
-
-    return true;
-  }
-}
-
 class CpuSlices implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const cpus = await ctx.engine.getCpus();
+    const cpus = ctx.trace.cpus;
     const cpuToSize = await this.guessCpuSizes(ctx.engine);
 
     for (const cpu of cpus) {
@@ -516,29 +79,6 @@
   }
 }
 
-// Creates a diagonal hatched pattern to be used for distinguishing slices with
-// real-time priorities. The pattern is created once as an offscreen canvas and
-// is kept cached inside the Context2D of the main canvas, without making
-// assumptions on the lifetime of the main canvas.
-function getHatchedPattern(mainCtx: CanvasRenderingContext2D): CanvasPattern {
-  const mctx = mainCtx as CanvasRenderingContext2D & {
-    sliceHatchedPattern?: CanvasPattern;
-  };
-  if (mctx.sliceHatchedPattern !== undefined) return mctx.sliceHatchedPattern;
-  const canvas = document.createElement('canvas');
-  const SIZE = 8;
-  canvas.width = canvas.height = SIZE;
-  const ctx = assertExists(canvas.getContext('2d'));
-  ctx.strokeStyle = 'rgba(255,255,255,0.3)';
-  ctx.beginPath();
-  ctx.lineWidth = 1;
-  ctx.moveTo(0, SIZE);
-  ctx.lineTo(SIZE, 0);
-  ctx.stroke();
-  mctx.sliceHatchedPattern = assertExists(mctx.createPattern(canvas, 'repeat'));
-  return mctx.sliceHatchedPattern;
-}
-
 export const plugin: PluginDescriptor = {
   pluginId: 'perfetto.CpuSlices',
   plugin: CpuSlices,
diff --git a/ui/src/core_plugins/debug/counter_track.ts b/ui/src/core_plugins/debug/counter_track.ts
index 56274d3..5c0022d 100644
--- a/ui/src/core_plugins/debug/counter_track.ts
+++ b/ui/src/core_plugins/debug/counter_track.ts
@@ -67,8 +67,6 @@
   }
 
   private async dropTrackTable(): Promise<void> {
-    if (this.engine.isAlive) {
-      this.engine.query(`drop table if exists ${this.sqlTableName}`);
-    }
+    this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/core_plugins/debug/slice_track.ts b/ui/src/core_plugins/debug/slice_track.ts
index 49e5142..a0734d9 100644
--- a/ui/src/core_plugins/debug/slice_track.ts
+++ b/ui/src/core_plugins/debug/slice_track.ts
@@ -19,7 +19,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
+} from '../../frontend/tracks/custom_sql_table_slice_track';
 
 import {DebugSliceDetailsTab} from './details_tab';
 import {
@@ -111,8 +111,6 @@
   }
 
   private async destroyTrackTable() {
-    if (this.engine.isAlive) {
-      await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/core_plugins/frames/actual_frames_track_v2.ts b/ui/src/core_plugins/frames/actual_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/actual_frames_track_v2.ts
rename to ui/src/core_plugins/frames/actual_frames_track.ts
diff --git a/ui/src/core_plugins/frames/expected_frames_track_v2.ts b/ui/src/core_plugins/frames/expected_frames_track.ts
similarity index 100%
rename from ui/src/core_plugins/frames/expected_frames_track_v2.ts
rename to ui/src/core_plugins/frames/expected_frames_track.ts
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 2cc877f..8e67de9 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -16,8 +16,8 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 
-import {ActualFramesTrack as ActualFramesTrackV2} from './actual_frames_track_v2';
-import {ExpectedFramesTrack as ExpectedFramesTrackV2} from './expected_frames_track_v2';
+import {ActualFramesTrack} from './actual_frames_track';
+import {ExpectedFramesTrack} from './expected_frames_track';
 
 export const EXPECTED_FRAMES_SLICE_TRACK_KIND = 'ExpectedFramesSliceTrack';
 export const ACTUAL_FRAMES_SLICE_TRACK_KIND = 'ActualFramesSliceTrack';
@@ -75,12 +75,7 @@
         trackIds,
         kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new ExpectedFramesTrackV2(
-            engine,
-            maxDepth,
-            trackKey,
-            trackIds,
-          );
+          return new ExpectedFramesTrack(engine, maxDepth, trackKey, trackIds);
         },
       });
     }
@@ -138,7 +133,7 @@
         trackIds,
         kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new ActualFramesTrackV2(engine, maxDepth, trackKey, trackIds);
+          return new ActualFramesTrack(engine, maxDepth, trackKey, trackIds);
         },
       });
     }
diff --git a/ui/src/core_plugins/heap_profile/heap_profile_track.ts b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
new file mode 100644
index 0000000..9ecd947
--- /dev/null
+++ b/ui/src/core_plugins/heap_profile/heap_profile_track.ts
@@ -0,0 +1,108 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {ProfileType, LegacySelection} from '../../common/state';
+import {profileType} from '../../controller/flamegraph_controller';
+import {
+  BASE_ROW,
+  BaseSliceTrack,
+  BaseSliceTrackTypes,
+  OnSliceClickArgs,
+  OnSliceOverArgs,
+} from '../../frontend/base_slice_track';
+import {globals} from '../../frontend/globals';
+import {NewTrackArgs} from '../../frontend/track';
+import {Slice} from '../../public';
+import {STR} from '../../trace_processor/query_result';
+
+export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
+
+const HEAP_PROFILE_ROW = {
+  ...BASE_ROW,
+  type: STR,
+};
+type HeapProfileRow = typeof HEAP_PROFILE_ROW;
+interface HeapProfileSlice extends Slice {
+  type: ProfileType;
+}
+
+interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
+  row: HeapProfileRow;
+  slice: HeapProfileSlice;
+}
+
+export class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
+  private upid: number;
+
+  constructor(args: NewTrackArgs, upid: number) {
+    super(args);
+    this.upid = upid;
+  }
+
+  getSqlSource(): string {
+    return `select
+      *,
+      0 AS dur,
+      0 AS depth
+      from (
+        select distinct
+          id,
+          ts,
+          'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
+        from heap_profile_allocation
+        where upid = ${this.upid}
+        union
+        select distinct
+          id,
+          graph_sample_ts AS ts,
+          'graph' AS type
+        from heap_graph_object
+        where upid = ${this.upid}
+      )`;
+  }
+
+  getRowSpec(): HeapProfileRow {
+    return HEAP_PROFILE_ROW;
+  }
+
+  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
+    const slice = super.rowToSlice(row);
+    let type = row.type;
+    if (type === 'heap_profile:libc.malloc,com.android.art') {
+      type = 'heap_profile:com.android.art,libc.malloc';
+    }
+    slice.type = profileType(type);
+    return slice;
+  }
+
+  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
+    args.tooltip = [args.slice.type];
+  }
+
+  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
+    globals.makeSelection(
+      Actions.selectHeapProfile({
+        id: args.slice.id,
+        upid: this.upid,
+        ts: args.slice.ts,
+        type: args.slice.type,
+      }),
+    );
+  }
+
+  protected isSelectionHandled(selection: LegacySelection): boolean {
+    return selection.kind === 'HEAP_PROFILE';
+  }
+}
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 512af1a..c3576f2 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -12,107 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Actions} from '../../common/actions';
-import {ProfileType, LegacySelection} from '../../common/state';
-import {profileType} from '../../controller/flamegraph_controller';
-import {
-  BASE_ROW,
-  BaseSliceTrack,
-  BaseSliceTrackTypes,
-  OnSliceClickArgs,
-  OnSliceOverArgs,
-} from '../../frontend/base_slice_track';
 import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {NewTrackArgs} from '../../frontend/track';
-import {
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Slice,
-} from '../../public';
-import {NUM, STR} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {HeapProfileTrack} from './heap_profile_track';
 
 export const HEAP_PROFILE_TRACK_KIND = 'HeapProfileTrack';
 
-const HEAP_PROFILE_ROW = {
-  ...BASE_ROW,
-  type: STR,
-};
-type HeapProfileRow = typeof HEAP_PROFILE_ROW;
-interface HeapProfileSlice extends Slice {
-  type: ProfileType;
-}
-
-interface HeapProfileTrackTypes extends BaseSliceTrackTypes {
-  row: HeapProfileRow;
-  slice: HeapProfileSlice;
-}
-
-class HeapProfileTrack extends BaseSliceTrack<HeapProfileTrackTypes> {
-  private upid: number;
-
-  constructor(args: NewTrackArgs, upid: number) {
-    super(args);
-    this.upid = upid;
-  }
-
-  getSqlSource(): string {
-    return `select
-      *,
-      0 AS dur,
-      0 AS depth
-      from (
-        select distinct
-          id,
-          ts,
-          'heap_profile:' || (select group_concat(distinct heap_name) from heap_profile_allocation where upid = ${this.upid}) AS type
-        from heap_profile_allocation
-        where upid = ${this.upid}
-        union
-        select distinct
-          id,
-          graph_sample_ts AS ts,
-          'graph' AS type
-        from heap_graph_object
-        where upid = ${this.upid}
-      )`;
-  }
-
-  getRowSpec(): HeapProfileRow {
-    return HEAP_PROFILE_ROW;
-  }
-
-  rowToSlice(row: HeapProfileRow): HeapProfileSlice {
-    const slice = super.rowToSlice(row);
-    let type = row.type;
-    if (type === 'heap_profile:libc.malloc,com.android.art') {
-      type = 'heap_profile:com.android.art,libc.malloc';
-    }
-    slice.type = profileType(type);
-    return slice;
-  }
-
-  onSliceOver(args: OnSliceOverArgs<HeapProfileSlice>) {
-    args.tooltip = [args.slice.type];
-  }
-
-  onSliceClick(args: OnSliceClickArgs<HeapProfileSlice>) {
-    globals.makeSelection(
-      Actions.selectHeapProfile({
-        id: args.slice.id,
-        upid: this.upid,
-        ts: args.slice.ts,
-        type: args.slice.type,
-      }),
-    );
-  }
-
-  protected isSelectionHandled(selection: LegacySelection): boolean {
-    return selection.kind === 'HEAP_PROFILE';
-  }
-}
-
 class HeapProfilePlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 319fb25..df9b7c0 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -12,25 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {searchSegment} from '../../base/binary_search';
-import {duration, Time, time} from '../../base/time';
-import {Actions} from '../../common/actions';
-import {ProfileType, getLegacySelection} from '../../common/state';
 import {TrackData} from '../../common/track_data';
-import {TimelineFetcher} from '../../common/track_helper';
-import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
 import {FlamegraphDetailsPanel} from '../../frontend/flamegraph_panel';
-import {globals} from '../../frontend/globals';
-import {PanelSize} from '../../frontend/panel';
-import {TimeScale} from '../../frontend/time_scale';
-import {
-  Engine,
-  Plugin,
-  PluginContextTrace,
-  PluginDescriptor,
-  Track,
-} from '../../public';
-import {LONG, NUM} from '../../trace_processor/query_result';
+import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
+import {NUM} from '../../trace_processor/query_result';
+import {PerfSamplesProfileTrack} from './perf_samples_profile_track';
 
 export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
 
@@ -38,211 +24,6 @@
   tsStarts: BigInt64Array;
 }
 
-const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
-
-// 0.5 Makes the horizontal lines sharp.
-const MARGIN_TOP = 4.5;
-const RECT_HEIGHT = 30.5;
-
-class PerfSamplesProfileTrack implements Track {
-  private centerY = this.getHeight() / 2;
-  private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
-  private hoveredTs: time | undefined = undefined;
-  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private upid: number;
-  private engine: Engine;
-
-  constructor(engine: Engine, upid: number) {
-    this.upid = upid;
-    this.engine = engine;
-  }
-
-  async onUpdate(): Promise<void> {
-    await this.fetcher.requestDataForCurrentTime();
-  }
-
-  async onDestroy(): Promise<void> {
-    this.fetcher.dispose();
-  }
-
-  async onBoundsChange(
-    start: time,
-    end: time,
-    resolution: duration,
-  ): Promise<Data> {
-    if (this.upid === undefined) {
-      return {
-        start,
-        end,
-        resolution,
-        length: 0,
-        tsStarts: new BigInt64Array(),
-      };
-    }
-    const queryRes = await this.engine.query(`
-      select ts, upid from perf_sample
-      join thread using (utid)
-      where upid = ${this.upid}
-      and callsite_id is not null
-      order by ts`);
-    const numRows = queryRes.numRows();
-    const data: Data = {
-      start,
-      end,
-      resolution,
-      length: numRows,
-      tsStarts: new BigInt64Array(numRows),
-    };
-
-    const it = queryRes.iter({ts: LONG});
-    for (let row = 0; it.valid(); it.next(), row++) {
-      data.tsStarts[row] = it.ts;
-    }
-    return data;
-  }
-
-  getHeight() {
-    return MARGIN_TOP + RECT_HEIGHT - 1;
-  }
-
-  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
-    const {visibleTimeScale} = globals.timeline;
-    const data = this.fetcher.data;
-
-    if (data === undefined) return;
-
-    for (let i = 0; i < data.tsStarts.length; i++) {
-      const centerX = Time.fromRaw(data.tsStarts[i]);
-      const selection = getLegacySelection(globals.state);
-      const isHovered = this.hoveredTs === centerX;
-      const isSelected =
-        selection !== null &&
-        selection.kind === 'PERF_SAMPLES' &&
-        selection.leftTs <= centerX &&
-        selection.rightTs >= centerX;
-      const strokeWidth = isSelected ? 3 : 0;
-      this.drawMarker(
-        ctx,
-        visibleTimeScale.timeToPx(centerX),
-        this.centerY,
-        isHovered,
-        strokeWidth,
-      );
-    }
-  }
-
-  drawMarker(
-    ctx: CanvasRenderingContext2D,
-    x: number,
-    y: number,
-    isHovered: boolean,
-    strokeWidth: number,
-  ): void {
-    ctx.beginPath();
-    ctx.moveTo(x, y - this.markerWidth);
-    ctx.lineTo(x - this.markerWidth, y);
-    ctx.lineTo(x, y + this.markerWidth);
-    ctx.lineTo(x + this.markerWidth, y);
-    ctx.lineTo(x, y - this.markerWidth);
-    ctx.closePath();
-    ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
-    ctx.fill();
-    if (strokeWidth > 0) {
-      ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
-      ctx.lineWidth = strokeWidth;
-      ctx.stroke();
-    }
-  }
-
-  onMouseMove({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return;
-    const {visibleTimeScale} = globals.timeline;
-    const time = visibleTimeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-    const index = this.findTimestampIndex(
-      left,
-      visibleTimeScale,
-      data,
-      x,
-      y,
-      right,
-    );
-    this.hoveredTs =
-      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
-  }
-
-  onMouseOut() {
-    this.hoveredTs = undefined;
-  }
-
-  onMouseClick({x, y}: {x: number; y: number}) {
-    const data = this.fetcher.data;
-    if (data === undefined) return false;
-    const {visibleTimeScale} = globals.timeline;
-
-    const time = visibleTimeScale.pxToHpTime(x);
-    const [left, right] = searchSegment(data.tsStarts, time.toTime());
-
-    const index = this.findTimestampIndex(
-      left,
-      visibleTimeScale,
-      data,
-      x,
-      y,
-      right,
-    );
-
-    if (index !== -1) {
-      const ts = Time.fromRaw(data.tsStarts[index]);
-      globals.makeSelection(
-        Actions.selectPerfSamples({
-          id: index,
-          upid: this.upid,
-          leftTs: ts,
-          rightTs: ts,
-          type: ProfileType.PERF_SAMPLE,
-        }),
-      );
-      return true;
-    }
-    return false;
-  }
-
-  // If the markers overlap the rightmost one will be selected.
-  findTimestampIndex(
-    left: number,
-    timeScale: TimeScale,
-    data: Data,
-    x: number,
-    y: number,
-    right: number,
-  ): number {
-    let index = -1;
-    if (left !== -1) {
-      const start = Time.fromRaw(data.tsStarts[left]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = left;
-      }
-    }
-    if (right !== -1) {
-      const start = Time.fromRaw(data.tsStarts[right]);
-      const centerX = timeScale.timeToPx(start);
-      if (this.isInMarker(x, y, centerX)) {
-        index = right;
-      }
-    }
-    return index;
-  }
-
-  isInMarker(x: number, y: number, centerX: number) {
-    return (
-      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
-    );
-  }
-}
-
 class PerfSamplesProfilePlugin implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
     const result = await ctx.engine.query(`
diff --git a/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
new file mode 100644
index 0000000..aca33de
--- /dev/null
+++ b/ui/src/core_plugins/perf_samples_profile/perf_samples_profile_track.ts
@@ -0,0 +1,237 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {searchSegment} from '../../base/binary_search';
+import {duration, Time, time} from '../../base/time';
+import {Actions} from '../../common/actions';
+import {ProfileType, getLegacySelection} from '../../common/state';
+import {TrackData} from '../../common/track_data';
+import {TimelineFetcher} from '../../common/track_helper';
+import {FLAMEGRAPH_HOVERED_COLOR} from '../../frontend/flamegraph';
+import {globals} from '../../frontend/globals';
+import {PanelSize} from '../../frontend/panel';
+import {TimeScale} from '../../frontend/time_scale';
+import {Engine, Track} from '../../public';
+import {LONG} from '../../trace_processor/query_result';
+
+export const PERF_SAMPLES_PROFILE_TRACK_KIND = 'PerfSamplesProfileTrack';
+
+export interface Data extends TrackData {
+  tsStarts: BigInt64Array;
+}
+
+const PERP_SAMPLE_COLOR = 'hsl(224, 45%, 70%)';
+
+// 0.5 Makes the horizontal lines sharp.
+const MARGIN_TOP = 4.5;
+const RECT_HEIGHT = 30.5;
+
+export class PerfSamplesProfileTrack implements Track {
+  private centerY = this.getHeight() / 2;
+  private markerWidth = (this.getHeight() - MARGIN_TOP) / 2;
+  private hoveredTs: time | undefined = undefined;
+  private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
+  private upid: number;
+  private engine: Engine;
+
+  constructor(engine: Engine, upid: number) {
+    this.upid = upid;
+    this.engine = engine;
+  }
+
+  async onUpdate(): Promise<void> {
+    await this.fetcher.requestDataForCurrentTime();
+  }
+
+  async onDestroy(): Promise<void> {
+    this.fetcher.dispose();
+  }
+
+  async onBoundsChange(
+    start: time,
+    end: time,
+    resolution: duration,
+  ): Promise<Data> {
+    if (this.upid === undefined) {
+      return {
+        start,
+        end,
+        resolution,
+        length: 0,
+        tsStarts: new BigInt64Array(),
+      };
+    }
+    const queryRes = await this.engine.query(`
+      select ts, upid from perf_sample
+      join thread using (utid)
+      where upid = ${this.upid}
+      and callsite_id is not null
+      order by ts`);
+    const numRows = queryRes.numRows();
+    const data: Data = {
+      start,
+      end,
+      resolution,
+      length: numRows,
+      tsStarts: new BigInt64Array(numRows),
+    };
+
+    const it = queryRes.iter({ts: LONG});
+    for (let row = 0; it.valid(); it.next(), row++) {
+      data.tsStarts[row] = it.ts;
+    }
+    return data;
+  }
+
+  getHeight() {
+    return MARGIN_TOP + RECT_HEIGHT - 1;
+  }
+
+  render(ctx: CanvasRenderingContext2D, _size: PanelSize): void {
+    const {visibleTimeScale} = globals.timeline;
+    const data = this.fetcher.data;
+
+    if (data === undefined) return;
+
+    for (let i = 0; i < data.tsStarts.length; i++) {
+      const centerX = Time.fromRaw(data.tsStarts[i]);
+      const selection = getLegacySelection(globals.state);
+      const isHovered = this.hoveredTs === centerX;
+      const isSelected =
+        selection !== null &&
+        selection.kind === 'PERF_SAMPLES' &&
+        selection.leftTs <= centerX &&
+        selection.rightTs >= centerX;
+      const strokeWidth = isSelected ? 3 : 0;
+      this.drawMarker(
+        ctx,
+        visibleTimeScale.timeToPx(centerX),
+        this.centerY,
+        isHovered,
+        strokeWidth,
+      );
+    }
+  }
+
+  drawMarker(
+    ctx: CanvasRenderingContext2D,
+    x: number,
+    y: number,
+    isHovered: boolean,
+    strokeWidth: number,
+  ): void {
+    ctx.beginPath();
+    ctx.moveTo(x, y - this.markerWidth);
+    ctx.lineTo(x - this.markerWidth, y);
+    ctx.lineTo(x, y + this.markerWidth);
+    ctx.lineTo(x + this.markerWidth, y);
+    ctx.lineTo(x, y - this.markerWidth);
+    ctx.closePath();
+    ctx.fillStyle = isHovered ? FLAMEGRAPH_HOVERED_COLOR : PERP_SAMPLE_COLOR;
+    ctx.fill();
+    if (strokeWidth > 0) {
+      ctx.strokeStyle = FLAMEGRAPH_HOVERED_COLOR;
+      ctx.lineWidth = strokeWidth;
+      ctx.stroke();
+    }
+  }
+
+  onMouseMove({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return;
+    const {visibleTimeScale} = globals.timeline;
+    const time = visibleTimeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+    const index = this.findTimestampIndex(
+      left,
+      visibleTimeScale,
+      data,
+      x,
+      y,
+      right,
+    );
+    this.hoveredTs =
+      index === -1 ? undefined : Time.fromRaw(data.tsStarts[index]);
+  }
+
+  onMouseOut() {
+    this.hoveredTs = undefined;
+  }
+
+  onMouseClick({x, y}: {x: number; y: number}) {
+    const data = this.fetcher.data;
+    if (data === undefined) return false;
+    const {visibleTimeScale} = globals.timeline;
+
+    const time = visibleTimeScale.pxToHpTime(x);
+    const [left, right] = searchSegment(data.tsStarts, time.toTime());
+
+    const index = this.findTimestampIndex(
+      left,
+      visibleTimeScale,
+      data,
+      x,
+      y,
+      right,
+    );
+
+    if (index !== -1) {
+      const ts = Time.fromRaw(data.tsStarts[index]);
+      globals.makeSelection(
+        Actions.selectPerfSamples({
+          id: index,
+          upid: this.upid,
+          leftTs: ts,
+          rightTs: ts,
+          type: ProfileType.PERF_SAMPLE,
+        }),
+      );
+      return true;
+    }
+    return false;
+  }
+
+  // If the markers overlap the rightmost one will be selected.
+  findTimestampIndex(
+    left: number,
+    timeScale: TimeScale,
+    data: Data,
+    x: number,
+    y: number,
+    right: number,
+  ): number {
+    let index = -1;
+    if (left !== -1) {
+      const start = Time.fromRaw(data.tsStarts[left]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = left;
+      }
+    }
+    if (right !== -1) {
+      const start = Time.fromRaw(data.tsStarts[right]);
+      const centerX = timeScale.timeToPx(start);
+      if (this.isInMarker(x, y, centerX)) {
+        index = right;
+      }
+    }
+    return index;
+  }
+
+  isInMarker(x: number, y: number, centerX: number) {
+    return (
+      Math.abs(x - centerX) + Math.abs(y - this.centerY) <= this.markerWidth
+    );
+  }
+}
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
index f2a7475..da51bb0 100644
--- a/ui/src/core_plugins/process_summary/index.ts
+++ b/ui/src/core_plugins/process_summary/index.ts
@@ -34,6 +34,8 @@
   }
 
   private async addProcessTrackGroups(ctx: PluginContextTrace): Promise<void> {
+    const cpuCount = Math.max(...ctx.trace.cpus, -1) + 1;
+
     const result = await ctx.engine.query(`
       INCLUDE PERFETTO MODULE android.process_metadata;
 
@@ -106,7 +108,7 @@
             isDebuggable,
           },
           trackFactory: () => {
-            return new ProcessSchedulingTrack(ctx.engine, config);
+            return new ProcessSchedulingTrack(ctx.engine, config, cpuCount);
           },
         });
       } else {
diff --git a/ui/src/core_plugins/process_summary/process_scheduling_track.ts b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
index 28b536f..4b26432 100644
--- a/ui/src/core_plugins/process_summary/process_scheduling_track.ts
+++ b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
@@ -56,23 +56,18 @@
   private mousePos?: {x: number; y: number};
   private utidHoveredInThisTrack = -1;
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private maxCpu = 0;
+  private cpuCount: number;
   private engine: Engine;
   private trackUuid = uuidv4Sql();
   private config: Config;
 
-  constructor(engine: Engine, config: Config) {
+  constructor(engine: Engine, config: Config, cpuCount: number) {
     this.engine = engine;
     this.config = config;
+    this.cpuCount = cpuCount;
   }
 
   async onCreate(): Promise<void> {
-    const cpus = await this.engine.getCpus();
-
-    // A process scheduling track should only exist in a trace that has cpus.
-    assertTrue(cpus.length > 0);
-    this.maxCpu = Math.max(...cpus) + 1;
-
     if (this.config.upid !== null) {
       await this.engine.query(`
         create virtual table process_scheduling_${this.trackUuid}
@@ -119,11 +114,9 @@
 
   async onDestroy(): Promise<void> {
     this.fetcher.dispose();
-    if (this.engine.isAlive) {
-      await this.engine.query(`
-        drop table process_scheduling_${this.trackUuid}
-      `);
-    }
+    await this.engine.tryQuery(`
+      drop table process_scheduling_${this.trackUuid}
+    `);
   }
 
   async onBoundsChange(
@@ -142,7 +135,7 @@
       end,
       resolution,
       length: numRows,
-      maxCpu: this.maxCpu,
+      maxCpu: this.cpuCount,
       starts: new BigInt64Array(numRows),
       ends: new BigInt64Array(numRows),
       cpus: new Uint32Array(numRows),
diff --git a/ui/src/core_plugins/process_summary/process_summary_track.ts b/ui/src/core_plugins/process_summary/process_summary_track.ts
index 5fa31de..57abd75 100644
--- a/ui/src/core_plugins/process_summary/process_summary_track.ts
+++ b/ui/src/core_plugins/process_summary/process_summary_track.ts
@@ -171,13 +171,11 @@
   }
 
   async onDestroy(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(
-        `drop table if exists ${this.tableName(
-          'window',
-        )}; drop table if exists ${this.tableName('span')}`,
-      );
-    }
+    await this.engine.tryQuery(
+      `drop table if exists ${this.tableName(
+        'window',
+      )}; drop table if exists ${this.tableName('span')}`,
+    );
     this.fetcher.dispose();
   }
 
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index 029f818..f6eaebb 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -15,7 +15,6 @@
 import {uuidv4} from '../../base/uuid';
 import {AddTrackArgs} from '../../common/actions';
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
-import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {
   BottomTabToSCSAdapter,
   NUM,
@@ -25,34 +24,9 @@
   PrimaryTrackSortKey,
 } from '../../public';
 import {Engine} from '../../trace_processor/engine';
-import {
-  CustomSqlDetailsPanelConfig,
-  CustomSqlTableDefConfig,
-  CustomSqlTableSliceTrack,
-} from '../custom_sql_table_slices';
 
 import {ScreenshotTab} from './screenshot_panel';
-
-class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
-  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
-
-  getSqlDataSource(): CustomSqlTableDefConfig {
-    return {
-      sqlTableName: 'android_screenshots',
-      columns: ['*'],
-    };
-  }
-
-  getDetailsPanel(): CustomSqlDetailsPanelConfig {
-    return {
-      kind: ScreenshotTab.kind,
-      config: {
-        sqlTableName: this.tableName,
-        title: 'Screenshots',
-      },
-    };
-  }
-}
+import {ScreenshotsTrack} from './screenshots_track';
 
 export type DecideTracksResult = {
   tracksToAdd: AddTrackArgs[];
diff --git a/ui/src/core_plugins/screenshots/screenshots_track.ts b/ui/src/core_plugins/screenshots/screenshots_track.ts
new file mode 100644
index 0000000..a880b33
--- /dev/null
+++ b/ui/src/core_plugins/screenshots/screenshots_track.ts
@@ -0,0 +1,42 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {
+  CustomSqlDetailsPanelConfig,
+  CustomSqlTableDefConfig,
+  CustomSqlTableSliceTrack,
+} from '../../frontend/tracks/custom_sql_table_slice_track';
+import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
+import {ScreenshotTab} from './screenshot_panel';
+
+export class ScreenshotsTrack extends CustomSqlTableSliceTrack<NamedSliceTrackTypes> {
+  static readonly kind = 'dev.perfetto.ScreenshotsTrack';
+
+  getSqlDataSource(): CustomSqlTableDefConfig {
+    return {
+      sqlTableName: 'android_screenshots',
+      columns: ['*'],
+    };
+  }
+
+  getDetailsPanel(): CustomSqlDetailsPanelConfig {
+    return {
+      kind: ScreenshotTab.kind,
+      config: {
+        sqlTableName: this.tableName,
+        title: 'Screenshots',
+      },
+    };
+  }
+}
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index 73e2faf..ab0bcc5 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -24,7 +24,7 @@
 import {getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
 
-import {ThreadStateTrack as ThreadStateTrackV2} from './thread_state_v2';
+import {ThreadStateTrack} from './thread_state_track';
 
 export const THREAD_STATE_TRACK_KIND = 'ThreadStateTrack';
 
@@ -64,7 +64,7 @@
         kind: THREAD_STATE_TRACK_KIND,
         utid,
         trackFactory: ({trackKey}) => {
-          return new ThreadStateTrackV2(
+          return new ThreadStateTrack(
             {
               engine: ctx.engine,
               trackKey,
diff --git a/ui/src/core_plugins/thread_state/thread_state_v2.ts b/ui/src/core_plugins/thread_state/thread_state_track.ts
similarity index 100%
rename from ui/src/core_plugins/thread_state/thread_state_v2.ts
rename to ui/src/core_plugins/thread_state/thread_state_track.ts
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
new file mode 100644
index 0000000..937ba70
--- /dev/null
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -0,0 +1,100 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {Actions} from '../../common/actions';
+import {
+  getTimeSpanOfSelectionOrVisibleWindow,
+  globals,
+} from '../../frontend/globals';
+import {OmniboxMode} from '../../frontend/omnibox_manager';
+import {verticalScrollToTrack} from '../../frontend/scroll_helper';
+import {
+  Plugin,
+  PluginContextTrace,
+  PluginDescriptor,
+  PromptOption,
+} from '../../public';
+
+class TrackUtilsPlugin implements Plugin {
+  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+    ctx.registerCommand({
+      id: 'perfetto.RunQueryInSelectedTimeWindow',
+      name: `Run query in selected time window`,
+      callback: () => {
+        const window = getTimeSpanOfSelectionOrVisibleWindow();
+        globals.omnibox.setMode(OmniboxMode.Query);
+        globals.omnibox.setText(
+          `select  where ts >= ${window.start} and ts < ${window.end}`,
+        );
+        globals.omnibox.focusOmnibox(7);
+      },
+    });
+
+    ctx.registerCommand({
+      // Selects & reveals the first track on the timeline with a given URI.
+      id: 'perfetto.FindTrack',
+      name: 'Find track by URI',
+      callback: async () => {
+        const tracks = globals.trackManager.getAllTracks();
+        const options = tracks.map(({uri}): PromptOption => {
+          return {key: uri, displayName: uri};
+        });
+
+        // Sort tracks in a natural sort order
+        const collator = new Intl.Collator('en', {
+          numeric: true,
+          sensitivity: 'base',
+        });
+        const sortedOptions = options.sort((a, b) => {
+          return collator.compare(a.displayName, b.displayName);
+        });
+
+        try {
+          const selectedUri = await ctx.prompt(
+            'Choose a track...',
+            sortedOptions,
+          );
+
+          // Find the first track with this URI
+          const firstTrack = Object.values(globals.state.tracks).find(
+            ({uri}) => uri === selectedUri,
+          );
+          if (firstTrack) {
+            console.log(firstTrack);
+            verticalScrollToTrack(firstTrack.key, true);
+            const traceTime = globals.stateTraceTimeTP();
+            globals.makeSelection(
+              Actions.selectArea({
+                area: {
+                  start: traceTime.start,
+                  end: traceTime.end,
+                  tracks: [firstTrack.key],
+                },
+              }),
+            );
+          } else {
+            alert(`No tracks with uri ${selectedUri} on the timeline`);
+          }
+        } catch {
+          // Prompt was probably cancelled - do nothing.
+        }
+      },
+    });
+  }
+}
+
+export const plugin: PluginDescriptor = {
+  pluginId: 'perfetto.TrackUtils',
+  plugin: TrackUtilsPlugin,
+};
diff --git a/ui/src/core_plugins/visualised_args/visualized_args_track.ts b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
index 8de85d3..55cf49d 100644
--- a/ui/src/core_plugins/visualised_args/visualized_args_track.ts
+++ b/ui/src/core_plugins/visualised_args/visualized_args_track.ts
@@ -68,9 +68,7 @@
     `);
 
     return new DisposableCallback(() => {
-      if (this.engine.isAlive) {
-        this.engine.query(`drop view ${this.viewName}`);
-      }
+      this.engine.tryQuery(`drop view ${this.viewName}`);
     });
   }
 
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 09b1036..6c90216 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -18,12 +18,10 @@
 import {Trash} from '../base/disposable';
 import {findRef} from '../base/dom_utils';
 import {FuzzyFinder} from '../base/fuzzy';
-import {assertExists} from '../base/logging';
+import {assertExists, assertUnreachable} from '../base/logging';
 import {undoCommonChatAppReplacements} from '../base/string_utils';
-import {duration, Span, time, TimeSpan} from '../base/time';
 import {Actions} from '../common/actions';
 import {getLegacySelection} from '../common/state';
-import {runQuery} from '../common/queries';
 import {
   DurationPrecision,
   setDurationPrecision,
@@ -31,28 +29,22 @@
   TimestampFormat,
 } from '../core/timestamp_format';
 import {raf} from '../core/raf_scheduler';
-import {Command} from '../public';
-import {Engine} from '../trace_processor/engine';
-import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
+import {Command, Engine, addDebugSliceTrack} from '../public';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
 import {maybeRenderFullscreenModalDialog} from '../widgets/modal';
 
 import {onClickCopy} from './clipboard';
 import {CookieConsent} from './cookie_consent';
-import {globals} from './globals';
+import {getTimeSpanOfSelectionOrVisibleWindow, globals} from './globals';
 import {toggleHelp} from './help_modal';
 import {Notes} from './notes';
 import {Omnibox, OmniboxOption} from './omnibox';
 import {addQueryResultsTab} from './query_result_tab';
-import {verticalScrollToTrack} from './scroll_helper';
 import {executeSearch} from './search_handler';
 import {Sidebar} from './sidebar';
-import {Utid} from './sql_types';
-import {getThreadInfo} from './thread_and_process_info';
 import {Topbar} from './topbar';
 import {shareTrace} from './trace_attrs';
-import {addDebugSliceTrack} from './debug_tracks';
 import {AggregationsTabs} from './aggregation_tab';
 import {addSqlTableTab} from './sql_table/tab';
 import {SqlTables} from './sql_table/well_known_tables';
@@ -62,12 +54,16 @@
   lockSliceSpan,
   moveByFocusedFlow,
 } from './keyboard_event_handler';
-import {exists} from '../base/utils';
+import {publishPermalinkHash} from './publish';
+import {OmniboxMode, PromptOption} from './omnibox_manager';
+import {Utid} from './sql_types';
+import {getThreadInfo} from './thread_and_process_info';
+import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
 
 function renderPermalink(): m.Children {
-  const permalink = globals.state.permalink;
-  if (!permalink.requestId || !permalink.hash) return null;
-  const url = `${self.location.origin}/#!/?s=${permalink.hash}`;
+  const hash = globals.permalinkHash;
+  if (!hash) return null;
+  const url = `${self.location.origin}/#!/?s=${hash}`;
   const linkProps = {title: 'Click to copy the URL', onclick: onClickCopy(url)};
 
   return m('.alert-permalink', [
@@ -75,7 +71,7 @@
     m(
       'button',
       {
-        onclick: () => globals.dispatch(Actions.clearPermalink({})),
+        onclick: () => publishPermalinkHash(undefined),
       },
       m('i.material-icons.disallow-selection', 'close'),
     ),
@@ -88,25 +84,6 @@
   }
 }
 
-interface PromptOption {
-  key: string;
-  displayName: string;
-}
-
-interface Prompt {
-  text: string;
-  options?: PromptOption[];
-  resolve(result: string): void;
-  reject(): void;
-}
-
-enum OmniboxMode {
-  Search,
-  Query,
-  Command,
-  Prompt,
-}
-
 const criticalPathSliceColumns = {
   ts: 'ts',
   dur: 'dur',
@@ -138,14 +115,6 @@
 
 export class App implements m.ClassComponent {
   private trash = new Trash();
-
-  private omniboxMode: OmniboxMode = OmniboxMode.Search;
-  private omniboxText = '';
-  private queryText = '';
-  private omniboxSelectionIndex = 0;
-  private focusOmniboxNextRender = false;
-  private pendingCursorPlacement = -1;
-  private pendingPrompt?: Prompt;
   static readonly OMNIBOX_INPUT_REF = 'omnibox';
   private omniboxInputEl?: HTMLInputElement;
   private recentCommands: string[] = [];
@@ -164,80 +133,6 @@
     return engine;
   }
 
-  private enterCommandMode(): void {
-    this.omniboxMode = OmniboxMode.Command;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-    this.focusOmniboxNextRender = true;
-
-    raf.scheduleFullRedraw();
-  }
-
-  private enterQueryMode(): void {
-    this.omniboxMode = OmniboxMode.Query;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-    this.focusOmniboxNextRender = true;
-
-    raf.scheduleFullRedraw();
-  }
-
-  private enterSearchMode(focusOmnibox: boolean): void {
-    this.omniboxMode = OmniboxMode.Search;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-
-    if (focusOmnibox) {
-      this.focusOmniboxNextRender = true;
-    }
-
-    globals.dispatch(Actions.setOmniboxMode({mode: 'SEARCH'}));
-
-    raf.scheduleFullRedraw();
-  }
-
-  // Start a prompt. If options are supplied, the user must pick one from the
-  // list, otherwise the input is free-form text.
-  private prompt(text: string, options?: PromptOption[]): Promise<string> {
-    this.omniboxMode = OmniboxMode.Prompt;
-    this.resetOmnibox();
-    this.rejectPendingPrompt();
-
-    const promise = new Promise<string>((resolve, reject) => {
-      this.pendingPrompt = {
-        text,
-        options,
-        resolve,
-        reject,
-      };
-    });
-
-    this.focusOmniboxNextRender = true;
-    raf.scheduleFullRedraw();
-
-    return promise;
-  }
-
-  // Resolve the pending prompt with a value to return to the prompter.
-  private resolvePrompt(value: string): void {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.resolve(value);
-      this.pendingPrompt = undefined;
-    }
-    this.enterSearchMode(false);
-  }
-
-  // Reject the prompt outright. Doing this will force the owner of the prompt
-  // promise to catch, so only do this when things go seriously wrong.
-  // Use |resolvePrompt(null)| to indicate cancellation.
-  private rejectPrompt(): void {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.reject();
-      this.pendingPrompt = undefined;
-    }
-    this.enterSearchMode(false);
-  }
-
   private getFirstUtidOfSelectionOrVisibleWindow(): number {
     const selection = getLegacySelection(globals.state);
     if (selection && selection.kind === 'AREA') {
@@ -283,7 +178,7 @@
         const promptText = 'Select format...';
 
         try {
-          const result = await this.prompt(promptText, options);
+          const result = await globals.omnibox.prompt(promptText, options);
           setTimestampFormat(result as TimestampFormat);
           raf.scheduleFullRedraw();
         } catch {
@@ -305,7 +200,7 @@
         const promptText = 'Select duration precision mode...';
 
         try {
-          const result = await this.prompt(promptText, options);
+          const result = await globals.omnibox.prompt(promptText, options);
           setDurationPrecision(result as DurationPrecision);
           raf.scheduleFullRedraw();
         } catch {
@@ -322,9 +217,8 @@
         const engine = this.getEngine();
 
         if (engine !== undefined && trackUtid != 0) {
-          await runQuery(
+          await engine.query(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -365,9 +259,8 @@
         const engine = this.getEngine();
 
         if (engine !== undefined && trackUtid != 0) {
-          await runQuery(
+          await engine.query(
             `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            engine,
           );
           await addDebugSliceTrack(
             engine,
@@ -454,19 +347,19 @@
     {
       id: 'perfetto.OpenCommandPalette',
       name: 'Open command palette',
-      callback: () => this.enterCommandMode(),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Command),
       defaultHotkey: '!Mod+Shift+P',
     },
     {
       id: 'perfetto.RunQuery',
       name: 'Run query',
-      callback: () => this.enterQueryMode(),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Query),
       defaultHotkey: '!Mod+O',
     },
     {
       id: 'perfetto.Search',
       name: 'Search',
-      callback: () => this.enterSearchMode(true),
+      callback: () => globals.omnibox.setMode(OmniboxMode.Search),
       defaultHotkey: '/',
     },
     {
@@ -476,16 +369,6 @@
       defaultHotkey: '?',
     },
     {
-      id: 'perfetto.RunQueryInSelectedTimeWindow',
-      name: `Run query in selected time window`,
-      callback: () => {
-        const window = getTimeSpanOfSelectionOrVisibleWindow();
-        this.enterQueryMode();
-        this.queryText = `select  where ts >= ${window.start} and ts < ${window.end}`;
-        this.pendingCursorPlacement = 7;
-      },
-    },
-    {
       id: 'perfetto.CopyTimeWindow',
       name: `Copy selected time window to clipboard`,
       callback: () => {
@@ -495,56 +378,6 @@
       },
     },
     {
-      // Selects & reveals the first track on the timeline with a given URI.
-      id: 'perfetto.FindTrack',
-      name: 'Find track by URI',
-      callback: async () => {
-        const tracks = globals.trackManager.getAllTracks();
-        const options = tracks.map(({uri}): PromptOption => {
-          return {key: uri, displayName: uri};
-        });
-
-        // Sort tracks in a natural sort order
-        const collator = new Intl.Collator('en', {
-          numeric: true,
-          sensitivity: 'base',
-        });
-        const sortedOptions = options.sort((a, b) => {
-          return collator.compare(a.displayName, b.displayName);
-        });
-
-        try {
-          const selectedUri = await this.prompt(
-            'Choose a track...',
-            sortedOptions,
-          );
-
-          // Find the first track with this URI
-          const firstTrack = Object.values(globals.state.tracks).find(
-            ({uri}) => uri === selectedUri,
-          );
-          if (firstTrack) {
-            console.log(firstTrack);
-            verticalScrollToTrack(firstTrack.key, true);
-            const traceTime = globals.stateTraceTimeTP();
-            globals.makeSelection(
-              Actions.selectArea({
-                area: {
-                  start: traceTime.start,
-                  end: traceTime.end,
-                  tracks: [firstTrack.key],
-                },
-              }),
-            );
-          } else {
-            alert(`No tracks with uri ${selectedUri} on the timeline`);
-          }
-        } catch {
-          // Prompt was probably cancelled - do nothing.
-        }
-      },
-    },
-    {
       id: 'perfetto.FocusSelection',
       name: 'Focus current selection',
       callback: () => findCurrentSelection(),
@@ -620,8 +453,8 @@
         if (selection !== null && selection.kind === 'AREA') {
           const area = globals.state.areas[selection.areaId];
           const coversEntireTimeRange =
-            globals.traceTime.start === area.start &&
-            globals.traceTime.end === area.end;
+            globals.traceContext.start === area.start &&
+            globals.traceContext.end === area.end;
           if (!coversEntireTimeRange) {
             // If the current selection is an area which does not cover the
             // entire time range, preserve the list of selected tracks and
@@ -636,7 +469,7 @@
           // If the current selection is not an area, select all.
           tracksToSelect = Object.keys(globals.state.tracks);
         }
-        const {start, end} = globals.traceTime;
+        const {start, end} = globals.traceContext;
         globals.dispatch(
           Actions.selectArea({
             area: {
@@ -655,18 +488,6 @@
     return this.cmds;
   }
 
-  private rejectPendingPrompt() {
-    if (this.pendingPrompt) {
-      this.pendingPrompt.reject();
-      this.pendingPrompt = undefined;
-    }
-  }
-
-  private resetOmnibox() {
-    this.omniboxText = '';
-    this.omniboxSelectionIndex = 0;
-  }
-
   private renderOmnibox(): m.Children {
     const msgTTL = globals.state.status.timestamp + 1 - Date.now() / 1e3;
     const engineIsBusy =
@@ -683,22 +504,23 @@
       );
     }
 
-    if (this.omniboxMode === OmniboxMode.Command) {
+    const omniboxMode = globals.omnibox.omniboxMode;
+
+    if (omniboxMode === OmniboxMode.Command) {
       return this.renderCommandOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Prompt) {
+    } else if (omniboxMode === OmniboxMode.Prompt) {
       return this.renderPromptOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Query) {
+    } else if (omniboxMode === OmniboxMode.Query) {
       return this.renderQueryOmnibox();
-    } else if (this.omniboxMode === OmniboxMode.Search) {
+    } else if (omniboxMode === OmniboxMode.Search) {
       return this.renderSearchOmnibox();
     } else {
-      const x: never = this.omniboxMode;
-      throw new Error(`Unhandled omnibox mode ${x}`);
+      assertUnreachable(omniboxMode);
     }
   }
 
   renderPromptOmnibox(): m.Children {
-    const prompt = assertExists(this.pendingPrompt);
+    const prompt = assertExists(globals.omnibox.pendingPrompt);
 
     let options: OmniboxOption[] | undefined = undefined;
 
@@ -707,7 +529,7 @@
         prompt.options,
         ({displayName}) => displayName,
       );
-      const result = fuzzy.find(this.omniboxText);
+      const result = fuzzy.find(globals.omnibox.text);
       options = result.map((result) => {
         return {
           key: result.item.key,
@@ -717,27 +539,27 @@
     }
 
     return m(Omnibox, {
-      value: this.omniboxText,
+      value: globals.omnibox.text,
       placeholder: prompt.text,
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'prompt-mode',
       closeOnOutsideClick: true,
       options,
-      selectedOptionIndex: this.omniboxSelectionIndex,
+      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
       onSelectedOptionChanged: (index) => {
-        this.omniboxSelectionIndex = index;
+        globals.omnibox.setOmniboxSelectionIndex(index);
         raf.scheduleFullRedraw();
       },
       onInput: (value) => {
-        this.omniboxText = value;
-        this.omniboxSelectionIndex = 0;
+        globals.omnibox.setText(value);
+        globals.omnibox.setOmniboxSelectionIndex(0);
         raf.scheduleFullRedraw();
       },
       onSubmit: (value, _alt) => {
-        this.resolvePrompt(value);
+        globals.omnibox.resolvePrompt(value);
       },
       onClose: () => {
-        this.rejectPrompt();
+        globals.omnibox.rejectPrompt();
       },
     });
   }
@@ -746,7 +568,7 @@
     const cmdMgr = globals.commandManager;
 
     // Fuzzy-filter commands by the filter string.
-    const filteredCmds = cmdMgr.fuzzyFilterCommands(this.omniboxText);
+    const filteredCmds = cmdMgr.fuzzyFilterCommands(globals.omnibox.text);
 
     // Create an array of commands with attached heuristics from the recent
     // command register.
@@ -777,36 +599,35 @@
     });
 
     return m(Omnibox, {
-      value: this.omniboxText,
+      value: globals.omnibox.text,
       placeholder: 'Filter commands...',
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'command-mode',
       options,
       closeOnSubmit: true,
       closeOnOutsideClick: true,
-      selectedOptionIndex: this.omniboxSelectionIndex,
+      selectedOptionIndex: globals.omnibox.omniboxSelectionIndex,
       onSelectedOptionChanged: (index) => {
-        this.omniboxSelectionIndex = index;
+        globals.omnibox.setOmniboxSelectionIndex(index);
         raf.scheduleFullRedraw();
       },
       onInput: (value) => {
-        this.omniboxText = value;
-        this.omniboxSelectionIndex = 0;
+        globals.omnibox.setText(value);
+        globals.omnibox.setOmniboxSelectionIndex(0);
         raf.scheduleFullRedraw();
       },
       onClose: () => {
         if (this.omniboxInputEl) {
           this.omniboxInputEl.blur();
         }
-        this.enterSearchMode(false);
-        raf.scheduleFullRedraw();
+        globals.omnibox.reset();
       },
       onSubmit: (key: string) => {
         this.addRecentCommand(key);
         cmdMgr.runCommand(key);
       },
       onGoBack: () => {
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
       },
     });
   }
@@ -822,13 +643,13 @@
   renderQueryOmnibox(): m.Children {
     const ph = 'e.g. select * from sched left join thread using(utid) limit 10';
     return m(Omnibox, {
-      value: this.queryText,
+      value: globals.omnibox.text,
       placeholder: ph,
       inputRef: App.OMNIBOX_INPUT_REF,
       extraClasses: 'query-mode',
 
       onInput: (value) => {
-        this.queryText = value;
+        globals.omnibox.setText(value);
         raf.scheduleFullRedraw();
       },
       onSubmit: (query, alt) => {
@@ -840,15 +661,15 @@
         addQueryResultsTab(config, tag);
       },
       onClose: () => {
-        this.queryText = '';
+        globals.omnibox.setText('');
         if (this.omniboxInputEl) {
           this.omniboxInputEl.blur();
         }
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
         raf.scheduleFullRedraw();
       },
       onGoBack: () => {
-        this.enterSearchMode(false);
+        globals.omnibox.reset();
       },
     });
   }
@@ -865,10 +686,10 @@
       onInput: (value, prev) => {
         if (prev === '') {
           if (value === '>') {
-            this.enterCommandMode();
+            globals.omnibox.setMode(OmniboxMode.Command);
             return;
           } else if (value === ':') {
-            this.enterQueryMode();
+            globals.omnibox.setMode(OmniboxMode.Query);
             return;
           }
         }
@@ -987,32 +808,20 @@
   }
 
   private maybeFocusOmnibar() {
-    if (this.focusOmniboxNextRender) {
+    if (globals.omnibox.focusOmniboxNextRender) {
       const omniboxEl = this.omniboxInputEl;
       if (omniboxEl) {
         omniboxEl.focus();
-        if (this.pendingCursorPlacement === -1) {
+        if (globals.omnibox.pendingCursorPlacement === undefined) {
           omniboxEl.select();
         } else {
           omniboxEl.setSelectionRange(
-            this.pendingCursorPlacement,
-            this.pendingCursorPlacement,
+            globals.omnibox.pendingCursorPlacement,
+            globals.omnibox.pendingCursorPlacement,
           );
-          this.pendingCursorPlacement = -1;
         }
       }
-      this.focusOmniboxNextRender = false;
+      globals.omnibox.clearOmniboxFocusFlag();
     }
   }
 }
-
-// Returns the time span of the current selection, or the visible window if
-// there is no current selection.
-function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
-  const range = globals.findTimeRangeOfSelection();
-  if (exists(range)) {
-    return new TimeSpan(range.start, range.end);
-  } else {
-    return globals.stateVisibleTime();
-  }
-}
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 29a5966..d010f95 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -688,9 +688,7 @@
       this.initState.dispose();
       this.initState = undefined;
     }
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table if exists ${this.getTableName()}`);
-    }
+    await this.engine.tryQuery(`drop table if exists ${this.getTableName()}`);
   }
 
   // Compute the range of values to display and range label.
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 720ace2..253ed41 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -666,9 +666,7 @@
       this.initState.dispose();
       this.initState = undefined;
     }
-    if (this.engine.isAlive) {
-      await this.engine.execute(`drop table ${this.getTableName()}`);
-    }
+    await this.engine.tryQuery(`drop table ${this.getTableName()}`);
   }
 
   // This method figures out if the visible window is outside the bounds of
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 91cf54f..dd13a8d 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -17,7 +17,6 @@
 import {Icons} from '../base/semantic_icons';
 import {duration, Time, TimeSpan} from '../base/time';
 import {exists} from '../base/utils';
-import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
@@ -104,17 +103,18 @@
     run: (slice: SliceDetails) => {
       const engine = getEngine();
       if (engine === undefined) return;
-      runQuery(
-        `
+      engine
+        .query(
+          `
         INCLUDE PERFETTO MODULE android.binder;
         INCLUDE PERFETTO MODULE android.monitor_contention;
       `,
-        engine,
-      ).then(() =>
-        addDebugSliceTrack(
-          engine,
-          {
-            sqlSource: `
+        )
+        .then(() =>
+          addDebugSliceTrack(
+            engine,
+            {
+              sqlSource: `
                                 WITH merged AS (
                                   SELECT s.ts, s.dur, tx.aidl_name AS name, 0 AS depth
                                   FROM android_binder_txns tx
@@ -151,14 +151,14 @@
                                         AND short_blocked_method IS NOT NULL
                                   ORDER BY depth
                                 ) SELECT ts, dur, name FROM merged`,
-          },
-          `Binder names (${getProcessNameFromSlice(
-            slice,
-          )}:${getThreadNameFromSlice(slice)})`,
-          {ts: 'ts', dur: 'dur', name: 'name'},
-          [],
-        ),
-      );
+            },
+            `Binder names (${getProcessNameFromSlice(
+              slice,
+            )}:${getThreadNameFromSlice(slice)})`,
+            {ts: 'ts', dur: 'dur', name: 'name'},
+            [],
+          ),
+        );
     },
   },
 ];
diff --git a/ui/src/frontend/flow_events_renderer.ts b/ui/src/frontend/flow_events_renderer.ts
index 4ffa6cf..69097ec 100644
--- a/ui/src/frontend/flow_events_renderer.ts
+++ b/ui/src/frontend/flow_events_renderer.ts
@@ -71,8 +71,8 @@
       for (const trackId of getTrackIds(track)) {
         this.trackIdToTrackPanel.set(trackId, {panel, yStart});
       }
-    } else if (exists(panel.trackGroupId)) {
-      this.groupIdToTrackGroupPanel.set(panel.trackGroupId, {
+    } else if (exists(panel.groupKey)) {
+      this.groupIdToTrackGroupPanel.set(panel.groupKey, {
         panel,
         yStart,
         height,
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 04cdc17..46d6cbe 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -56,6 +56,7 @@
 import {PxSpan, TimeScale} from './time_scale';
 import {SelectionManager, LegacySelection} from '../core/selection_manager';
 import {exists} from '../base/utils';
+import {OmniboxManager} from './omnibox_manager';
 
 const INSTANT_FOCUS_DURATION = 1n;
 const INCOMPLETE_SLICE_DURATION = 30_000n;
@@ -221,7 +222,7 @@
   pendingScrollId: number | undefined;
 }
 
-export interface TraceTime {
+export interface TraceContext {
   readonly start: time;
   readonly end: time;
 
@@ -237,14 +238,22 @@
   // Trace TZ is like UTC but keeps into account also the timezone_off_mins
   // recorded into the trace, to show timestamps in the device local time.
   readonly traceTzOffset: time;
+
+  // The list of CPUs in the trace
+  readonly cpus: number[];
+
+  // The number of gpus in the trace
+  readonly gpuCount: number;
 }
 
-export const defaultTraceTime: TraceTime = {
+export const defaultTraceContext: TraceContext = {
   start: Time.ZERO,
   end: Time.fromSeconds(10),
   realtimeOffset: Time.ZERO,
   utcOffset: Time.ZERO,
   traceTzOffset: Time.ZERO,
+  cpus: [],
+  gpuCount: 0,
 };
 
 /**
@@ -291,12 +300,15 @@
   private _selectionManager = new SelectionManager(this._store);
   private _hasFtrace: boolean = false;
 
+  omnibox = new OmniboxManager();
+
   scrollToTrackKey?: string | number;
   httpRpcState: HttpRpcState = {connected: false};
   newVersionAvailable = false;
   showPanningHint = false;
+  permalinkHash?: string;
 
-  traceTime = defaultTraceTime;
+  traceContext = defaultTraceContext;
 
   // TODO(hjd): Remove once we no longer need to update UUID on redraw.
   private _publishRedraw?: () => void = undefined;
@@ -716,19 +728,19 @@
 
   // Get a timescale that covers the entire trace
   getTraceTimeScale(pxSpan: PxSpan): TimeScale {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     const traceTime = HighPrecisionTimeSpan.fromTime(start, end);
     return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
   }
 
   // Get the trace time bounds
   stateTraceTime(): Span<HighPrecisionTime> {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     return HighPrecisionTimeSpan.fromTime(start, end);
   }
 
   stateTraceTimeTP(): Span<time, duration> {
-    const {start, end} = this.traceTime;
+    const {start, end} = this.traceContext;
     return new TimeSpan(start, end);
   }
 
@@ -762,14 +774,14 @@
     switch (fmt) {
       case TimestampFormat.Timecode:
       case TimestampFormat.Seconds:
-        return this.traceTime.start;
+        return this.traceContext.start;
       case TimestampFormat.Raw:
       case TimestampFormat.RawLocale:
         return Time.ZERO;
       case TimestampFormat.UTC:
-        return this.traceTime.utcOffset;
+        return this.traceContext.utcOffset;
       case TimestampFormat.TraceTz:
-        return this.traceTime.traceTzOffset;
+        return this.traceContext.traceTzOffset;
       default:
         const x: never = fmt;
         throw new Error(`Unsupported format ${x}`);
@@ -859,4 +871,15 @@
   }
 }
 
+// Returns the time span of the current selection, or the visible window if
+// there is no current selection.
+export function getTimeSpanOfSelectionOrVisibleWindow(): Span<time, duration> {
+  const range = globals.findTimeRangeOfSelection();
+  if (exists(range)) {
+    return new TimeSpan(range.start, range.end);
+  } else {
+    return globals.stateVisibleTime();
+  }
+}
+
 export const globals = new Globals();
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index c583e2d..0dc71bd 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -57,12 +57,9 @@
 export class NotesPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
 
   hoveredX: null | number = null;
 
-  constructor(readonly key: string) {}
-
   render(): m.Children {
     const allCollapsed = Object.values(globals.state.trackGroups).every(
       (group) => group.collapsed,
diff --git a/ui/src/frontend/omnibox_manager.ts b/ui/src/frontend/omnibox_manager.ts
new file mode 100644
index 0000000..bb09810
--- /dev/null
+++ b/ui/src/frontend/omnibox_manager.ts
@@ -0,0 +1,155 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {raf} from '../core/raf_scheduler';
+
+export enum OmniboxMode {
+  Search,
+  Query,
+  Command,
+  Prompt,
+}
+
+export interface PromptOption {
+  key: string;
+  displayName: string;
+}
+
+interface Prompt {
+  text: string;
+  options?: PromptOption[];
+  resolve(result: string): void;
+  reject(): void;
+}
+
+const defaultMode = OmniboxMode.Search;
+
+export class OmniboxManager {
+  private _omniboxMode = defaultMode;
+  private _focusOmniboxNextRender = false;
+  private _pendingCursorPlacement?: number;
+  private _pendingPrompt?: Prompt;
+  private _text = '';
+  private _omniboxSelectionIndex = 0;
+
+  get omniboxMode(): OmniboxMode {
+    return this._omniboxMode;
+  }
+
+  get pendingPrompt(): Prompt | undefined {
+    return this._pendingPrompt;
+  }
+
+  get text(): string {
+    return this._text;
+  }
+
+  get omniboxSelectionIndex(): number {
+    return this._omniboxSelectionIndex;
+  }
+
+  get focusOmniboxNextRender(): boolean {
+    return this._focusOmniboxNextRender;
+  }
+
+  get pendingCursorPlacement(): number | undefined {
+    return this._pendingCursorPlacement;
+  }
+
+  setText(value: string): void {
+    this._text = value;
+  }
+
+  setOmniboxSelectionIndex(index: number): void {
+    this._omniboxSelectionIndex = index;
+  }
+
+  focusOmnibox(cursorPlacement?: number): void {
+    this._focusOmniboxNextRender = true;
+    this._pendingCursorPlacement = cursorPlacement;
+    raf.scheduleFullRedraw();
+  }
+
+  clearOmniboxFocusFlag(): void {
+    this._focusOmniboxNextRender = false;
+    this._pendingCursorPlacement = undefined;
+  }
+
+  setMode(mode: OmniboxMode): void {
+    this._omniboxMode = mode;
+    this.resetOmniboxText();
+    this.rejectPendingPrompt();
+    raf.scheduleFullRedraw();
+  }
+
+  // Start a prompt. If options are supplied, the user must pick one from the
+  // list, otherwise the input is free-form text.
+  prompt(text: string, options?: PromptOption[]): Promise<string> {
+    this._omniboxMode = OmniboxMode.Prompt;
+    this.resetOmniboxText();
+    this.rejectPendingPrompt();
+
+    const promise = new Promise<string>((resolve, reject) => {
+      this._pendingPrompt = {
+        text,
+        options,
+        resolve,
+        reject,
+      };
+    });
+
+    this._focusOmniboxNextRender = true;
+    raf.scheduleFullRedraw();
+
+    return promise;
+  }
+
+  // Resolve the pending prompt with a value to return to the prompter.
+  resolvePrompt(value: string): void {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.resolve(value);
+      this._pendingPrompt = undefined;
+    }
+    this.setMode(OmniboxMode.Search);
+  }
+
+  // Reject the prompt outright. Doing this will force the owner of the prompt
+  // promise to catch, so only do this when things go seriously wrong.
+  // Use |resolvePrompt(null)| to indicate cancellation.
+  rejectPrompt(): void {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.reject();
+      this._pendingPrompt = undefined;
+    }
+    this.setMode(OmniboxMode.Search);
+  }
+
+  reset(): void {
+    this.setMode(defaultMode);
+    this.resetOmniboxText();
+    raf.scheduleFullRedraw();
+  }
+
+  private rejectPendingPrompt() {
+    if (this._pendingPrompt) {
+      this._pendingPrompt.reject();
+      this._pendingPrompt = undefined;
+    }
+  }
+
+  private resetOmniboxText() {
+    this._text = '';
+    this._omniboxSelectionIndex = 0;
+  }
+}
diff --git a/ui/src/frontend/overview_timeline_panel.ts b/ui/src/frontend/overview_timeline_panel.ts
index a47434a..4ff95bb 100644
--- a/ui/src/frontend/overview_timeline_panel.ts
+++ b/ui/src/frontend/overview_timeline_panel.ts
@@ -42,7 +42,6 @@
   private static HANDLE_SIZE_PX = 5;
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
 
   private width = 0;
   private gesture?: DragGestureHandler;
@@ -51,8 +50,6 @@
   private dragStrategy?: DragStrategy;
   private readonly boundOnMouseMove = this.onMouseMove.bind(this);
 
-  constructor(readonly key: string) {}
-
   // Must explicitly type now; arguments types are no longer auto-inferred.
   // https://github.com/Microsoft/TypeScript/issues/1373
   onupdate({dom}: m.CVnodeDOM) {
diff --git a/ui/src/frontend/panel_container.ts b/ui/src/frontend/panel_container.ts
index 9214430..5611a19 100644
--- a/ui/src/frontend/panel_container.ts
+++ b/ui/src/frontend/panel_container.ts
@@ -47,22 +47,20 @@
 const CANVAS_OVERDRAW_PX = 100;
 
 export interface Panel {
-  kind: 'panel';
+  readonly kind: 'panel';
   render(): m.Children;
-  selectable: boolean;
-  key: string;
-  trackKey?: string;
-  trackGroupId?: string;
+  readonly selectable: boolean;
+  readonly trackKey?: string; // Defined if this panel represents are track
+  readonly groupKey?: string; // Defined if this panel represents a group - i.e. a group summary track
   renderCanvas(ctx: CanvasRenderingContext2D, size: PanelSize): void;
   getSliceRect?(tStart: time, tDur: time, depth: number): SliceRect | undefined;
 }
 
 export interface PanelGroup {
-  kind: 'group';
-  collapsed: boolean;
-  header: Panel;
-  childPanels: Panel[];
-  trackGroupId: string;
+  readonly kind: 'group';
+  readonly collapsed: boolean;
+  readonly header: Panel;
+  readonly childPanels: Panel[];
 }
 
 export type PanelOrGroup = Panel | PanelGroup;
@@ -74,7 +72,7 @@
 }
 
 interface PanelInfo {
-  id: string; // Can be == '' for singleton panels.
+  trackOrGroupKey: string; // Can be == '' for singleton panels.
   panel: Panel;
   height: number;
   width: number;
@@ -91,7 +89,7 @@
   private panelContainerHeight = 0;
 
   // Updated every render cycle in the view() hook
-  private panelByKey = new Map<string, Panel>();
+  private panelById = new Map<string, Panel>();
 
   // Updated every render cycle in the oncreate/onupdate hook
   private panelInfos: PanelInfo[] = [];
@@ -179,11 +177,11 @@
         tracks.push(panel.trackKey);
         continue;
       }
-      if (panel.trackGroupId !== undefined) {
-        const trackGroup = globals.state.trackGroups[panel.trackGroupId];
+      if (panel.groupKey !== undefined) {
+        const trackGroup = globals.state.trackGroups[panel.groupKey];
         // Only select a track group and all child tracks if it is closed.
         if (trackGroup.collapsed) {
-          tracks.push(panel.trackGroupId);
+          tracks.push(panel.groupKey);
           for (const track of trackGroup.tracks) {
             tracks.push(track);
           }
@@ -258,37 +256,40 @@
     this.trash.dispose();
   }
 
-  renderPanel(node: Panel, key: string, extraClass = ''): m.Vnode {
-    assertFalse(this.panelByKey.has(key));
-    this.panelByKey.set(key, node);
-    return m(`.pf-panel${extraClass}`, {key, 'data-key': key}, node.render());
+  renderPanel(node: Panel, panelId: string, extraClass = ''): m.Vnode {
+    assertFalse(this.panelById.has(panelId));
+    this.panelById.set(panelId, node);
+    return m(
+      `.pf-panel${extraClass}`,
+      {'data-panel-id': panelId},
+      node.render(),
+    );
   }
 
   // Render a tree of panels into one vnode. Argument `path` is used to build
   // `key` attribute for intermediate tree vnodes: otherwise Mithril internals
   // will complain about keyed and non-keyed vnodes mixed together.
-  renderTree(node: PanelOrGroup, path: string): m.Vnode {
+  renderTree(node: PanelOrGroup, panelId: string): m.Vnode {
     if (node.kind === 'group') {
       return m(
         'div.pf-panel-group',
-        {key: path},
         this.renderPanel(
           node.header,
-          `${path}-header`,
+          `${panelId}-header`,
           node.collapsed ? '' : '.pf-sticky',
         ),
         ...node.childPanels.map((child, index) =>
-          this.renderTree(child, `${path}-${index}`),
+          this.renderTree(child, `${panelId}-${index}`),
         ),
       );
     }
-    return this.renderPanel(node, assertExists(node.key));
+    return this.renderPanel(node, panelId);
   }
 
   view({attrs}: m.CVnode<PanelContainerAttrs>) {
-    this.panelByKey.clear();
+    this.panelById.clear();
     const children = attrs.panels.map((panel, index) =>
-      this.renderTree(panel, `track-tree-${index}`),
+      this.renderTree(panel, `${index}`),
     );
 
     return m(
@@ -316,14 +317,15 @@
     this.panelContainerHeight = domRect.height;
 
     dom.querySelectorAll('.pf-panel').forEach((panelElement) => {
-      const key = assertExists(panelElement.getAttribute('data-key'));
-      const panel = assertExists(this.panelByKey.get(key));
+      const panelHTMLElement = toHTMLElement(panelElement);
+      const panelId = assertExists(panelHTMLElement.dataset.panelId);
+      const panel = assertExists(this.panelById.get(panelId));
 
       // NOTE: the id can be undefined for singletons like overview timeline.
-      const id = panel.trackKey || panel.trackGroupId || '';
+      const key = panel.trackKey || panel.groupKey || '';
       const rect = panelElement.getBoundingClientRect();
       this.panelInfos.push({
-        id,
+        trackOrGroupKey: key,
         height: rect.height,
         width: rect.width,
         clientX: rect.x,
@@ -440,7 +442,7 @@
     let selectedTracksMaxY = this.panelContainerTop;
     let trackFromCurrentContainerSelected = false;
     for (let i = 0; i < this.panelInfos.length; i++) {
-      if (area.tracks.includes(this.panelInfos[i].id)) {
+      if (area.tracks.includes(this.panelInfos[i].trackOrGroupKey)) {
         trackFromCurrentContainerSelected = true;
         selectedTracksMinY = Math.min(
           selectedTracksMinY,
diff --git a/ui/src/frontend/permalink.ts b/ui/src/frontend/permalink.ts
new file mode 100644
index 0000000..888fe55
--- /dev/null
+++ b/ui/src/frontend/permalink.ts
@@ -0,0 +1,250 @@
+// Copyright (C) 2024 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import {produce} from 'immer';
+import {assertExists} from '../base/logging';
+import {runValidator} from '../base/validators';
+import {Actions} from '../common/actions';
+import {ConversionJobStatus} from '../common/conversion_jobs';
+import {
+  createEmptyNonSerializableState,
+  createEmptyState,
+} from '../common/empty_state';
+import {EngineConfig, ObjectById, STATE_VERSION, State} from '../common/state';
+import {
+  BUCKET_NAME,
+  TraceGcsUploader,
+  buggyToSha256,
+  deserializeStateObject,
+  saveState,
+  toSha256,
+} from '../common/upload_utils';
+import {
+  RecordConfig,
+  recordConfigValidator,
+} from '../controller/record_config_types';
+import {globals} from './globals';
+import {
+  publishConversionJobStatusUpdate,
+  publishPermalinkHash,
+} from './publish';
+import {Router} from './router';
+import {showModal} from '../widgets/modal';
+
+export interface PermalinkOptions {
+  isRecordingConfig?: boolean;
+}
+
+export async function createPermalink(
+  options: PermalinkOptions = {},
+): Promise<void> {
+  const {isRecordingConfig = false} = options;
+  const jobName = 'create_permalink';
+  publishConversionJobStatusUpdate({
+    jobName,
+    jobStatus: ConversionJobStatus.InProgress,
+  });
+
+  try {
+    const hash = await createPermalinkInternal(isRecordingConfig);
+    publishPermalinkHash(hash);
+  } finally {
+    publishConversionJobStatusUpdate({
+      jobName,
+      jobStatus: ConversionJobStatus.NotRunning,
+    });
+  }
+}
+
+async function createPermalinkInternal(
+  isRecordingConfig: boolean,
+): Promise<string> {
+  let uploadState: State | RecordConfig = globals.state;
+
+  if (isRecordingConfig) {
+    uploadState = globals.state.recordConfig;
+  } else {
+    const engine = assertExists(globals.getCurrentEngine());
+    let dataToUpload: File | ArrayBuffer | undefined = undefined;
+    let traceName = `trace ${engine.id}`;
+    if (engine.source.type === 'FILE') {
+      dataToUpload = engine.source.file;
+      traceName = dataToUpload.name;
+    } else if (engine.source.type === 'ARRAY_BUFFER') {
+      dataToUpload = engine.source.buffer;
+    } else if (engine.source.type !== 'URL') {
+      throw new Error(`Cannot share trace ${JSON.stringify(engine.source)}`);
+    }
+
+    if (dataToUpload !== undefined) {
+      updateStatus(`Uploading ${traceName}`);
+      const uploader = new TraceGcsUploader(dataToUpload, () => {
+        switch (uploader.state) {
+          case 'UPLOADING':
+            const statusTxt = `Uploading ${uploader.getEtaString()}`;
+            updateStatus(statusTxt);
+            break;
+          case 'UPLOADED':
+            // Convert state to use URLs and remove permalink.
+            const url = uploader.uploadedUrl;
+            uploadState = produce(globals.state, (draft) => {
+              assertExists(draft.engine).source = {type: 'URL', url};
+            });
+            break;
+          case 'ERROR':
+            updateStatus(`Upload failed ${uploader.error}`);
+            break;
+        } // switch (state)
+      }); // onProgress
+      await uploader.waitForCompletion();
+    }
+  }
+
+  // Upload state.
+  updateStatus(`Creating permalink...`);
+  const hash = await saveState(uploadState);
+  updateStatus(`Permalink ready`);
+  return hash;
+}
+
+function updateStatus(msg: string): void {
+  // TODO(hjd): Unify loading updates.
+  globals.dispatch(
+    Actions.updateStatus({
+      msg,
+      timestamp: Date.now() / 1000,
+    }),
+  );
+}
+
+export async function loadPermalink(hash: string): Promise<void> {
+  // Otherwise, this is a request to load the permalink.
+  const stateOrConfig = await loadState(hash);
+
+  if (isRecordConfig(stateOrConfig)) {
+    // This permalink state only contains a RecordConfig. Show the
+    // recording page with the config, but keep other state as-is.
+    const validConfig = runValidator(
+      recordConfigValidator,
+      stateOrConfig as unknown,
+    ).result;
+    globals.dispatch(Actions.setRecordConfig({config: validConfig}));
+    Router.navigate('#!/record');
+    return;
+  }
+  globals.dispatch(Actions.setState({newState: stateOrConfig}));
+}
+
+async function loadState(id: string): Promise<State | RecordConfig> {
+  const url = `https://storage.googleapis.com/${BUCKET_NAME}/${id}`;
+  const response = await fetch(url);
+  if (!response.ok) {
+    throw new Error(
+      `Could not fetch permalink.\n` +
+        `Are you sure the id (${id}) is correct?\n` +
+        `URL: ${url}`,
+    );
+  }
+  const text = await response.text();
+  const stateHash = await toSha256(text);
+  const state = deserializeStateObject<State>(text);
+  if (stateHash !== id) {
+    // Old permalinks incorrectly dropped some digits from the
+    // hexdigest of the SHA256. We don't want to invalidate those
+    // links so we also compute the old string and try that here
+    // also.
+    const buggyStateHash = await buggyToSha256(text);
+    if (buggyStateHash !== id) {
+      throw new Error(`State hash does not match ${id} vs. ${stateHash}`);
+    }
+  }
+  if (!isRecordConfig(state)) {
+    return upgradeState(state);
+  }
+  return state;
+}
+
+function isRecordConfig(
+  stateOrConfig: State | RecordConfig,
+): stateOrConfig is RecordConfig {
+  const mode = (stateOrConfig as {mode?: string}).mode;
+  return (
+    mode !== undefined &&
+    ['STOP_WHEN_FULL', 'RING_BUFFER', 'LONG_TRACE'].includes(mode)
+  );
+}
+
+function upgradeState(state: State): State {
+  if (state.engine !== undefined && state.engine.source.type !== 'URL') {
+    // All permalink traces should be modified to have a source.type=URL
+    // pointing to the uploaded trace. Due to a bug in some older version
+    // of the UI (b/327049372), an upload failure can end up with a state that
+    // has type=FILE but a null file object. If this happens, invalidate the
+    // trace and show a message.
+    showModal({
+      title: 'Cannot load trace permalink',
+      content: m(
+        'div',
+        'The permalink stored on the server is corrupted ' +
+          'and cannot be loaded.',
+      ),
+    });
+    return createEmptyState();
+  }
+
+  if (state.version !== STATE_VERSION) {
+    const newState = createEmptyState();
+    // Old permalinks from state versions prior to version 24
+    // have multiple engines of which only one is identified as the
+    // current engine via currentEngineId. Handle this case:
+    if (isMultiEngineState(state)) {
+      const engineId = state.currentEngineId;
+      if (engineId !== undefined) {
+        newState.engine = state.engines[engineId];
+      }
+    } else {
+      newState.engine = state.engine;
+    }
+
+    if (newState.engine !== undefined) {
+      newState.engine.ready = false;
+    }
+    const message =
+      `Unable to parse old state version. Discarding state ` +
+      `and loading trace.`;
+    console.warn(message);
+    updateStatus(message);
+    return newState;
+  } else {
+    // Loaded state is presumed to be compatible with the State type
+    // definition in the app. However, a non-serializable part has to be
+    // recreated.
+    state.nonSerializableState = createEmptyNonSerializableState();
+  }
+  return state;
+}
+
+interface MultiEngineState {
+  currentEngineId?: string;
+  engines: ObjectById<EngineConfig>;
+}
+
+function isMultiEngineState(
+  state: State | MultiEngineState,
+): state is MultiEngineState {
+  if ((state as MultiEngineState).engines !== undefined) {
+    return true;
+  }
+  return false;
+}
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index bfb34ca..2fbc6df 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -31,7 +31,7 @@
   SliceDetails,
   ThreadDesc,
   ThreadStateDetails,
-  TraceTime,
+  TraceContext,
 } from './globals';
 import {findCurrentSelection} from './keyboard_event_handler';
 
@@ -96,8 +96,8 @@
   globals.publishRedraw();
 }
 
-export function publishTraceDetails(details: TraceTime): void {
-  globals.traceTime = details;
+export function publishTraceContext(details: TraceContext): void {
+  globals.traceContext = details;
   globals.publishRedraw();
 }
 
@@ -208,3 +208,8 @@
   globals.showPanningHint = true;
   globals.publishRedraw();
 }
+
+export function publishPermalinkHash(hash: string | undefined): void {
+  globals.permalinkHash = hash;
+  globals.publishRedraw();
+}
diff --git a/ui/src/frontend/record_page.ts b/ui/src/frontend/record_page.ts
index ad59ea8..49cc7b3 100644
--- a/ui/src/frontend/record_page.ts
+++ b/ui/src/frontend/record_page.ts
@@ -57,6 +57,7 @@
 import {RecordingSectionAttrs} from './recording/recording_sections';
 import {RecordingSettings} from './recording/recording_settings';
 import {EtwSettings} from './recording/etw_settings';
+import {createPermalink} from './permalink';
 
 export const PERSIST_CONFIG_FLAG = featureFlags.register({
   id: 'persistConfigsUI',
@@ -176,9 +177,7 @@
           'button.permalinkconfig',
           {
             onclick: () => {
-              globals.dispatch(
-                Actions.createPermalink({isRecordingConfig: true}),
-              );
+              createPermalink({isRecordingConfig: true});
             },
           },
           'Share recording settings',
diff --git a/ui/src/frontend/record_page_v2.ts b/ui/src/frontend/record_page_v2.ts
index db87cfa..dc1d015 100644
--- a/ui/src/frontend/record_page_v2.ts
+++ b/ui/src/frontend/record_page_v2.ts
@@ -16,7 +16,6 @@
 import {Attributes} from 'mithril';
 
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
 import {RecordingConfigUtils} from '../common/recordingV2/recording_config_utils';
 import {
   ChromeTargetInfo,
@@ -57,6 +56,7 @@
 import {RecordingSettings} from './recording/recording_settings';
 import {FORCE_RESET_MESSAGE} from './recording/recording_ui_utils';
 import {showAddNewTargetModal} from './recording/reset_target_modal';
+import {createPermalink} from './permalink';
 
 const START_RECORDING_MESSAGE = 'Start Recording';
 
@@ -186,9 +186,7 @@
           'button.permalinkconfig',
           {
             onclick: () => {
-              globals.dispatch(
-                Actions.createPermalink({isRecordingConfig: true}),
-              );
+              createPermalink({isRecordingConfig: true});
             },
           },
           'Share recording settings',
diff --git a/ui/src/frontend/scroll_helper.ts b/ui/src/frontend/scroll_helper.ts
index f643001..cd7e94d 100644
--- a/ui/src/frontend/scroll_helper.ts
+++ b/ui/src/frontend/scroll_helper.ts
@@ -18,7 +18,7 @@
   HighPrecisionTime,
   HighPrecisionTimeSpan,
 } from '../common/high_precision_time';
-import {getContainingTrackId} from '../common/state';
+import {getContainingGroupKey} from '../common/state';
 
 import {globals} from './globals';
 
@@ -127,12 +127,12 @@
   }
 
   let trackGroup = null;
-  const trackGroupId = getContainingTrackId(globals.state, trackKeyString);
-  if (trackGroupId) {
-    trackGroup = document.querySelector('#track_' + trackGroupId);
+  const groupKey = getContainingGroupKey(globals.state, trackKeyString);
+  if (groupKey) {
+    trackGroup = document.querySelector('#track_' + groupKey);
   }
 
-  if (!trackGroupId || !trackGroup) {
+  if (!groupKey || !trackGroup) {
     console.error(`Can't scroll, track (${trackKeyString}) not found.`);
     return;
   }
@@ -142,7 +142,7 @@
   if (openGroup) {
     // After the track exists in the dom, it will be scrolled to.
     globals.scrollToTrackKey = trackKey;
-    globals.dispatch(Actions.toggleTrackGroupCollapsed({trackGroupId}));
+    globals.dispatch(Actions.toggleTrackGroupCollapsed({groupKey}));
     return;
   } else {
     trackGroup.scrollIntoView({behavior: 'smooth', block: 'nearest'});
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 5b21ded..7bf0f0e 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -74,8 +74,6 @@
   }
 
   private async dropTrackTable(): Promise<void> {
-    if (this.engine.isAlive) {
-      await this.engine.query(`drop table if exists ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`drop table if exists ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index c292fe6..3eebb83 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -17,7 +17,7 @@
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
   CustomSqlTableSliceTrack,
-} from '../core_plugins/custom_sql_table_slices';
+} from './tracks/custom_sql_table_slice_track';
 import {NamedSliceTrackTypes} from './named_slice_track';
 import {ARG_PREFIX, SliceColumns, SqlDataSource} from './debug_tracks';
 import {uuidv4Sql} from '../base/uuid';
@@ -108,8 +108,6 @@
   }
 
   private async destroyTrackTable() {
-    if (this.engine.isAlive) {
-      await this.engine.query(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
-    }
+    await this.engine.tryQuery(`DROP TABLE IF EXISTS ${this.sqlTableName}`);
   }
 }
diff --git a/ui/src/frontend/thread_state_tab.ts b/ui/src/frontend/thread_state_tab.ts
index fac6b8a..f028b15 100644
--- a/ui/src/frontend/thread_state_tab.ts
+++ b/ui/src/frontend/thread_state_tab.ts
@@ -15,7 +15,6 @@
 import m from 'mithril';
 
 import {Time, time} from '../base/time';
-import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Anchor} from '../widgets/anchor';
 import {Button} from '../widgets/button';
@@ -322,14 +321,13 @@
         label: 'Critical path lite',
         intent: Intent.Primary,
         onclick: () =>
-          runQuery(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span;`,
-            this.engine,
-          ).then(() =>
-            addDebugSliceTrack(
-              this.engine,
-              {
-                sqlSource: `
+          this.engine
+            .query(`INCLUDE PERFETTO MODULE sched.thread_executing_span;`)
+            .then(() =>
+              addDebugSliceTrack(
+                this.engine,
+                {
+                  sqlSource: `
                     SELECT
                       cr.id,
                       cr.utid,
@@ -347,26 +345,27 @@
                     JOIN thread USING(utid)
                     JOIN process USING(upid)
                   `,
-                columns: sliceLiteColumnNames,
-              },
-              `${this.state?.thread?.name}`,
-              sliceLiteColumns,
-              sliceLiteColumnNames,
+                  columns: sliceLiteColumnNames,
+                },
+                `${this.state?.thread?.name}`,
+                sliceLiteColumns,
+                sliceLiteColumnNames,
+              ),
             ),
-          ),
       }),
       m(Button, {
         label: 'Critical path',
         intent: Intent.Primary,
         onclick: () =>
-          runQuery(
-            `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
-            this.engine,
-          ).then(() =>
-            addDebugSliceTrack(
-              this.engine,
-              {
-                sqlSource: `
+          this.engine
+            .query(
+              `INCLUDE PERFETTO MODULE sched.thread_executing_span_with_slice;`,
+            )
+            .then(() =>
+              addDebugSliceTrack(
+                this.engine,
+                {
+                  sqlSource: `
                     SELECT cr.id, cr.utid, cr.ts, cr.dur, cr.name, cr.table_name
                       FROM
                         _thread_executing_span_critical_path_stack(
@@ -375,13 +374,13 @@
                           trace_bounds.end_ts - trace_bounds.start_ts) cr,
                         trace_bounds WHERE name IS NOT NULL
                   `,
-                columns: sliceColumnNames,
-              },
-              `${this.state?.thread?.name}`,
-              sliceColumns,
-              sliceColumnNames,
+                  columns: sliceColumnNames,
+                },
+                `${this.state?.thread?.name}`,
+                sliceColumns,
+                sliceColumnNames,
+              ),
             ),
-          ),
       }),
     ];
   }
diff --git a/ui/src/frontend/tickmark_panel.ts b/ui/src/frontend/tickmark_panel.ts
index 90f3e4c..632052d 100644
--- a/ui/src/frontend/tickmark_panel.ts
+++ b/ui/src/frontend/tickmark_panel.ts
@@ -31,9 +31,6 @@
 export class TickmarkPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.tickbar');
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index af1f3df..16c22ed 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -32,9 +32,6 @@
 export class TimeAxisPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.time-axis-panel');
@@ -57,16 +54,16 @@
         break;
       case TimestampFormat.UTC:
         const offsetDate = Time.toDate(
-          globals.traceTime.utcOffset,
-          globals.traceTime.realtimeOffset,
+          globals.traceContext.utcOffset,
+          globals.traceContext.realtimeOffset,
         );
         const dateStr = toISODateOnly(offsetDate);
         ctx.fillText(`UTC ${dateStr}`, 6, 10);
         break;
       case TimestampFormat.TraceTz:
         const offsetTzDate = Time.toDate(
-          globals.traceTime.traceTzOffset,
-          globals.traceTime.realtimeOffset,
+          globals.traceContext.traceTzOffset,
+          globals.traceContext.realtimeOffset,
         );
         const dateTzStr = toISODateOnly(offsetTzDate);
         ctx.fillText(dateTzStr, 6, 10);
diff --git a/ui/src/frontend/time_selection_panel.ts b/ui/src/frontend/time_selection_panel.ts
index d77b095..66e401c 100644
--- a/ui/src/frontend/time_selection_panel.ts
+++ b/ui/src/frontend/time_selection_panel.ts
@@ -140,9 +140,6 @@
 export class TimeSelectionPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = false;
-  readonly trackKey = undefined;
-
-  constructor(readonly key: string) {}
 
   render(): m.Children {
     return m('.time-selection-panel');
diff --git a/ui/src/frontend/trace_attrs.ts b/ui/src/frontend/trace_attrs.ts
index 61f991e..ff80c85 100644
--- a/ui/src/frontend/trace_attrs.ts
+++ b/ui/src/frontend/trace_attrs.ts
@@ -13,8 +13,8 @@
 // limitations under the License.
 
 import {assertExists} from '../base/logging';
-import {Actions} from '../common/actions';
 import {TraceArrayBufferSource} from '../common/state';
+import {createPermalink} from './permalink';
 import {showModal} from '../widgets/modal';
 
 import {onClickCopy} from './clipboard';
@@ -74,7 +74,7 @@
   );
   if (result) {
     globals.logging.logEvent('Trace Actions', 'Create permalink');
-    globals.dispatch(Actions.createPermalink({isRecordingConfig: false}));
+    createPermalink();
   }
 }
 
diff --git a/ui/src/frontend/trace_info_page.ts b/ui/src/frontend/trace_info_page.ts
index c8d7547..87f270a 100644
--- a/ui/src/frontend/trace_info_page.ts
+++ b/ui/src/frontend/trace_info_page.ts
@@ -14,12 +14,51 @@
 
 import m from 'mithril';
 
-import {QueryResponse, runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
 import {Engine} from '../trace_processor/engine';
 
 import {globals} from './globals';
 import {createPage} from './pages';
+import {QueryResult, UNKNOWN} from '../trace_processor/query_result';
+
+function getEngine(name: string): Engine | undefined {
+  const currentEngine = globals.getCurrentEngine();
+  if (currentEngine === undefined) return undefined;
+  const engineId = currentEngine.id;
+  return globals.engines.get(engineId)?.getProxy(name);
+}
+
+/**
+ * Extracts and copies fields from a source object based on the keys present in
+ * a spec object, effectively creating a new object that includes only the
+ * fields that are present in the spec object.
+ *
+ * @template S - A type representing the spec object, a subset of T.
+ * @template T - A type representing the source object, a superset of S.
+ *
+ * @param {T} source - The source object containing the full set of properties.
+ * @param {S} spec - The specification object whose keys determine which fields
+ * should be extracted from the source object.
+ *
+ * @returns {S} A new object containing only the fields from the source object
+ * that are also present in the specification object.
+ *
+ * @example
+ * const fullObject = { foo: 123, bar: '123', baz: true };
+ * const spec = { foo: 0, bar: '' };
+ * const result = pickFields(fullObject, spec);
+ * console.log(result); // Output: { foo: 123, bar: '123' }
+ */
+function pickFields<S extends Record<string, unknown>, T extends S>(
+  source: T,
+  spec: S,
+): S {
+  const result: Record<string, unknown> = {};
+  for (const key of Object.keys(spec)) {
+    result[key] = source[key];
+  }
+  return result as S;
+}
 
 interface StatsSectionAttrs {
   title: string;
@@ -29,58 +68,71 @@
   queryId: string;
 }
 
-function getEngine(name: string): Engine | undefined {
-  const currentEngine = globals.getCurrentEngine();
-  if (currentEngine === undefined) return undefined;
-  const engineId = currentEngine.id;
-  return globals.engines.get(engineId)?.getProxy(name);
-}
+const statsSpec = {
+  name: UNKNOWN,
+  value: UNKNOWN,
+  description: UNKNOWN,
+  idx: UNKNOWN,
+  severity: UNKNOWN,
+  source: UNKNOWN,
+};
+
+type StatsSectionRow = typeof statsSpec;
 
 // Generic class that generate a <section> + <table> from the stats table.
 // The caller defines the query constraint, title and styling.
 // Used for errors, data losses and debugging sections.
 class StatsSection implements m.ClassComponent<StatsSectionAttrs> {
-  private queryResponse?: QueryResponse;
+  private data?: StatsSectionRow[];
 
   constructor({attrs}: m.CVnode<StatsSectionAttrs>) {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select name, value, cast(ifnull(idx, '') as text) as idx,
-              description, severity, source from stats
-              where ${attrs.sqlConstraints || '1=1'}
-              order by name, idx`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      select
+        name,
+        value,
+        cast(ifnull(idx, '') as text) as idx,
+        description,
+        severity,
+        source from stats
+      where ${attrs.sqlConstraints || '1=1'}
+      order by name, idx
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: StatsSectionRow[] = [];
+      const it = resp.iter(statsSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, statsSpec));
+      }
+      this.data = data;
+
       raf.scheduleFullRedraw();
     });
   }
 
   view({attrs}: m.CVnode<StatsSectionAttrs>) {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
-    if (resp.error) throw new Error(resp.error);
 
-    const tableRows = [];
-    for (const row of resp.rows) {
+    const tableRows = data.map((row) => {
       const help = [];
-      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-      if (row.description) {
+      if (Boolean(row.description)) {
         help.push(m('i.material-icons.contextual-help', 'help_outline'));
       }
       const idx = row.idx !== '' ? `[${row.idx}]` : '';
-      tableRows.push(
-        m(
-          'tr',
-          m('td.name', {title: row.description}, `${row.name}${idx}`, help),
-          m('td', `${row.value}`),
-          m('td', `${row.severity} (${row.source})`),
-        ),
+      return m(
+        'tr',
+        m('td.name', {title: row.description}, `${row.name}${idx}`, help),
+        m('td', `${row.value}`),
+        m('td', `${row.severity} (${row.source})`),
       );
-    }
+    });
 
     return m(
       `section${attrs.cssClass}`,
@@ -107,49 +159,63 @@
   }
 }
 
+const traceMetadataRowSpec = {name: UNKNOWN, value: UNKNOWN};
+
+type TraceMetadataRow = typeof traceMetadataRowSpec;
+
 class TraceMetadata implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+  private data?: TraceMetadataRow[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `with 
-          metadata_with_priorities as (select
-            name, ifnull(str_value, cast(int_value as text)) as value,
-            name in (
-               "trace_size_bytes", 
-               "cr-os-arch",
-               "cr-os-name",
-               "cr-os-version",
-               "cr-physical-memory",
-               "cr-product-version",
-               "cr-hardware-class"
-            ) as priority 
-            from metadata
-          )
-          select name, value
-          from metadata_with_priorities 
-          order by priority desc, name`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      with metadata_with_priorities as (
+        select
+          name,
+          ifnull(str_value, cast(int_value as text)) as value,
+          name in (
+            "trace_size_bytes", 
+            "cr-os-arch",
+            "cr-os-name",
+            "cr-os-version",
+            "cr-physical-memory",
+            "cr-product-version",
+            "cr-hardware-class"
+          ) as priority 
+        from metadata
+      )
+      select
+        name,
+        value
+      from metadata_with_priorities 
+      order by
+        priority desc,
+        name
+    `;
+
+    engine.query(query).then((resp: QueryResult) => {
+      const tableRows: TraceMetadataRow[] = [];
+      const it = resp.iter(traceMetadataRowSpec);
+      for (; it.valid(); it.next()) {
+        tableRows.push(pickFields(it, traceMetadataRowSpec));
+      }
+      this.data = tableRows;
       raf.scheduleFullRedraw();
     });
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
 
-    const tableRows = [];
-    for (const row of resp.rows) {
-      tableRows.push(
-        m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`)),
-      );
-    }
+    const tableRows = data.map((row) => {
+      return m('tr', m('td.name', `${row.name}`), m('td', `${row.value}`));
+    });
 
     return m(
       'section',
@@ -163,40 +229,68 @@
   }
 }
 
+const androidGameInterventionRowSpec = {
+  package_name: UNKNOWN,
+  uid: UNKNOWN,
+  current_mode: UNKNOWN,
+  standard_mode_supported: UNKNOWN,
+  standard_mode_downscale: UNKNOWN,
+  standard_mode_use_angle: UNKNOWN,
+  standard_mode_fps: UNKNOWN,
+  perf_mode_supported: UNKNOWN,
+  perf_mode_downscale: UNKNOWN,
+  perf_mode_use_angle: UNKNOWN,
+  perf_mode_fps: UNKNOWN,
+  battery_mode_supported: UNKNOWN,
+  battery_mode_downscale: UNKNOWN,
+  battery_mode_use_angle: UNKNOWN,
+  battery_mode_fps: UNKNOWN,
+};
+
+type AndroidGameInterventionRow = typeof androidGameInterventionRowSpec;
+
 class AndroidGameInterventionList implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+  private data?: AndroidGameInterventionRow[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select
-                package_name,
-                uid,
-                current_mode,
-                standard_mode_supported,
-                standard_mode_downscale,
-                standard_mode_use_angle,
-                standard_mode_fps,
-                perf_mode_supported,
-                perf_mode_downscale,
-                perf_mode_use_angle,
-                perf_mode_fps,
-                battery_mode_supported,
-                battery_mode_downscale,
-                battery_mode_use_angle,
-                battery_mode_fps
-                from android_game_intervention_list`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
+    const query = `
+      select
+        package_name,
+        uid,
+        current_mode,
+        standard_mode_supported,
+        standard_mode_downscale,
+        standard_mode_use_angle,
+        standard_mode_fps,
+        perf_mode_supported,
+        perf_mode_downscale,
+        perf_mode_use_angle,
+        perf_mode_fps,
+        battery_mode_supported,
+        battery_mode_downscale,
+        battery_mode_use_angle,
+        battery_mode_fps
+      from android_game_intervention_list
+    `;
+
+    engine.query(query).then((resp) => {
+      const data: AndroidGameInterventionRow[] = [];
+      const it = resp.iter(androidGameInterventionRowSpec);
+      for (; it.valid(); it.next()) {
+        data.push(pickFields(it, androidGameInterventionRowSpec));
+      }
+      this.data = data;
       raf.scheduleFullRedraw();
     });
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
+    const data = this.data;
+    if (data === undefined || data.length === 0) {
       return m('');
     }
 
@@ -204,7 +298,8 @@
     let standardInterventions = '';
     let perfInterventions = '';
     let batteryInterventions = '';
-    for (const row of resp.rows) {
+
+    for (const row of data) {
       // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
       if (row.standard_mode_supported) {
         standardInterventions = `angle=${row.standard_mode_use_angle},downscale=${row.standard_mode_downscale},fps=${row.standard_mode_fps}`;
@@ -268,46 +363,68 @@
   }
 }
 
-class PackageList implements m.ClassComponent {
-  private queryResponse?: QueryResponse;
+const packageDataSpec = {
+  packageName: UNKNOWN,
+  versionCode: UNKNOWN,
+  debuggable: UNKNOWN,
+  profileableFromShell: UNKNOWN,
+};
+
+type PackageData = typeof packageDataSpec;
+
+class PackageListSection implements m.ClassComponent {
+  private packageList?: PackageData[];
 
   constructor() {
     const engine = getEngine('StatsSection');
     if (engine === undefined) {
       return;
     }
-    const query = `select package_name, version_code, debuggable,
-                profileable_from_shell from package_list`;
-    runQuery(query, engine).then((resp: QueryResponse) => {
-      this.queryResponse = resp;
-      raf.scheduleFullRedraw();
-    });
+    this.loadData(engine);
+  }
+
+  private async loadData(engine: Engine): Promise<void> {
+    const query = `
+      select
+        package_name as packageName,
+        version_code as versionCode,
+        debuggable,
+        profileable_from_shell as profileableFromShell
+      from package_list
+    `;
+
+    const packageList: PackageData[] = [];
+    const result = await engine.query(query);
+    const it = result.iter(packageDataSpec);
+    for (; it.valid(); it.next()) {
+      packageList.push(pickFields(it, packageDataSpec));
+    }
+
+    this.packageList = packageList;
+    raf.scheduleFullRedraw();
   }
 
   view() {
-    const resp = this.queryResponse;
-    if (resp === undefined || resp.totalRowCount === 0) {
-      return m('');
+    const packageList = this.packageList;
+    if (packageList === undefined || packageList.length === 0) {
+      return undefined;
     }
 
-    const tableRows = [];
-    for (const row of resp.rows) {
-      tableRows.push(
+    const tableRows = packageList.map((it) => {
+      return m(
+        'tr',
+        m('td.name', `${it.packageName}`),
+        m('td', `${it.versionCode}`),
+        /* eslint-disable @typescript-eslint/strict-boolean-expressions */
         m(
-          'tr',
-          m('td.name', `${row.package_name}`),
-          m('td', `${row.version_code}`),
-          /* eslint-disable @typescript-eslint/strict-boolean-expressions */
-          m(
-            'td',
-            `${row.debuggable ? 'debuggable' : ''} ${
-              row.profileable_from_shell ? 'profileable' : ''
-            }`,
-          ),
-          /* eslint-enable */
+          'td',
+          `${it.debuggable ? 'debuggable' : ''} ${
+            it.profileableFromShell ? 'profileable' : ''
+          }`,
         ),
+        /* eslint-enable */
       );
-    }
+    });
 
     return m(
       'section',
@@ -348,7 +465,7 @@
         sqlConstraints: `severity = 'data_loss' and value > 0`,
       }),
       m(TraceMetadata),
-      m(PackageList),
+      m(PackageListSection),
       m(AndroidGameInterventionList),
       m(StatsSection, {
         queryId: 'info_all',
diff --git a/ui/src/frontend/trace_url_handler.ts b/ui/src/frontend/trace_url_handler.ts
index a50bed7..0e60724 100644
--- a/ui/src/frontend/trace_url_handler.ts
+++ b/ui/src/frontend/trace_url_handler.ts
@@ -18,6 +18,7 @@
 import {tryGetTrace} from '../common/cache_manager';
 import {showModal} from '../widgets/modal';
 
+import {loadPermalink} from './permalink';
 import {loadAndroidBugToolInfo} from './android_bug_tool';
 import {globals} from './globals';
 import {Route, Router} from './router';
@@ -34,7 +35,7 @@
 export function maybeOpenTraceFromRoute(route: Route) {
   if (route.args.s) {
     // /?s=xxxx for permalinks.
-    globals.dispatch(Actions.loadPermalink({hash: route.args.s}));
+    loadPermalink(route.args.s);
     return;
   }
 
diff --git a/ui/src/frontend/track_group_panel.ts b/ui/src/frontend/track_group_panel.ts
index ec9a43a..10ca865 100644
--- a/ui/src/frontend/track_group_panel.ts
+++ b/ui/src/frontend/track_group_panel.ts
@@ -16,7 +16,7 @@
 
 import {Icons} from '../base/semantic_icons';
 import {Actions} from '../common/actions';
-import {getContainingTrackId, getLegacySelection} from '../common/state';
+import {getContainingGroupKey, getLegacySelection} from '../common/state';
 import {TrackCacheEntry} from '../common/track_cache';
 import {TrackTags} from '../public';
 
@@ -38,8 +38,7 @@
 import {Button} from '../widgets/button';
 
 interface Attrs {
-  trackGroupId: string;
-  key: string;
+  groupKey: string;
   title: string;
   collapsed: boolean;
   trackFSM?: TrackCacheEntry;
@@ -50,16 +49,14 @@
 export class TrackGroupPanel implements Panel {
   readonly kind = 'panel';
   readonly selectable = true;
-  readonly key: string;
-  readonly trackGroupId: string;
+  readonly groupKey: string;
 
   constructor(private attrs: Attrs) {
-    this.trackGroupId = attrs.trackGroupId;
-    this.key = attrs.key;
+    this.groupKey = attrs.groupKey;
   }
 
   render(): m.Children {
-    const {trackGroupId, title, labels, tags, collapsed, trackFSM} = this.attrs;
+    const {groupKey, title, labels, tags, collapsed, trackFSM} = this.attrs;
 
     let name = title;
     if (name[0] === '/') {
@@ -72,25 +69,25 @@
     const searchIndex = globals.state.searchIndex;
     if (searchIndex !== -1) {
       const trackKey = globals.currentSearchResults.trackKeys[searchIndex];
-      const parentTrackId = getContainingTrackId(globals.state, trackKey);
-      if (parentTrackId === trackGroupId) {
+      const containingGroupKey = getContainingGroupKey(globals.state, trackKey);
+      if (containingGroupKey === groupKey) {
         highlightClass = 'flash';
       }
     }
 
     const selection = getLegacySelection(globals.state);
 
-    const trackGroup = globals.state.trackGroups[trackGroupId];
+    const trackGroup = globals.state.trackGroups[groupKey];
     let checkBox = Icons.BlankCheckbox;
     if (selection !== null && selection.kind === 'AREA') {
       const selectedArea = globals.state.areas[selection.areaId];
       if (
-        selectedArea.tracks.includes(trackGroupId) &&
+        selectedArea.tracks.includes(groupKey) &&
         trackGroup.tracks.every((id) => selectedArea.tracks.includes(id))
       ) {
         checkBox = Icons.Checkbox;
       } else if (
-        selectedArea.tracks.includes(trackGroupId) ||
+        selectedArea.tracks.includes(groupKey) ||
         trackGroup.tracks.some((id) => selectedArea.tracks.includes(id))
       ) {
         checkBox = Icons.IndeterminateCheckbox;
@@ -107,7 +104,7 @@
     return m(
       `.track-group-panel[collapsed=${collapsed}]`,
       {
-        id: 'track_' + trackGroupId,
+        id: 'track_' + groupKey,
         oncreate: () => this.onupdate(),
         onupdate: () => this.onupdate(),
       },
@@ -118,7 +115,7 @@
             if (e.defaultPrevented) return;
             globals.dispatch(
               Actions.toggleTrackGroupCollapsed({
-                trackGroupId,
+                groupKey,
               }),
             ),
               e.stopPropagation();
@@ -143,7 +140,7 @@
               onclick: (e: MouseEvent) => {
                 globals.dispatch(
                   Actions.toggleTrackSelection({
-                    id: trackGroupId,
+                    key: groupKey,
                     isTrackGroup: true,
                   }),
                 );
@@ -180,7 +177,7 @@
     if (!selection || selection.kind !== 'AREA') return;
     const selectedArea = globals.state.areas[selection.areaId];
     const selectedAreaDuration = selectedArea.end - selectedArea.start;
-    if (selectedArea.tracks.includes(this.trackGroupId)) {
+    if (selectedArea.tracks.includes(this.groupKey)) {
       ctx.fillStyle = 'rgba(131, 152, 230, 0.3)';
       ctx.fillRect(
         visibleTimeScale.timeToPx(selectedArea.start) + TRACK_SHELL_WIDTH,
diff --git a/ui/src/frontend/track_panel.ts b/ui/src/frontend/track_panel.ts
index 806b49c..e0aca05 100644
--- a/ui/src/frontend/track_panel.ts
+++ b/ui/src/frontend/track_panel.ts
@@ -199,7 +199,7 @@
                 onclick: (e: MouseEvent) => {
                   globals.dispatch(
                     Actions.toggleTrackSelection({
-                      id: attrs.trackKey,
+                      key: attrs.trackKey,
                       isTrackGroup: false,
                     }),
                   );
@@ -423,10 +423,6 @@
 
   constructor(private readonly attrs: TrackPanelAttrs) {}
 
-  get key(): string {
-    return this.attrs.trackKey;
-  }
-
   get trackKey(): string {
     return this.attrs.trackKey;
   }
diff --git a/ui/src/core_plugins/custom_sql_table_slices/index.ts b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
similarity index 82%
rename from ui/src/core_plugins/custom_sql_table_slices/index.ts
rename to ui/src/frontend/tracks/custom_sql_table_slice_track.ts
index 72f2075..cd31610 100644
--- a/ui/src/core_plugins/custom_sql_table_slices/index.ts
+++ b/ui/src/frontend/tracks/custom_sql_table_slice_track.ts
@@ -18,15 +18,11 @@
 import {Actions} from '../../common/actions';
 import {generateSqlWithInternalLayout} from '../../common/internal_layout_utils';
 import {LegacySelection} from '../../common/state';
-import {OnSliceClickArgs} from '../../frontend/base_slice_track';
-import {GenericSliceDetailsTabConfigBase} from '../../frontend/generic_slice_details_tab';
-import {globals} from '../../frontend/globals';
-import {
-  NamedSliceTrack,
-  NamedSliceTrackTypes,
-} from '../../frontend/named_slice_track';
-import {NewTrackArgs} from '../../frontend/track';
-import {Plugin, PluginDescriptor} from '../../public';
+import {OnSliceClickArgs} from '../base_slice_track';
+import {GenericSliceDetailsTabConfigBase} from '../generic_slice_details_tab';
+import {globals} from '../globals';
+import {NamedSliceTrack, NamedSliceTrackTypes} from '../named_slice_track';
+import {NewTrackArgs} from '../track';
 
 export interface CustomSqlImportConfig {
   modules: string[];
@@ -94,10 +90,8 @@
       });
     await this.engine.query(sql);
     return DisposableCallback.from(() => {
-      if (this.engine.isAlive) {
-        this.engine.query(`DROP VIEW ${this.tableName}`);
-        config.dispose?.dispose();
-      }
+      this.engine.tryQuery(`DROP VIEW ${this.tableName}`);
+      config.dispose?.dispose();
     });
   }
 
@@ -139,10 +133,3 @@
     }
   }
 }
-
-class CustomSqlTrackPlugin implements Plugin {}
-
-export const plugin: PluginDescriptor = {
-  pluginId: 'perfetto.CustomSqlTrack',
-  plugin: CustomSqlTrackPlugin,
-};
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index b67f2e5..429c391 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -82,11 +82,11 @@
   // Used to prevent global deselection if a pan/drag select occurred.
   private keepCurrentSelection = false;
 
-  private overviewTimelinePanel = new OverviewTimelinePanel('overview');
-  private timeAxisPanel = new TimeAxisPanel('timeaxis');
-  private timeSelectionPanel = new TimeSelectionPanel('timeselection');
-  private notesPanel = new NotesPanel('notes');
-  private tickmarkPanel = new TickmarkPanel('searchTickmarks');
+  private overviewTimelinePanel = new OverviewTimelinePanel();
+  private timeAxisPanel = new TimeAxisPanel();
+  private timeSelectionPanel = new TimeSelectionPanel();
+  private notesPanel = new NotesPanel();
+  private tickmarkPanel = new TickmarkPanel();
 
   private readonly PAN_ZOOM_CONTENT_REF = 'pan-and-zoom-content';
 
@@ -128,7 +128,7 @@
         currentY: number,
         editing: boolean,
       ) => {
-        const traceTime = globals.traceTime;
+        const traceTime = globals.traceContext;
         const {visibleTimeScale} = timeline;
         this.keepCurrentSelection = true;
         if (editing) {
@@ -231,8 +231,7 @@
       if (key) {
         const trackBundle = this.resolveTrack(key);
         headerPanel = new TrackGroupPanel({
-          trackGroupId: group.id,
-          key: `trackgroup-${group.id}`,
+          groupKey: group.key,
           trackFSM: trackBundle.trackFSM,
           labels: trackBundle.labels,
           tags: trackBundle.tags,
@@ -241,8 +240,7 @@
         });
       } else {
         headerPanel = new TrackGroupPanel({
-          trackGroupId: group.id,
-          key: `trackgroup-${group.id}`,
+          groupKey: group.key,
           collapsed: group.collapsed,
           title: group.name,
         });
@@ -268,7 +266,6 @@
         collapsed: group.collapsed,
         childPanels: childTracks,
         header: headerPanel,
-        trackGroupId: group.id,
       });
     }
 
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index aaef484..b4f20ca 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -12,7 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {runQuery} from '../../common/queries';
 import {addDebugSliceTrack} from '../../public';
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 
@@ -170,7 +169,7 @@
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
       name: 'Add track: Android jank CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() => {
+        ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => {
           addDebugSliceTrack(
             ctx.engine,
             {
@@ -189,9 +188,9 @@
       id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
       name: 'Run query: Android jank CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
-          ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'),
-        );
+        ctx.engine
+          .query(JANK_CUJ_QUERY_PRECONDITIONS)
+          .then(() => ctx.tabs.openQuery(JANK_CUJ_QUERY, 'Android Jank CUJs'));
       },
     });
 
@@ -223,7 +222,7 @@
       id: 'dev.perfetto.AndroidCujs#PinBlockingCalls',
       name: 'Add track: Android Blocking calls during CUJs',
       callback: () => {
-        runQuery(JANK_CUJ_QUERY_PRECONDITIONS, ctx.engine).then(() =>
+        ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() =>
           addDebugSliceTrack(
             ctx.engine,
             {
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 8f360b4..0666986 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -490,13 +490,25 @@
 const HIGH_CPU = `
   drop table if exists high_cpu;
   create table high_cpu as
-  with base as (
+  with cpu_cycles_args as (
     select
-      ts,
-      EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.uid') as uid,
-      EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.cluster') as cluster,
-      sum(EXTRACT_ARG(arg_set_id, 'cpu_cycles_per_uid_cluster.time_millis')) as time_millis
-    from track t join slice s on t.id = s.track_id
+      arg_set_id,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.uid', int_value, null)) as uid,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.cluster', int_value, null)) as cluster,
+      min(iif(key = 'cpu_cycles_per_uid_cluster.time_millis', int_value, null)) as time_millis
+    from args
+    where key in (
+      'cpu_cycles_per_uid_cluster.uid',
+      'cpu_cycles_per_uid_cluster.cluster',
+      'cpu_cycles_per_uid_cluster.time_millis'
+    )
+    group by 1
+  ),
+  base as (
+    select ts, uid, cluster, sum(time_millis) as time_millis
+    from track t
+    join slice s on t.id = s.track_id
+    join cpu_cycles_args using (arg_set_id)
     where t.name = 'Statsd Atoms'
       and s.name = 'cpu_cycles_per_uid_cluster'
     group by 1, 2, 3
@@ -521,8 +533,7 @@
   with_ratio as (
     select
       ts,
-      100.0 * cpu_dur / dur as value,
-      dur,
+      iif(dur is null, 0, 100.0 * cpu_dur / dur) as value,
       case cluster when 0 then 'little' when 1 then 'mid' when 2 then 'big' else 'cl-' || cluster end as cluster,
       case
           when uid = 0 then 'AID_ROOT'
@@ -533,17 +544,9 @@
           else pl.package_name
       end as pkg
     from with_windows left join app_package_list pl using(uid)
-    where cpu_dur is not null
-  ),
-  with_zeros as (
-      select ts, value, cluster, pkg
-      from with_ratio
-      union all
-      select ts + dur as ts, 0 as value, cluster, pkg
-      from with_ratio
   )
   select ts, sum(value) as value, cluster, pkg
-  from with_zeros
+  from with_ratio
   group by 1, 3, 4`;
 
 const WAKEUPS = `
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 2ec9223..b8feb57 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -14,7 +14,6 @@
 
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {addDebugSliceTrack} from '../../public';
-import {runQuery} from '../../common/queries';
 
 const PERF_TRACE_COUNTERS_PRECONDITION = `
   SELECT
@@ -27,8 +26,8 @@
 
 class AndroidPerfTraceCounters implements Plugin {
   async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
-    const resp = await runQuery(PERF_TRACE_COUNTERS_PRECONDITION, ctx.engine);
-    if (resp.totalRowCount === 0) return;
+    const resp = await ctx.engine.query(PERF_TRACE_COUNTERS_PRECONDITION);
+    if (resp.numRows() === 0) return;
     ctx.registerCommand({
       id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
       name: 'Add a track to show a thread runtime ipc',
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index eca39b4..9f627b8 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -20,7 +20,7 @@
   STR_NULL,
 } from '../../public';
 import {ASYNC_SLICE_TRACK_KIND} from '../../core_plugins/async_slices';
-import {AsyncSliceTrackV2} from '../../core_plugins/async_slices/async_slice_track_v2';
+import {AsyncSliceTrack} from '../../core_plugins/async_slices/async_slice_track';
 
 // This plugin renders visualizations of runtime power state transitions for
 // Linux kernel devices (devices managed by Linux drivers).
@@ -50,7 +50,7 @@
         trackIds: [trackId],
         kind: ASYNC_SLICE_TRACK_KIND,
         trackFactory: ({trackKey}) => {
-          return new AsyncSliceTrackV2(
+          return new AsyncSliceTrack(
             {
               engine: ctx.engine,
               trackKey,
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index 7fb2e44..9e540fd 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -22,6 +22,8 @@
 import {PanelSize} from '../frontend/panel';
 import {Engine} from '../trace_processor/engine';
 import {UntypedEventSet} from '../core/event_set';
+import {TraceContext} from '../frontend/globals';
+import {PromptOption} from '../frontend/omnibox_manager';
 
 export {Engine} from '../trace_processor/engine';
 export {
@@ -34,6 +36,7 @@
 } from '../trace_processor/query_result';
 export {BottomTabToSCSAdapter} from './utils';
 export {createStore, Migrate, Store} from '../base/store';
+export {PromptOption} from '../frontend/omnibox_manager';
 
 // This is a temporary fix until this is available in the plugin API.
 export {
@@ -433,10 +436,9 @@
   // Create a store mounted over the top of this plugin's persistent state.
   mountStore<T>(migrate: Migrate<T>): Store<T>;
 
-  trace: {
-    // A span representing the start and end time of the trace
-    readonly span: Span<time, duration>;
-  };
+  trace: TraceContext;
+
+  prompt(text: string, options?: PromptOption[]): Promise<string>;
 }
 
 export interface Plugin {
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index 90901c5..d0f70af 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -14,7 +14,6 @@
 
 import {defer, Deferred} from '../base/deferred';
 import {assertExists, assertTrue} from '../base/logging';
-import {duration, Span, Time, time, TimeSpan} from '../base/time';
 import {
   ComputeMetricArgs,
   ComputeMetricResult,
@@ -31,17 +30,14 @@
 import {ProtoRingBuffer} from './proto_ring_buffer';
 import {
   createQueryResult,
-  LONG,
-  LONG_NULL,
-  NUM,
   QueryError,
   QueryResult,
-  STR,
   WritableQueryResult,
 } from './query_result';
 
 import TPM = TraceProcessorRpc.TraceProcessorMethod;
 import {Disposable} from '../base/disposable';
+import {Result} from '../base/utils';
 
 export interface LoadingTracker {
   beginLoading(): void;
@@ -67,16 +63,43 @@
 }
 
 export interface Engine {
-  execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult;
-  query(sqlQuery: string, tag?: string): Promise<QueryResult>;
-  getCpus(): Promise<number[]>;
-  getNumberOfGpus(): Promise<number>;
-  getTracingMetadataTimeBounds(): Promise<Span<time, duration>>;
+  /**
+   * Execute a query against the database, returning a promise that resolves
+   * when the query has completed but rejected when the query fails for whatever
+   * reason. On success, the promise will only resolve once all the resulting
+   * rows have been received.
+   *
+   * The promise will be rejected if the query fails.
+   *
+   * @param sql The query to execute.
+   * @param tag An optional tag used to trace the origin of the query.
+   */
+  query(sql: string, tag?: string): Promise<QueryResult>;
+
+  /**
+   * Execute a query against the database, returning a promise that resolves
+   * when the query has completed or failed. The promise will never get
+   * rejected, it will always successfully resolve. Use the returned wrapper
+   * object to determine whether the query completed successfully.
+   *
+   * The promise will only resolve once all the resulting rows have been
+   * received.
+   *
+   * @param sql The query to execute.
+   * @param tag An optional tag used to trace the origin of the query.
+   */
+  tryQuery(sql: string, tag?: string): Promise<Result<QueryResult, Error>>;
+
+  /**
+   * Execute one or more metric and get the result.
+   *
+   * @param metrics The metrics to run.
+   * @param format The format of the response.
+   */
   computeMetric(
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array>;
-  readonly isAlive: boolean;
 }
 
 // Abstract interface of a trace proccessor.
@@ -92,8 +115,6 @@
 // 2. Call onRpcResponseBytes() when response data is received.
 export abstract class EngineBase implements Engine {
   abstract readonly id: string;
-  private _cpus?: number[];
-  private _numGpus?: number;
   private loadingTracker: LoadingTracker;
   private txSeqId = 0;
   private rxSeqId = 0;
@@ -106,7 +127,6 @@
   private pendingComputeMetrics = new Array<Deferred<string | Uint8Array>>();
   private pendingReadMetatrace?: Deferred<DisableAndReadMetatraceResult>;
   private _isMetatracingEnabled = false;
-  readonly isAlive = false;
 
   constructor(tracker?: LoadingTracker) {
     this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
@@ -360,7 +380,10 @@
   //
   // Optional |tag| (usually a component name) can be provided to allow
   // attributing trace processor workload to different UI components.
-  execute(sqlQuery: string, tag?: string): Promise<QueryResult> & QueryResult {
+  private streamingQuery(
+    sqlQuery: string,
+    tag?: string,
+  ): Promise<QueryResult> & QueryResult {
     const rpc = TraceProcessorRpc.create();
     rpc.request = TPM.TPM_QUERY_STREAMING;
     rpc.queryArgs = new QueryArgs();
@@ -376,13 +399,13 @@
     return result;
   }
 
-  // Wraps .execute(), captures errors and re-throws with current stack.
+  // Wraps .streamingQuery(), captures errors and re-throws with current stack.
   //
-  // Note: This function is less flexible that .execute() as it only returns a
+  // Note: This function is less flexible than .execute() as it only returns a
   // promise which must be unwrapped before the QueryResult may be accessed.
   async query(sqlQuery: string, tag?: string): Promise<QueryResult> {
     try {
-      return await this.execute(sqlQuery, tag);
+      return await this.streamingQuery(sqlQuery, tag);
     } catch (e) {
       // Replace the error's stack trace with the one from here
       // Note: It seems only V8 can trace the stack up the promise chain, so its
@@ -394,6 +417,19 @@
     }
   }
 
+  async tryQuery(
+    sql: string,
+    tag?: string,
+  ): Promise<Result<QueryResult, Error>> {
+    try {
+      const result = await this.query(sql, tag);
+      return {success: true, result};
+    } catch (error: unknown) {
+      // We know we only throw Error type objects so we can type assert safely
+      return {success: false, error: error as Error};
+    }
+  }
+
   isMetatracingEnabled(): boolean {
     return this._isMetatracingEnabled;
   }
@@ -438,79 +474,6 @@
     this.rpcSendRequestBytes(buf);
   }
 
-  // TODO(hjd): When streaming must invalidate this somehow.
-  async getCpus(): Promise<number[]> {
-    if (!this._cpus) {
-      const cpus = [];
-      const queryRes = await this.query(
-        'select distinct(cpu) as cpu from sched order by cpu;',
-      );
-      for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
-        cpus.push(it.cpu);
-      }
-      this._cpus = cpus;
-    }
-    return this._cpus;
-  }
-
-  async getNumberOfGpus(): Promise<number> {
-    // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
-    if (!this._numGpus) {
-      const result = await this.query(`
-        select count(distinct(gpu_id)) as gpuCount
-        from gpu_counter_track
-        where name = 'gpufreq';
-      `);
-      this._numGpus = result.firstRow({gpuCount: NUM}).gpuCount;
-    }
-    return this._numGpus;
-  }
-
-  // TODO: This should live in code that's more specific to chrome, instead of
-  // in engine.
-  async getNumberOfProcesses(): Promise<number> {
-    const result = await this.query('select count(*) as cnt from process;');
-    return result.firstRow({cnt: NUM}).cnt;
-  }
-
-  async getTraceTimeBounds(): Promise<Span<time, duration>> {
-    const result = await this.query(
-      `select start_ts as startTs, end_ts as endTs from trace_bounds`,
-    );
-    const bounds = result.firstRow({
-      startTs: LONG,
-      endTs: LONG,
-    });
-    return new TimeSpan(
-      Time.fromRaw(bounds.startTs),
-      Time.fromRaw(bounds.endTs),
-    );
-  }
-
-  async getTracingMetadataTimeBounds(): Promise<Span<time, duration>> {
-    const queryRes = await this.query(`select
-         name,
-         int_value as intValue
-         from metadata
-         where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
-         or name = 'all_data_source_started_ns'`);
-    let startBound = Time.MIN;
-    let endBound = Time.MAX;
-    const it = queryRes.iter({name: STR, intValue: LONG_NULL});
-    for (; it.valid(); it.next()) {
-      const columnName = it.name;
-      const timestamp = it.intValue;
-      if (timestamp === null) continue;
-      if (columnName === 'tracing_disabled_ns') {
-        endBound = Time.min(endBound, Time.fromRaw(timestamp));
-      } else {
-        startBound = Time.max(startBound, Time.fromRaw(timestamp));
-      }
-    }
-
-    return new TimeSpan(startBound, endBound);
-  }
-
   getProxy(tag: string): EngineProxy {
     return new EngineProxy(this, tag);
   }
@@ -522,58 +485,42 @@
   private tag: string;
   private _isAlive: boolean;
 
-  get isAlive(): boolean {
-    return this._isAlive;
-  }
-
   constructor(engine: EngineBase, tag: string) {
     this.engine = engine;
     this.tag = tag;
     this._isAlive = true;
   }
 
-  execute(query: string, tag?: string): Promise<QueryResult> & QueryResult {
-    if (!this.isAlive) {
-      throw new Error(`EngineProxy ${this.tag} was disposed.`);
-    }
-    return this.engine.execute(query, tag || this.tag);
-  }
-
   async query(query: string, tag?: string): Promise<QueryResult> {
-    if (!this.isAlive) {
+    if (!this._isAlive) {
       throw new Error(`EngineProxy ${this.tag} was disposed.`);
     }
     return await this.engine.query(query, tag);
   }
 
+  async tryQuery(
+    query: string,
+    tag?: string,
+  ): Promise<Result<QueryResult, Error>> {
+    if (!this._isAlive) {
+      return {
+        success: false,
+        error: new Error(`EngineProxy ${this.tag} was disposed.`),
+      };
+    }
+    return await this.engine.tryQuery(query, tag);
+  }
+
   async computeMetric(
     metrics: string[],
     format: 'json' | 'prototext' | 'proto',
   ): Promise<string | Uint8Array> {
-    if (!this.isAlive) {
+    if (!this._isAlive) {
       return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
     }
     return this.engine.computeMetric(metrics, format);
   }
 
-  async getCpus(): Promise<number[]> {
-    if (!this.isAlive) {
-      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
-    }
-    return this.engine.getCpus();
-  }
-
-  async getNumberOfGpus(): Promise<number> {
-    if (!this.isAlive) {
-      return Promise.reject(new Error(`EngineProxy ${this.tag} was disposed.`));
-    }
-    return this.engine.getNumberOfGpus();
-  }
-
-  async getTracingMetadataTimeBounds(): Promise<Span<time, bigint>> {
-    return this.engine.getTracingMetadataTimeBounds();
-  }
-
   get engineId(): string {
     return this.engine.id;
   }
diff --git a/ui/src/trace_processor/query_result.ts b/ui/src/trace_processor/query_result.ts
index ac48079..a29f4dc 100644
--- a/ui/src/trace_processor/query_result.ts
+++ b/ui/src/trace_processor/query_result.ts
@@ -57,6 +57,7 @@
 import {utf8Decode} from '../base/string_utils';
 import {Duration, duration, Time, time} from '../base/time';
 
+export const UNKNOWN: ColumnType = null;
 export const NUM = 0;
 export const STR = 'str';
 export const NUM_NULL: number | null = 1;
@@ -203,6 +204,8 @@
       return 'LONG';
     case LONG_NULL:
       return 'LONG_NULL';
+    case UNKNOWN:
+      return 'UNKNOWN';
     default:
       return `INVALID(${t})`;
   }
@@ -215,21 +218,25 @@
         expected === NUM_NULL ||
         expected === STR_NULL ||
         expected === BLOB_NULL ||
-        expected === LONG_NULL
+        expected === LONG_NULL ||
+        expected === UNKNOWN
       );
     case CellType.CELL_VARINT:
       return (
         expected === NUM ||
         expected === NUM_NULL ||
         expected === LONG ||
-        expected === LONG_NULL
+        expected === LONG_NULL ||
+        expected === UNKNOWN
       );
     case CellType.CELL_FLOAT64:
-      return expected === NUM || expected === NUM_NULL;
+      return expected === NUM || expected === NUM_NULL || expected === UNKNOWN;
     case CellType.CELL_STRING:
-      return expected === STR || expected === STR_NULL;
+      return expected === STR || expected === STR_NULL || expected === UNKNOWN;
     case CellType.CELL_BLOB:
-      return expected === BLOB || expected === BLOB_NULL;
+      return (
+        expected === BLOB || expected === BLOB_NULL || expected === UNKNOWN
+      );
     default:
       throw new Error(`Unknown CellType ${actual}`);
   }
@@ -285,11 +292,10 @@
   // If true all rows have been fetched. Calling iter() will iterate through the
   // last row. If false, iter() will return an iterator which might iterate
   // through some rows (or none) but will surely not reach the end.
-
   isComplete(): boolean;
 
   // Returns a promise that is resolved only when all rows (i.e. all batches)
-  // have been fetched. The promise return value is always the object iself.
+  // have been fetched. The promise return value is always the object itself.
   waitAllRows(): Promise<QueryResult>;
 
   // Returns a promise that is resolved when either:
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 39be606..89185c3 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -75,31 +75,32 @@
     if (this.engine === undefined) {
       return '';
     }
-    const result = this.engine.execute(uri);
     try {
-      await result.waitAllRows();
+      const result = await this.engine.query(uri);
+      const columns = result.columns();
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      const rows: any[] = [];
+      for (const it = result.iter({}); it.valid(); it.next()) {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const row: any = {};
+        for (const name of columns) {
+          let value = it.get(name);
+          if (typeof value === 'bigint') {
+            value = Number(value);
+          }
+          row[name] = value;
+        }
+        rows.push(row);
+      }
+      return JSON.stringify(rows);
     } catch (e) {
       if (e instanceof QueryError) {
-        console.error(result.error());
+        console.error(e);
         return '';
+      } else {
+        throw e;
       }
     }
-    const columns = result.columns();
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const rows: any[] = [];
-    for (const it = result.iter({}); it.valid(); it.next()) {
-      // eslint-disable-next-line @typescript-eslint/no-explicit-any
-      const row: any = {};
-      for (const name of columns) {
-        let value = it.get(name);
-        if (typeof value === 'bigint') {
-          value = Number(value);
-        }
-        row[name] = value;
-      }
-      rows.push(row);
-    }
-    return JSON.stringify(rows);
   }
 
   // eslint-disable-next-line @typescript-eslint/no-explicit-any