Merge "CHANGELOG: TTID and TTFD in android_startup_metric" into main
diff --git a/Android.bp b/Android.bp
index b379ef7..14e7ab9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -11871,6 +11871,7 @@
     srcs: [
         "src/trace_processor/containers/bit_vector_unittest.cc",
         "src/trace_processor/containers/implicit_segment_forest_unittest.cc",
+        "src/trace_processor/containers/interval_tree_unittest.cc",
         "src/trace_processor/containers/null_term_string_view_unittest.cc",
         "src/trace_processor/containers/row_map_unittest.cc",
         "src/trace_processor/containers/string_pool_unittest.cc",
@@ -12393,6 +12394,7 @@
         "src/trace_processor/importers/proto/stack_profile_sequence_state.cc",
         "src/trace_processor/importers/proto/track_event_module.cc",
         "src/trace_processor/importers/proto/track_event_parser.cc",
+        "src/trace_processor/importers/proto/track_event_sequence_state.cc",
         "src/trace_processor/importers/proto/track_event_tokenizer.cc",
         "src/trace_processor/importers/proto/track_event_tracker.cc",
     ],
diff --git a/BUILD b/BUILD
index bf86ecc..3437d7c 100644
--- a/BUILD
+++ b/BUILD
@@ -1375,6 +1375,7 @@
         ":include_perfetto_public_protozero",
         "src/trace_processor/containers/bit_vector.h",
         "src/trace_processor/containers/implicit_segment_forest.h",
+        "src/trace_processor/containers/interval_tree.h",
         "src/trace_processor/containers/null_term_string_view.h",
         "src/trace_processor/containers/row_map.h",
         "src/trace_processor/containers/row_map_algorithms.h",
@@ -1896,7 +1897,7 @@
         "src/trace_processor/importers/proto/network_trace_module.h",
         "src/trace_processor/importers/proto/packet_analyzer.cc",
         "src/trace_processor/importers/proto/packet_analyzer.h",
-        "src/trace_processor/importers/proto/packet_sequence_state.h",
+        "src/trace_processor/importers/proto/packet_sequence_state_builder.h",
         "src/trace_processor/importers/proto/packet_sequence_state_generation.cc",
         "src/trace_processor/importers/proto/perf_sample_tracker.cc",
         "src/trace_processor/importers/proto/perf_sample_tracker.h",
@@ -1906,7 +1907,6 @@
         "src/trace_processor/importers/proto/profile_packet_sequence_state.h",
         "src/trace_processor/importers/proto/profile_packet_utils.cc",
         "src/trace_processor/importers/proto/profile_packet_utils.h",
-        "src/trace_processor/importers/proto/proto_incremental_state.h",
         "src/trace_processor/importers/proto/proto_trace_parser_impl.cc",
         "src/trace_processor/importers/proto/proto_trace_parser_impl.h",
         "src/trace_processor/importers/proto/proto_trace_reader.cc",
@@ -1919,6 +1919,7 @@
         "src/trace_processor/importers/proto/track_event_module.h",
         "src/trace_processor/importers/proto/track_event_parser.cc",
         "src/trace_processor/importers/proto/track_event_parser.h",
+        "src/trace_processor/importers/proto/track_event_sequence_state.cc",
         "src/trace_processor/importers/proto/track_event_tokenizer.cc",
         "src/trace_processor/importers/proto/track_event_tokenizer.h",
         "src/trace_processor/importers/proto/track_event_tracker.cc",
@@ -1931,6 +1932,7 @@
     name = "src_trace_processor_importers_proto_packet_sequence_state_generation_hdr",
     srcs = [
         "src/trace_processor/importers/proto/packet_sequence_state_generation.h",
+        "src/trace_processor/importers/proto/track_event_sequence_state.h",
     ],
 )
 
diff --git a/include/perfetto/public/data_source.h b/include/perfetto/public/data_source.h
index 23fba92..83fb527 100644
--- a/include/perfetto/public/data_source.h
+++ b/include/perfetto/public/data_source.h
@@ -75,6 +75,10 @@
 
   // How to behave when running out of shared memory buffer space.
   enum PerfettoDsBufferExhaustedPolicy buffer_exhausted_policy;
+
+  // When true the data source is expected to ack the stop request through the
+  // NotifyDataSourceStopped() IPC.
+  bool will_notify_on_stop;
 };
 
 static inline struct PerfettoDsParams PerfettoDsParamsDefault(void) {
@@ -88,7 +92,8 @@
                                  PERFETTO_NULL,
                                  PERFETTO_NULL,
                                  PERFETTO_NULL,
-                                 PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP};
+                                 PERFETTO_DS_BUFFER_EXHAUSTED_POLICY_DROP,
+                                 true};
   return ret;
 }
 
@@ -112,6 +117,7 @@
     PerfettoPbMsgInit(&desc.msg, &writer);
 
     perfetto_protos_DataSourceDescriptor_set_cstr_name(&desc, data_source_name);
+    perfetto_protos_DataSourceDescriptor_set_will_notify_on_stop(&desc, params.will_notify_on_stop);
 
     desc_size = PerfettoStreamWriterGetWrittenSize(&writer.writer);
     desc_buf = malloc(desc_size);
diff --git a/src/trace_processor/containers/BUILD.gn b/src/trace_processor/containers/BUILD.gn
index c724df8..7974003 100644
--- a/src/trace_processor/containers/BUILD.gn
+++ b/src/trace_processor/containers/BUILD.gn
@@ -23,6 +23,7 @@
   public = [
     "bit_vector.h",
     "implicit_segment_forest.h",
+    "interval_tree.h",
     "null_term_string_view.h",
     "row_map.h",
     "row_map_algorithms.h",
@@ -46,6 +47,7 @@
   sources = [
     "bit_vector_unittest.cc",
     "implicit_segment_forest_unittest.cc",
+    "interval_tree_unittest.cc",
     "null_term_string_view_unittest.cc",
     "row_map_unittest.cc",
     "string_pool_unittest.cc",
diff --git a/src/trace_processor/containers/interval_tree.h b/src/trace_processor/containers/interval_tree.h
new file mode 100644
index 0000000..c083da0
--- /dev/null
+++ b/src/trace_processor/containers/interval_tree.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_CONTAINERS_INTERVAL_TREE_H_
+#define SRC_TRACE_PROCESSOR_CONTAINERS_INTERVAL_TREE_H_
+
+#include <cstdint>
+#include <limits>
+#include <memory>
+#include <vector>
+
+namespace perfetto::trace_processor {
+
+// An implementation of an interval tree data structure, designed to efficiently
+// perform overlap queries on a set of intervals. Used by `interval_intersect`,
+// where one set of intervals (generally the bigger one) has interval tree
+// created based on it, as another queries `FindOverlaps` function for each
+// interval.
+// As interval tree is build on sorted (by `start`) set of N intervals, the
+// complexity of creating a tree goes down from O(N*logN) to O(N) and the
+// created tree is optimally balanced. Each call to `FindOverlaps` is O(logN).
+class IntervalTree {
+ public:
+  struct Interval {
+    uint32_t start;
+    uint32_t end;
+    uint32_t id;
+  };
+
+  // Takes vector of sorted intervals.
+  explicit IntervalTree(std::vector<Interval>& sorted_intervals) {
+    tree_root_ = BuildFromSortedIntervals(
+        sorted_intervals, 0, static_cast<int32_t>(sorted_intervals.size() - 1));
+  }
+
+  // Modifies |overlaps| to contain ids of all intervals in the interval tree
+  // that overlap with |interval|.
+  void FindOverlaps(Interval interval, std::vector<uint32_t>& overlaps) const {
+    if (tree_root_) {
+      FindOverlaps(*tree_root_, interval, overlaps);
+    }
+  }
+
+ private:
+  struct Node {
+    Interval interval;
+    uint32_t max;
+    std::unique_ptr<Node> left;
+    std::unique_ptr<Node> right;
+
+    explicit Node(Interval i) : interval(i), max(i.end) {}
+  };
+
+  static std::unique_ptr<Node> Insert(std::unique_ptr<Node> root, Interval i) {
+    if (root == nullptr) {
+      return std::make_unique<Node>(i);
+    }
+
+    if (i.start < root->interval.start) {
+      root->left = Insert(std::move(root->left), i);
+    } else {
+      root->right = Insert(std::move(root->right), i);
+    }
+
+    if (root->max < i.end) {
+      root->max = i.end;
+    }
+
+    return root;
+  }
+
+  static std::unique_ptr<Node> BuildFromSortedIntervals(
+      const std::vector<Interval>& is,
+      int32_t start,
+      int32_t end) {
+    // |start == end| happens if there is one element so we need to check for
+    // |start > end| that happens in the next recursive call.
+    if (start > end) {
+      return nullptr;
+    }
+
+    int32_t mid = start + (end - start) / 2;
+    auto node = std::make_unique<Node>(is[static_cast<uint32_t>(mid)]);
+
+    node->left = BuildFromSortedIntervals(is, start, mid - 1);
+    node->right = BuildFromSortedIntervals(is, mid + 1, end);
+
+    uint32_t max_from_children = std::max(
+        node->left ? node->left->max : std::numeric_limits<uint32_t>::min(),
+        node->right ? node->right->max : std::numeric_limits<uint32_t>::min());
+
+    node->max = std::max(node->interval.end, max_from_children);
+
+    return node;
+  }
+
+  static void FindOverlaps(const Node& node,
+                           const Interval& i,
+                           std::vector<uint32_t>& overlaps) {
+    // Intervals overlap if one starts before the other ends and ends after it
+    // starts.
+    if (node.interval.start < i.end && node.interval.end > i.start) {
+      overlaps.push_back(node.interval.id);
+    }
+
+    // Try to find overlaps with left.
+    if (i.start <= node.interval.start && node.left) {
+      FindOverlaps(*node.left, i, overlaps);
+    }
+
+    // Try to find overlaps with right.
+    if (i.start < node.max && node.right) {
+      FindOverlaps(*node.right, i, overlaps);
+    }
+  }
+
+  std::unique_ptr<Node> tree_root_;
+};
+
+}  // namespace perfetto::trace_processor
+
+#endif  // SRC_TRACE_PROCESSOR_CONTAINERS_INTERVAL_TREE_H_
diff --git a/src/trace_processor/containers/interval_tree_unittest.cc b/src/trace_processor/containers/interval_tree_unittest.cc
new file mode 100644
index 0000000..61023d4
--- /dev/null
+++ b/src/trace_processor/containers/interval_tree_unittest.cc
@@ -0,0 +1,165 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/containers/interval_tree.h"
+
+#include <cstddef>
+#include <cstdint>
+#include <numeric>
+#include <random>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include "perfetto/base/compiler.h"
+#include "test/gtest_and_gmock.h"
+
+namespace perfetto::trace_processor {
+
+inline bool operator==(const IntervalTree::Interval& a,
+                       const IntervalTree::Interval& b) {
+  return std::tie(a.start, a.end, a.id) == std::tie(b.start, b.end, b.id);
+}
+
+namespace {
+
+using Interval = IntervalTree::Interval;
+using testing::IsEmpty;
+using testing::UnorderedElementsAre;
+
+std::vector<Interval> CreateIntervals(
+    std::vector<std::pair<uint32_t, uint32_t>> periods) {
+  std::vector<Interval> res;
+  uint32_t id = 0;
+  for (auto period : periods) {
+    res.push_back({period.first, period.second, id++});
+  }
+  return res;
+}
+
+TEST(IntervalTree, Trivial) {
+  std::vector<Interval> interval({{10, 20, 5}});
+  IntervalTree tree(interval);
+  std::vector<uint32_t> overlaps;
+  tree.FindOverlaps({5, 30, 0}, overlaps);
+
+  ASSERT_THAT(overlaps, UnorderedElementsAre(5));
+}
+
+TEST(IntervalTree, Simple) {
+  auto intervals = CreateIntervals({{0, 10}, {5, 20}, {30, 40}});
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+  tree.FindOverlaps({4, 30, 0}, overlaps);
+
+  ASSERT_THAT(overlaps, UnorderedElementsAre(0, 1));
+}
+
+TEST(IntervalTree, SinglePointOverlap) {
+  auto intervals = CreateIntervals({{10, 20}});
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+
+  // Overlaps at the start point only
+  tree.FindOverlaps({10, 10, 0}, overlaps);
+  ASSERT_THAT(overlaps, IsEmpty());
+
+  overlaps.clear();
+
+  // Overlaps at the end point only
+  tree.FindOverlaps({20, 20, 0}, overlaps);
+  ASSERT_THAT(overlaps, IsEmpty());
+}
+
+TEST(IntervalTree, NoOverlaps) {
+  auto intervals = CreateIntervals({{10, 20}, {30, 40}});
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+
+  // Before all intervals
+  tree.FindOverlaps({5, 9, 0}, overlaps);
+  ASSERT_THAT(overlaps, IsEmpty());
+  overlaps.clear();
+
+  // Between intervals
+  tree.FindOverlaps({21, 29, 0}, overlaps);
+  ASSERT_THAT(overlaps, IsEmpty());
+  overlaps.clear();
+
+  // After all intervals
+  tree.FindOverlaps({41, 50, 0}, overlaps);
+  ASSERT_THAT(overlaps, IsEmpty());
+}
+
+TEST(IntervalTree, IdenticalIntervals) {
+  auto intervals = CreateIntervals({{10, 20}, {10, 20}});
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+  tree.FindOverlaps({10, 20, 0}, overlaps);
+  ASSERT_THAT(overlaps, UnorderedElementsAre(0, 1));
+}
+
+TEST(IntervalTree, MultipleOverlapsVariousPositions) {
+  auto intervals = CreateIntervals({{5, 15}, {10, 20}, {12, 22}, {25, 35}});
+  IntervalTree tree(intervals);
+
+  std::vector<uint32_t> overlaps;
+  /// Starts before, ends within
+  tree.FindOverlaps({9, 11, 0}, overlaps);
+  ASSERT_THAT(overlaps, UnorderedElementsAre(0, 1));
+
+  overlaps.clear();
+  // Starts within, ends within
+  tree.FindOverlaps({13, 21, 0}, overlaps);
+  ASSERT_THAT(overlaps, UnorderedElementsAre(1, 2));
+
+  overlaps.clear();
+  // Starts within, ends after
+  tree.FindOverlaps({18, 26, 0}, overlaps);
+  ASSERT_THAT(overlaps, UnorderedElementsAre(1, 2, 3));
+}
+
+TEST(IntervalTree, OverlappingEndpoints) {
+  auto intervals = CreateIntervals({{10, 20}, {20, 30}});
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+
+  tree.FindOverlaps({19, 21, 0}, overlaps);
+  ASSERT_THAT(overlaps, UnorderedElementsAre(0, 1));
+}
+
+TEST(IntervalTree, Stress) {
+  static constexpr size_t kCount = 9249;
+  std::minstd_rand0 rng(42);
+
+  std::vector<std::pair<uint32_t, uint32_t>> periods;
+  uint32_t prev_max = 0;
+  for (uint32_t i = 0; i < kCount; ++i) {
+    prev_max += static_cast<uint32_t>(rng()) % 100;
+    periods.push_back(
+        {prev_max, prev_max + (static_cast<uint32_t>(rng()) % 100)});
+  }
+  auto intervals = CreateIntervals(periods);
+  Interval query_i{periods.front().first, periods.back().first + 1, 5};
+  IntervalTree tree(intervals);
+  std::vector<uint32_t> overlaps;
+  tree.FindOverlaps(query_i, overlaps);
+
+  EXPECT_EQ(overlaps.size(), kCount);
+}
+
+}  // namespace
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/BUILD.gn b/src/trace_processor/importers/proto/BUILD.gn
index 18a617a..dde995c 100644
--- a/src/trace_processor/importers/proto/BUILD.gn
+++ b/src/trace_processor/importers/proto/BUILD.gn
@@ -40,7 +40,7 @@
     "network_trace_module.h",
     "packet_analyzer.cc",
     "packet_analyzer.h",
-    "packet_sequence_state.h",
+    "packet_sequence_state_builder.h",
     "packet_sequence_state_generation.cc",
     "perf_sample_tracker.cc",
     "perf_sample_tracker.h",
@@ -50,7 +50,6 @@
     "profile_packet_sequence_state.h",
     "profile_packet_utils.cc",
     "profile_packet_utils.h",
-    "proto_incremental_state.h",
     "proto_trace_parser_impl.cc",
     "proto_trace_parser_impl.h",
     "proto_trace_reader.cc",
@@ -63,6 +62,7 @@
     "track_event_module.h",
     "track_event_parser.cc",
     "track_event_parser.h",
+    "track_event_sequence_state.cc",
     "track_event_tokenizer.cc",
     "track_event_tokenizer.h",
     "track_event_tracker.cc",
@@ -70,7 +70,6 @@
   ]
   public_deps = [ ":proto_importer_module" ]
   deps = [
-    ":packet_sequence_state_generation_hdr",
     "../../../../gn:default_deps",
     "../../../../include/perfetto/trace_processor:trace_processor",
     "../../../../protos/perfetto/common:zero",
@@ -201,6 +200,7 @@
     "proto_importer_module.cc",
     "proto_importer_module.h",
   ]
+  public_deps = [ ":packet_sequence_state_generation_hdr" ]
   deps = [
     ":packet_sequence_state_generation_hdr",
     "../../../../gn:default_deps",
@@ -212,12 +212,16 @@
 }
 
 source_set("packet_sequence_state_generation_hdr") {
-  sources = [ "packet_sequence_state_generation.h" ]
+  sources = [
+    "packet_sequence_state_generation.h",
+    "track_event_sequence_state.h",
+  ]
   deps = [
     "../../../../gn:default_deps",
     "../../../../include/perfetto/ext/base",
     "../../../../protos/perfetto/trace:non_minimal_zero",
     "../../../../protos/perfetto/trace/track_event:zero",
+    "../../types:types",
     "../../util:interned_message_view",
   ]
 }
@@ -262,6 +266,7 @@
   deps = [
     ":full",
     ":minimal",
+    ":packet_sequence_state_generation_hdr",
     "../../../../gn:default_deps",
     "../../../../gn:gtest_and_gmock",
     "../../../../protos/perfetto/common:cpp",
diff --git a/src/trace_processor/importers/proto/frame_timeline_event_parser.h b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
index 596f415..b1c1e12 100644
--- a/src/trace_processor/importers/proto/frame_timeline_event_parser.h
+++ b/src/trace_processor/importers/proto/frame_timeline_event_parser.h
@@ -20,7 +20,6 @@
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
 #include "src/trace_processor/importers/common/async_track_set_tracker.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
 #include "protos/perfetto/trace/android/frame_timeline_event.pbzero.h"
diff --git a/src/trace_processor/importers/proto/gpu_event_parser.h b/src/trace_processor/importers/proto/gpu_event_parser.h
index e2eaac8..6ca6527 100644
--- a/src/trace_processor/importers/proto/gpu_event_parser.h
+++ b/src/trace_processor/importers/proto/gpu_event_parser.h
@@ -25,7 +25,6 @@
 #include "protos/perfetto/trace/android/gpu_mem_event.pbzero.h"
 #include "protos/perfetto/trace/gpu/gpu_render_stage_event.pbzero.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/importers/proto/vulkan_memory_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
diff --git a/src/trace_processor/importers/proto/graphics_frame_event_parser.h b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
index 2d37612..146d126 100644
--- a/src/trace_processor/importers/proto/graphics_frame_event_parser.h
+++ b/src/trace_processor/importers/proto/graphics_frame_event_parser.h
@@ -23,7 +23,6 @@
 #include "perfetto/ext/base/string_writer.h"
 #include "perfetto/protozero/field.h"
 #include "src/trace_processor/importers/common/args_tracker.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/importers/proto/vulkan_memory_tracker.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
diff --git a/src/trace_processor/importers/proto/packet_sequence_state.h b/src/trace_processor/importers/proto/packet_sequence_state.h
deleted file mode 100644
index b50af20..0000000
--- a/src/trace_processor/importers/proto/packet_sequence_state.h
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_H_
-
-#include <stdint.h>
-
-#include <memory>
-#include <type_traits>
-#include <vector>
-
-#include "perfetto/base/compiler.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
-#include "src/trace_processor/types/trace_processor_context.h"
-#include "src/trace_processor/util/interned_message_view.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-class PacketSequenceState {
- public:
-  explicit PacketSequenceState(TraceProcessorContext* context)
-      : context_(context) {
-    current_generation_.reset(new PacketSequenceStateGeneration(this));
-  }
-
-  int64_t IncrementAndGetTrackEventTimeNs(int64_t delta_ns) {
-    PERFETTO_DCHECK(track_event_timestamps_valid());
-    track_event_timestamp_ns_ += delta_ns;
-    return track_event_timestamp_ns_;
-  }
-
-  int64_t IncrementAndGetTrackEventThreadTimeNs(int64_t delta_ns) {
-    PERFETTO_DCHECK(track_event_timestamps_valid());
-    track_event_thread_timestamp_ns_ += delta_ns;
-    return track_event_thread_timestamp_ns_;
-  }
-
-  int64_t IncrementAndGetTrackEventThreadInstructionCount(int64_t delta) {
-    PERFETTO_DCHECK(track_event_timestamps_valid());
-    track_event_thread_instruction_count_ += delta;
-    return track_event_thread_instruction_count_;
-  }
-
-  // Intern a message into the current generation.
-  void InternMessage(uint32_t field_id, TraceBlobView message) {
-    current_generation_->InternMessage(field_id, std::move(message));
-  }
-
-  // Set the trace packet defaults for the current generation. If the current
-  // generation already has defaults set, starts a new generation without
-  // invalidating other incremental state (such as interned data).
-  void UpdateTracePacketDefaults(TraceBlobView defaults) {
-    if (!current_generation_->GetTracePacketDefaultsView()) {
-      current_generation_->SetTracePacketDefaults(std::move(defaults));
-      return;
-    }
-
-    // The new defaults should only apply to subsequent messages on the
-    // sequence. Add a new generation with the updated defaults but the
-    // current generation's interned data state.
-    current_generation_.reset(new PacketSequenceStateGeneration(
-        this, current_generation_.get(), std::move(defaults)));
-  }
-
-  void SetThreadDescriptor(int32_t pid,
-                           int32_t tid,
-                           int64_t timestamp_ns,
-                           int64_t thread_timestamp_ns,
-                           int64_t thread_instruction_count) {
-    track_event_timestamps_valid_ = true;
-    pid_and_tid_valid_ = true;
-    pid_ = pid;
-    tid_ = tid;
-    track_event_timestamp_ns_ = timestamp_ns;
-    track_event_thread_timestamp_ns_ = thread_timestamp_ns;
-    track_event_thread_instruction_count_ = thread_instruction_count;
-  }
-
-  void OnPacketLoss() {
-    packet_loss_ = true;
-    track_event_timestamps_valid_ = false;
-  }
-
-  // Starts a new generation with clean-slate incremental state and defaults.
-  void OnIncrementalStateCleared() {
-    packet_loss_ = false;
-    current_generation_.reset(new PacketSequenceStateGeneration(this));
-  }
-
-  bool IsIncrementalStateValid() const { return !packet_loss_; }
-
-  // Returns a ref-counted ptr to the current generation.
-  RefPtr<PacketSequenceStateGeneration> current_generation() const {
-    return current_generation_;
-  }
-
-  bool track_event_timestamps_valid() const {
-    return track_event_timestamps_valid_;
-  }
-
-  bool pid_and_tid_valid() const { return pid_and_tid_valid_; }
-
-  int32_t pid() const { return pid_; }
-  int32_t tid() const { return tid_; }
-
-  TraceProcessorContext* context() const { return context_; }
-
- private:
-  TraceProcessorContext* context_;
-
-  // If true, incremental state on the sequence is considered invalid until we
-  // see the next packet with incremental_state_cleared. We assume that we
-  // missed some packets at the beginning of the trace.
-  bool packet_loss_ = true;
-
-  // We can only consider TrackEvent delta timestamps to be correct after we
-  // have observed a thread descriptor (since the last packet loss).
-  bool track_event_timestamps_valid_ = false;
-
-  // |pid_| and |tid_| are only valid after we parsed at least one
-  // ThreadDescriptor packet on the sequence.
-  bool pid_and_tid_valid_ = false;
-
-  // Process/thread ID of the packet sequence set by a ThreadDescriptor
-  // packet. Used as default values for TrackEvents that don't specify a
-  // pid/tid override. Only valid after |pid_and_tid_valid_| is set to true.
-  int32_t pid_ = 0;
-  int32_t tid_ = 0;
-
-  // Current wall/thread timestamps/counters used as reference for the next
-  // TrackEvent delta timestamp.
-  int64_t track_event_timestamp_ns_ = 0;
-  int64_t track_event_thread_timestamp_ns_ = 0;
-  int64_t track_event_thread_instruction_count_ = 0;
-
-  RefPtr<PacketSequenceStateGeneration> current_generation_;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_H_
diff --git a/src/trace_processor/importers/proto/packet_sequence_state_builder.h b/src/trace_processor/importers/proto/packet_sequence_state_builder.h
new file mode 100644
index 0000000..599b03d
--- /dev/null
+++ b/src/trace_processor/importers/proto/packet_sequence_state_builder.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_BUILDER_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_BUILDER_H_
+
+#include <cstdint>
+
+#include "perfetto/trace_processor/ref_counted.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/types/trace_processor_context.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+// Helper class to generate a stream of PacketSequenceStateGeneration as we
+// receive packets for a sequence. This class deals with various events that
+// incrementally build up state that can be accessed by packet handling code
+// (tokenization nad parsing). An example of such state are interned messages or
+// trace packet defaults.
+class PacketSequenceStateBuilder {
+ public:
+  explicit PacketSequenceStateBuilder(TraceProcessorContext* context) {
+    generation_ = PacketSequenceStateGeneration::CreateFirst(context);
+  }
+
+  // Intern a message into the current generation.
+  void InternMessage(uint32_t field_id, TraceBlobView message) {
+    generation_->InternMessage(field_id, std::move(message));
+  }
+
+  // Set the trace packet defaults for the current generation. If the current
+  // generation already has defaults set, starts a new generation without
+  // invalidating other incremental state (such as interned data).
+  void UpdateTracePacketDefaults(TraceBlobView defaults) {
+    generation_ = generation_->OnNewTracePacketDefaults(std::move(defaults));
+  }
+
+  void OnPacketLoss() {
+    generation_ = generation_->OnPacketLoss();
+    packet_loss_ = true;
+  }
+
+  // Starts a new generation with clean-slate incremental state and defaults.
+  void OnIncrementalStateCleared() {
+    packet_loss_ = false;
+    generation_ = generation_->OnIncrementalStateCleared();
+  }
+
+  bool IsIncrementalStateValid() const { return !packet_loss_; }
+
+  // Returns a ref-counted ptr to the current generation.
+  RefPtr<PacketSequenceStateGeneration> current_generation() const {
+    return generation_;
+  }
+
+ private:
+  // If true, incremental state on the sequence is considered invalid until we
+  // see the next packet with incremental_state_cleared. We assume that we
+  // missed some packets at the beginning of the trace.
+  bool packet_loss_ = true;
+
+  RefPtr<PacketSequenceStateGeneration> generation_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_BUILDER_H_
diff --git a/src/trace_processor/importers/proto/packet_sequence_state_generation.cc b/src/trace_processor/importers/proto/packet_sequence_state_generation.cc
index c520e92..711bb64 100644
--- a/src/trace_processor/importers/proto/packet_sequence_state_generation.cc
+++ b/src/trace_processor/importers/proto/packet_sequence_state_generation.cc
@@ -17,42 +17,89 @@
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include <cstddef>
 
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
+#include "src/trace_processor/importers/proto/track_event_sequence_state.h"
+#include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
 
-namespace perfetto {
-namespace trace_processor {
+namespace perfetto::trace_processor {
+
+PacketSequenceStateGeneration::CustomState::~CustomState() = default;
+
+// static
+RefPtr<PacketSequenceStateGeneration>
+PacketSequenceStateGeneration::CreateFirst(TraceProcessorContext* context) {
+  return RefPtr<PacketSequenceStateGeneration>(
+      new PacketSequenceStateGeneration(
+          context, TrackEventSequenceState::CreateFirst(), false));
+}
 
 PacketSequenceStateGeneration::PacketSequenceStateGeneration(
-    PacketSequenceState* state,
-    PacketSequenceStateGeneration* prev_gen,
-    TraceBlobView defaults)
-    : state_(state),
-      interned_data_(prev_gen->interned_data_),
-      trace_packet_defaults_(InternedMessageView(std::move(defaults))),
-      trackers_(prev_gen->trackers_) {
-  for (auto& t : trackers_) {
-    if (t.get() != nullptr) {
-      t->set_generation(this);
+    TraceProcessorContext* context,
+    InternedFieldMap interned_data,
+    TrackEventSequenceState track_event_sequence_state,
+    CustomStateArray custom_state,
+    TraceBlobView trace_packet_defaults,
+    bool is_incremental_state_valid)
+    : context_(context),
+      interned_data_(std::move(interned_data)),
+      track_event_sequence_state_(std::move(track_event_sequence_state)),
+      custom_state_(std::move(custom_state)),
+      trace_packet_defaults_(std::move(trace_packet_defaults)),
+      is_incremental_state_valid_(is_incremental_state_valid) {
+  for (auto& s : custom_state_) {
+    if (s.get() != nullptr) {
+      s->set_generation(this);
     }
   }
 }
 
-PacketSequenceStateGeneration::InternedDataTracker::~InternedDataTracker() =
-    default;
-
-bool PacketSequenceStateGeneration::pid_and_tid_valid() const {
-  return state_->pid_and_tid_valid();
-}
-int32_t PacketSequenceStateGeneration::pid() const {
-  return state_->pid();
-}
-int32_t PacketSequenceStateGeneration::tid() const {
-  return state_->tid();
+RefPtr<PacketSequenceStateGeneration>
+PacketSequenceStateGeneration::OnPacketLoss() {
+  // No need to increment the generation. If any future packet depends on
+  // previous messages to update the incremental state its packet (if the
+  // DataSource is behaving correctly) would have the
+  // SEQ_NEEDS_INCREMENTAL_STATE bit set and such a packet will be dropped by
+  // the ProtoTraceReader and never make it far enough to update any incremental
+  // state.
+  track_event_sequence_state_.OnPacketLoss();
+  is_incremental_state_valid_ = false;
+  return RefPtr<PacketSequenceStateGeneration>(this);
 }
 
-TraceProcessorContext* PacketSequenceStateGeneration::GetContext() const {
-  return state_->context();
+RefPtr<PacketSequenceStateGeneration>
+PacketSequenceStateGeneration::OnIncrementalStateCleared() {
+  return RefPtr<PacketSequenceStateGeneration>(
+      new PacketSequenceStateGeneration(
+          context_, track_event_sequence_state_.OnIncrementalStateCleared(),
+          true));
+}
+
+RefPtr<PacketSequenceStateGeneration>
+PacketSequenceStateGeneration::OnNewTracePacketDefaults(
+    TraceBlobView trace_packet_defaults) {
+  return RefPtr<PacketSequenceStateGeneration>(
+      new PacketSequenceStateGeneration(
+          context_, interned_data_,
+          track_event_sequence_state_.OnIncrementalStateCleared(),
+          custom_state_, std::move(trace_packet_defaults),
+          is_incremental_state_valid_));
+}
+
+InternedMessageView* PacketSequenceStateGeneration::GetInternedMessageView(
+    uint32_t field_id,
+    uint64_t iid) {
+  auto field_it = interned_data_.find(field_id);
+  if (field_it != interned_data_.end()) {
+    auto* message_map = &field_it->second;
+    auto it = message_map->find(iid);
+    if (it != message_map->end()) {
+      return &it->second;
+    }
+  }
+
+  context_->storage->IncrementStats(stats::interned_data_tokenizer_errors);
+  return nullptr;
 }
 
 void PacketSequenceStateGeneration::InternMessage(uint32_t field_id,
@@ -67,8 +114,7 @@
   auto field = decoder.FindField(kIidFieldNumber);
   if (PERFETTO_UNLIKELY(!field)) {
     PERFETTO_DLOG("Interned message without interning_id");
-    state_->context()->storage->IncrementStats(
-        stats::interned_data_tokenizer_errors);
+    context_->storage->IncrementStats(stats::interned_data_tokenizer_errors);
     return;
   }
   iid = field.as_uint64();
@@ -87,51 +133,4 @@
                           message_size) == 0));
 }
 
-InternedMessageView* PacketSequenceStateGeneration::GetInternedMessageView(
-    uint32_t field_id,
-    uint64_t iid) {
-  auto field_it = interned_data_.find(field_id);
-  if (field_it != interned_data_.end()) {
-    auto* message_map = &field_it->second;
-    auto it = message_map->find(iid);
-    if (it != message_map->end()) {
-      return &it->second;
-    }
-  }
-  state_->context()->storage->IncrementStats(
-      stats::interned_data_tokenizer_errors);
-  return nullptr;
-}
-
-int64_t PacketSequenceStateGeneration::IncrementAndGetTrackEventTimeNs(
-    int64_t delta_ns) {
-  return state_->IncrementAndGetTrackEventTimeNs(delta_ns);
-}
-int64_t PacketSequenceStateGeneration::IncrementAndGetTrackEventThreadTimeNs(
-    int64_t delta_ns) {
-  return state_->IncrementAndGetTrackEventThreadTimeNs(delta_ns);
-}
-int64_t
-PacketSequenceStateGeneration::IncrementAndGetTrackEventThreadInstructionCount(
-    int64_t delta) {
-  return state_->IncrementAndGetTrackEventThreadInstructionCount(delta);
-}
-bool PacketSequenceStateGeneration::track_event_timestamps_valid() const {
-  return state_->track_event_timestamps_valid();
-}
-void PacketSequenceStateGeneration::SetThreadDescriptor(
-    int32_t pid,
-    int32_t tid,
-    int64_t timestamp_ns,
-    int64_t thread_timestamp_ns,
-    int64_t thread_instruction_count) {
-  state_->SetThreadDescriptor(pid, tid, timestamp_ns, thread_timestamp_ns,
-                              thread_instruction_count);
-}
-
-bool PacketSequenceStateGeneration::IsIncrementalStateValid() const {
-  return state_->IsIncrementalStateValid();
-}
-
-}  // namespace trace_processor
-}  // namespace perfetto
+}  // namespace perfetto::trace_processor
diff --git a/src/trace_processor/importers/proto/packet_sequence_state_generation.h b/src/trace_processor/importers/proto/packet_sequence_state_generation.h
index 6b60b59..82e2252 100644
--- a/src/trace_processor/importers/proto/packet_sequence_state_generation.h
+++ b/src/trace_processor/importers/proto/packet_sequence_state_generation.h
@@ -17,21 +17,16 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_GENERATION_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PACKET_SEQUENCE_STATE_GENERATION_H_
 
-#include <array>
-#include <cstddef>
-#include <cstdint>
-#include <memory>
 #include <optional>
-#include <tuple>
-#include <type_traits>
 #include <unordered_map>
 
-#include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
+#include "src/trace_processor/importers/proto/track_event_sequence_state.h"
 #include "src/trace_processor/util/interned_message_view.h"
 
 #include "protos/perfetto/trace/trace_packet_defaults.pbzero.h"
+#include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
 #include "protos/perfetto/trace/track_event/track_event.pbzero.h"
 
 namespace perfetto {
@@ -42,32 +37,36 @@
 using InternedFieldMap =
     std::unordered_map<uint32_t /*field_id*/, InternedMessageMap>;
 
-class PacketSequenceState;
 class TraceProcessorContext;
 
 class StackProfileSequenceState;
 class ProfilePacketSequenceState;
 class V8SequenceState;
 
-using InternedDataTrackers = std::tuple<StackProfileSequenceState,
-                                        ProfilePacketSequenceState,
-                                        V8SequenceState>;
+using CustomStateClasses = std::tuple<StackProfileSequenceState,
+                                      ProfilePacketSequenceState,
+                                      V8SequenceState>;
 
+// This is the public API exposed to packet tokenizers and parsers to access
+// state attached to a packet sequence. This state evolves as packets are
+// processed in sequence order. A packet that requires sequence state to be
+// properly parsed should snapshot this state by taking a copy of the RefPtr to
+// the currently active generation and passing it along with parsing specific
+// data to the sorting stage.
 class PacketSequenceStateGeneration : public RefCounted {
  public:
-  // Base class to add custom sequence state. This state is keep per sequence
-  // and per incremental state interval, that is, each time incremental state is
-  // reset a new instance is created but not each time `TracePacketDefaults` are
-  // updated. Note that this means that different
+  // Base class to attach custom state to the sequence state. This state is keep
+  // per sequence and per incremental state interval, that is, each time
+  // incremental state is reset a new instance is created but not each time
+  // `TracePacketDefaults` are updated. Note that this means that different
   // `PacketSequenceStateGeneration` instances might point to the same
-  // `InternedDataTracker` (because they only differ in their
-  // `TracePacketDefaults`).
+  // `CustomState` (because they only differ in their `TracePacketDefaults`).
   //
   // ATTENTION: You should not create instances of these classes yourself but
-  // use the `PacketSequenceStateGeneration::GetOrCreate<>' method instead.
-  class InternedDataTracker : public RefCounted {
+  // use the `PacketSequenceStateGeneration::GetCustomState<>' method instead.
+  class CustomState : public RefCounted {
    public:
-    virtual ~InternedDataTracker();
+    virtual ~CustomState();
 
    protected:
     template <uint32_t FieldId, typename MessageType>
@@ -81,8 +80,8 @@
     }
 
     template <typename T>
-    std::remove_cv_t<T>* GetOrCreate() {
-      return generation_->GetOrCreate<T>();
+    std::remove_cv_t<T>* GetCustomState() {
+      return generation_->GetCustomState<T>();
     }
 
     bool pid_and_tid_valid() const { return generation_->pid_and_tid_valid(); }
@@ -103,32 +102,57 @@
     // point to the latest one. We keep this member private to prevent misuse /
     // confusion around this fact. Instead subclasses should access the public
     // methods of this class to get any interned data.
+    // TODO(carlscab): Given that CustomState is ref counted this pointer might
+    // become invalid. CustomState should not be ref pointed and instead be
+    // owned by the `PacketSequenceStateGeneration` instance pointed at by
+    // `generation_`.
     PacketSequenceStateGeneration* generation_ = nullptr;
   };
 
-  bool pid_and_tid_valid() const;
-  int32_t pid() const;
-  int32_t tid() const;
+  static RefPtr<PacketSequenceStateGeneration> CreateFirst(
+      TraceProcessorContext* context);
+
+  RefPtr<PacketSequenceStateGeneration> OnPacketLoss();
+
+  RefPtr<PacketSequenceStateGeneration> OnIncrementalStateCleared();
+
+  RefPtr<PacketSequenceStateGeneration> OnNewTracePacketDefaults(
+      TraceBlobView trace_packet_defaults);
+
+  bool pid_and_tid_valid() const {
+    return track_event_sequence_state_.pid_and_tid_valid();
+  }
+  int32_t pid() const { return track_event_sequence_state_.pid(); }
+  int32_t tid() const { return track_event_sequence_state_.tid(); }
 
   // Returns |nullptr| if the message with the given |iid| was not found (also
   // records a stat in this case).
   template <uint32_t FieldId, typename MessageType>
-  typename MessageType::Decoder* LookupInternedMessage(uint64_t iid);
+  typename MessageType::Decoder* LookupInternedMessage(uint64_t iid) {
+    auto* interned_message_view = GetInternedMessageView(FieldId, iid);
+    if (!interned_message_view)
+      return nullptr;
+
+    return interned_message_view->template GetOrCreateDecoder<MessageType>();
+  }
 
   InternedMessageView* GetInternedMessageView(uint32_t field_id, uint64_t iid);
   // Returns |nullptr| if no defaults were set.
   InternedMessageView* GetTracePacketDefaultsView() {
-    if (!trace_packet_defaults_)
+    if (!trace_packet_defaults_.has_value()) {
       return nullptr;
-    return &trace_packet_defaults_.value();
+    }
+
+    return &*trace_packet_defaults_;
   }
 
   // Returns |nullptr| if no defaults were set.
   protos::pbzero::TracePacketDefaults::Decoder* GetTracePacketDefaults() {
-    InternedMessageView* view = GetTracePacketDefaultsView();
-    if (!view)
+    if (!trace_packet_defaults_.has_value()) {
       return nullptr;
-    return view->GetOrCreateDecoder<protos::pbzero::TracePacketDefaults>();
+    }
+    return trace_packet_defaults_
+        ->GetOrCreateDecoder<protos::pbzero::TracePacketDefaults>();
   }
 
   // Returns |nullptr| if no TrackEventDefaults were set.
@@ -148,30 +172,57 @@
     return nullptr;
   }
 
-  // Extension point for custom sequence state. To add new per sequence state
-  // just subclass ´PacketSequenceStateGeneration´ and get your sequence bound
-  // instance by calling this method.
+  // Extension point for custom incremental state. Custom state classes need to
+  // inherit from `CustomState`.
+  //
+  // A common use case for this custom state is to store cache mappings between
+  // interning ids (iid) and TraceProcessor objects (e.g. table row). When we
+  // see an iid we need to access the InternedMessageView for that iid, and
+  // possibly do some computations, the result of all of this could then be
+  // cached so that next time we encounter the same iid we could reuse this
+  // cached value. This caching is only valid until incremental state is
+  // cleared, from then on subsequent iid values on the sequence will no longer
+  // refer to the same entities as the iid values before the clear. Custom state
+  // classes no not need to explicitly handle this: they are attached to an
+  // `IncrementalState` instance, and a new one is created when the state is
+  // cleared, so iid values after the clear will be processed by a new (empty)
+  // custom state instance.
   template <typename T>
-  std::remove_cv_t<T>* GetOrCreate();
+  std::remove_cv_t<T>* GetCustomState();
 
-  // TODO(carlscab): All this should be tracked in a dedicated class
-  // TrackEventSequenceState or something attached to the "incremental state".
-  int64_t IncrementAndGetTrackEventTimeNs(int64_t delta_ns);
-  int64_t IncrementAndGetTrackEventThreadTimeNs(int64_t delta_ns);
-  int64_t IncrementAndGetTrackEventThreadInstructionCount(int64_t delta);
-  bool track_event_timestamps_valid() const;
-  void SetThreadDescriptor(int32_t pid,
-                           int32_t tid,
-                           int64_t timestamp_ns,
-                           int64_t thread_timestamp_ns,
-                           int64_t thread_instruction_count);
+  int64_t IncrementAndGetTrackEventTimeNs(int64_t delta_ns) {
+    return track_event_sequence_state_.IncrementAndGetTrackEventTimeNs(
+        delta_ns);
+  }
+
+  int64_t IncrementAndGetTrackEventThreadTimeNs(int64_t delta_ns) {
+    return track_event_sequence_state_.IncrementAndGetTrackEventThreadTimeNs(
+        delta_ns);
+  }
+
+  int64_t IncrementAndGetTrackEventThreadInstructionCount(int64_t delta) {
+    return track_event_sequence_state_
+        .IncrementAndGetTrackEventThreadInstructionCount(delta);
+  }
+
+  bool track_event_timestamps_valid() const {
+    return track_event_sequence_state_.timestamps_valid();
+  }
+
+  void SetThreadDescriptor(
+      const protos::pbzero::ThreadDescriptor::Decoder& descriptor) {
+    track_event_sequence_state_.SetThreadDescriptor(descriptor);
+  }
 
   // TODO(carlscab): Nobody other than `ProtoTraceReader` should care about
   // this. Remove.
-  bool IsIncrementalStateValid() const;
+  bool IsIncrementalStateValid() const { return is_incremental_state_valid_; }
 
  private:
-  friend class PacketSequenceState;
+  friend class PacketSequenceStateBuilder;
+
+  using CustomStateArray =
+      std::array<RefPtr<CustomState>, std::tuple_size_v<CustomStateClasses>>;
 
   // Helper to find the index in a tuple of a given type. Lookups are done
   // ignoring cv qualifiers. If no index is found size of the tuple is returned.
@@ -195,56 +246,50 @@
     }
   }
 
-  explicit PacketSequenceStateGeneration(PacketSequenceState* state)
-      : state_(state) {}
+  PacketSequenceStateGeneration(TraceProcessorContext* context,
+                                TrackEventSequenceState track_state,
+                                bool is_incremental_state_valid)
+      : context_(context),
+        track_event_sequence_state_(std::move(track_state)),
+        is_incremental_state_valid_(is_incremental_state_valid) {}
 
-  PacketSequenceStateGeneration(PacketSequenceState* state,
-                                PacketSequenceStateGeneration* prev_gen,
-                                TraceBlobView defaults);
+  PacketSequenceStateGeneration(
+      TraceProcessorContext* context,
+      InternedFieldMap interned_data,
+      TrackEventSequenceState track_event_sequence_state,
+      CustomStateArray custom_state,
+      TraceBlobView trace_packet_defaults,
+      bool is_incremental_state_valid);
 
-  TraceProcessorContext* GetContext() const;
-
+  // Add an interned message to this incremental state view. This can only be
+  // called by `PacketSequenceStateBuilder' (which is a friend) as packet
+  // tokenizers and parsers should never deal directly with reading interned
+  // data out of trace packets.
   void InternMessage(uint32_t field_id, TraceBlobView message);
 
-  void SetTracePacketDefaults(TraceBlobView defaults) {
-    // Defaults should only be set once per generation.
-    PERFETTO_DCHECK(!trace_packet_defaults_);
-    trace_packet_defaults_ = InternedMessageView(std::move(defaults));
-  }
-
-  // TODO(carlscab): This is dangerous given that PacketSequenceStateGeneration
-  // is refcounted and PacketSequenceState is not.
-  PacketSequenceState* state_;
+  TraceProcessorContext* const context_;
   InternedFieldMap interned_data_;
+  TrackEventSequenceState track_event_sequence_state_;
+  CustomStateArray custom_state_;
   std::optional<InternedMessageView> trace_packet_defaults_;
-  std::array<RefPtr<InternedDataTracker>,
-             std::tuple_size_v<InternedDataTrackers>>
-      trackers_;
+  // TODO(carlscab): Should not be needed as clients of this class should not
+  // care about validity.
+  bool is_incremental_state_valid_ = true;
 };
 
 template <typename T>
-std::remove_cv_t<T>* PacketSequenceStateGeneration::GetOrCreate() {
-  constexpr size_t index = FindUniqueType<InternedDataTrackers, T>();
-  static_assert(index < std::tuple_size_v<InternedDataTrackers>, "Not found");
-  auto& ptr = trackers_[index];
+std::remove_cv_t<T>* PacketSequenceStateGeneration::GetCustomState() {
+  constexpr size_t index = FindUniqueType<CustomStateClasses, T>();
+  static_assert(index < std::tuple_size_v<CustomStateClasses>, "Not found");
+  auto& ptr = custom_state_[index];
   if (PERFETTO_UNLIKELY(ptr.get() == nullptr)) {
-    ptr.reset(new T(GetContext()));
+    ptr.reset(new T(context_));
     ptr->set_generation(this);
   }
 
   return static_cast<std::remove_cv_t<T>*>(ptr.get());
 }
 
-template <uint32_t FieldId, typename MessageType>
-typename MessageType::Decoder*
-PacketSequenceStateGeneration::LookupInternedMessage(uint64_t iid) {
-  auto* interned_message_view = GetInternedMessageView(FieldId, iid);
-  if (!interned_message_view)
-    return nullptr;
-
-  return interned_message_view->template GetOrCreateDecoder<MessageType>();
-}
-
 }  // namespace trace_processor
 }  // namespace perfetto
 
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc
index 2bfb78a..55e3547 100644
--- a/src/trace_processor/importers/proto/profile_module.cc
+++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -155,7 +155,7 @@
   ProcessTracker* procs = context_->process_tracker.get();
   TraceStorage* storage = context_->storage.get();
   StackProfileSequenceState& stack_profile_sequence_state =
-      *sequence_state->GetOrCreate<StackProfileSequenceState>();
+      *sequence_state->GetCustomState<StackProfileSequenceState>();
 
   uint32_t pid = static_cast<uint32_t>(sequence_state->pid());
   uint32_t tid = static_cast<uint32_t>(sequence_state->tid());
@@ -255,7 +255,7 @@
       context_->process_tracker->GetOrCreateProcess(sample.pid());
 
   StackProfileSequenceState& stack_profile_sequence_state =
-      *sequence_state->GetOrCreate<StackProfileSequenceState>();
+      *sequence_state->GetCustomState<StackProfileSequenceState>();
   uint64_t callstack_iid = sample.callstack_iid();
   std::optional<CallsiteId> cs_id =
       stack_profile_sequence_state.FindOrInsertCallstack(upid, callstack_iid);
@@ -306,7 +306,7 @@
     PacketSequenceStateGeneration* sequence_state,
     ConstBytes blob) {
   ProfilePacketSequenceState& profile_packet_sequence_state =
-      *sequence_state->GetOrCreate<ProfilePacketSequenceState>();
+      *sequence_state->GetCustomState<ProfilePacketSequenceState>();
   protos::pbzero::ProfilePacket::Decoder packet(blob.data, blob.size);
   profile_packet_sequence_state.SetProfilePacketIndex(packet.index());
 
diff --git a/src/trace_processor/importers/proto/profile_packet_sequence_state.cc b/src/trace_processor/importers/proto/profile_packet_sequence_state.cc
index b5cdb70..f0858e3 100644
--- a/src/trace_processor/importers/proto/profile_packet_sequence_state.cc
+++ b/src/trace_processor/importers/proto/profile_packet_sequence_state.cc
@@ -283,8 +283,8 @@
   if (CallsiteId* id = callstacks_.Find(iid); id) {
     return *id;
   }
-  return GetOrCreate<StackProfileSequenceState>()->FindOrInsertCallstack(upid,
-                                                                         iid);
+  return GetCustomState<StackProfileSequenceState>()->FindOrInsertCallstack(
+      upid, iid);
 }
 
 }  // namespace trace_processor
diff --git a/src/trace_processor/importers/proto/profile_packet_sequence_state.h b/src/trace_processor/importers/proto/profile_packet_sequence_state.h
index 3fb8dcd..7282657 100644
--- a/src/trace_processor/importers/proto/profile_packet_sequence_state.h
+++ b/src/trace_processor/importers/proto/profile_packet_sequence_state.h
@@ -34,7 +34,7 @@
 
 // Keeps sequence specific state for profile packets.
 class ProfilePacketSequenceState final
-    : public PacketSequenceStateGeneration::InternedDataTracker {
+    : public PacketSequenceStateGeneration::CustomState {
  public:
   using SourceStringId = uint64_t;
 
diff --git a/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc b/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
index c9cd6e3..ddaaa85 100644
--- a/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
+++ b/src/trace_processor/importers/proto/profile_packet_sequence_state_unittest.cc
@@ -20,7 +20,7 @@
 
 #include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/common/stack_profile_tracker.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "test/gtest_and_gmock.h"
 
@@ -61,7 +61,7 @@
     context.storage.reset(new TraceStorage());
     context.mapping_tracker.reset(new MappingTracker(&context));
     context.stack_profile_tracker.reset(new StackProfileTracker(&context));
-    packet_sequence_state.reset(new PacketSequenceState(&context));
+    sequence_state = PacketSequenceStateGeneration::CreateFirst(&context);
 
     mapping_name = context.storage->InternString("[mapping]");
     fully_qualified_mapping_name = context.storage->InternString("/[mapping]");
@@ -71,8 +71,7 @@
 
  protected:
   ProfilePacketSequenceState& profile_packet_sequence_state() {
-    return *packet_sequence_state->current_generation()
-                ->GetOrCreate<ProfilePacketSequenceState>();
+    return *sequence_state->GetCustomState<ProfilePacketSequenceState>();
   }
   void InsertMapping(const Packet& packet) {
     profile_packet_sequence_state().AddString(packet.mapping_name_id,
@@ -117,7 +116,7 @@
   StringId build;
   StringId frame_name;
   TraceProcessorContext context;
-  std::unique_ptr<PacketSequenceState> packet_sequence_state;
+  RefPtr<PacketSequenceStateGeneration> sequence_state;
 };
 
 // Insert the same mapping from two different packets, with different strings
@@ -200,9 +199,9 @@
   context.storage.reset(new TraceStorage());
   context.mapping_tracker.reset(new MappingTracker(&context));
   context.stack_profile_tracker.reset(new StackProfileTracker(&context));
-  PacketSequenceState pss(&context);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context);
   ProfilePacketSequenceState& ppss =
-      *pss.current_generation()->GetOrCreate<ProfilePacketSequenceState>();
+      *state->GetCustomState<ProfilePacketSequenceState>();
 
   constexpr auto kBuildId = 1u;
   constexpr auto kMappingNameId1 = 2u;
@@ -235,9 +234,9 @@
   context.mapping_tracker.reset(new MappingTracker(&context));
   context.stack_profile_tracker.reset(new StackProfileTracker(&context));
 
-  PacketSequenceState pss(&context);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context);
   ProfilePacketSequenceState& ppss =
-      *pss.current_generation()->GetOrCreate<ProfilePacketSequenceState>();
+      *state->GetCustomState<ProfilePacketSequenceState>();
 
   uint32_t next_string_intern_id = 1;
 
diff --git a/src/trace_processor/importers/proto/proto_incremental_state.h b/src/trace_processor/importers/proto/proto_incremental_state.h
deleted file mode 100644
index aaac42f..0000000
--- a/src/trace_processor/importers/proto/proto_incremental_state.h
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_INCREMENTAL_STATE_H_
-#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_INCREMENTAL_STATE_H_
-
-#include <stdint.h>
-
-#include <map>
-
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
-
-namespace perfetto {
-namespace trace_processor {
-
-class TraceProcessorContext;
-
-// Stores per-packet-sequence incremental state during trace parsing, such as
-// reference timestamps for delta timestamp calculation and interned messages.
-class ProtoIncrementalState {
- public:
-  ProtoIncrementalState(TraceProcessorContext* context) : context_(context) {}
-
-  // Returns the PacketSequenceState for the packet sequence with the given id.
-  // If this is a new sequence which we haven't tracked before, initializes and
-  // inserts a new PacketSequenceState into the state map.
-  PacketSequenceState* GetOrCreateStateForPacketSequence(uint32_t sequence_id) {
-    auto& ptr = packet_sequence_states_[sequence_id];
-    if (!ptr)
-      ptr.reset(new PacketSequenceState(context_));
-    return ptr.get();
-  }
-
- private:
-  // Stores unique_ptrs to ensure that pointers to a PacketSequenceState remain
-  // valid even if the map rehashes.
-  std::map<uint32_t, std::unique_ptr<PacketSequenceState>>
-      packet_sequence_states_;
-
-  TraceProcessorContext* context_;
-};
-
-}  // namespace trace_processor
-}  // namespace perfetto
-
-#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_PROTO_INCREMENTAL_STATE_H_
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.cc b/src/trace_processor/importers/proto/proto_trace_reader.cc
index e139cbe..2e50f0c 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.cc
+++ b/src/trace_processor/importers/proto/proto_trace_reader.cc
@@ -34,8 +34,6 @@
 #include "src/trace_processor/importers/common/track_tracker.h"
 #include "src/trace_processor/importers/ftrace/ftrace_module.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
 #include "src/trace_processor/sorter/trace_sorter.h"
 #include "src/trace_processor/storage/stats.h"
 #include "src/trace_processor/storage/trace_storage.h"
diff --git a/src/trace_processor/importers/proto/proto_trace_reader.h b/src/trace_processor/importers/proto/proto_trace_reader.h
index 1243b97..a93ad98 100644
--- a/src/trace_processor/importers/proto/proto_trace_reader.h
+++ b/src/trace_processor/importers/proto/proto_trace_reader.h
@@ -19,11 +19,13 @@
 
 #include <stdint.h>
 
-#include <memory>
+#include <tuple>
+#include <utility>
 
+#include "perfetto/ext/base/flat_hash_map.h"
 #include "src/trace_processor/importers/common/chunked_trace_reader.h"
 #include "src/trace_processor/importers/proto/multi_machine_trace_manager.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_builder.h"
 #include "src/trace_processor/importers/proto/proto_trace_tokenizer.h"
 #include "src/trace_processor/storage/trace_storage.h"
 
@@ -79,11 +81,15 @@
 
   std::optional<StringId> GetBuiltinClockNameOrNull(int64_t clock_id);
 
-  PacketSequenceState* GetIncrementalStateForPacketSequence(
+  PacketSequenceStateBuilder* GetIncrementalStateForPacketSequence(
       uint32_t sequence_id) {
-    if (!incremental_state)
-      incremental_state.reset(new ProtoIncrementalState(context_));
-    return incremental_state->GetOrCreateStateForPacketSequence(sequence_id);
+    auto* builder = packet_sequence_state_builders_.Find(sequence_id);
+    if (builder == nullptr) {
+      builder = packet_sequence_state_builders_
+                    .Insert(sequence_id, PacketSequenceStateBuilder(context_))
+                    .first;
+    }
+    return builder;
   }
   util::Status ParseExtensionDescriptor(ConstBytes descriptor);
 
@@ -95,9 +101,8 @@
   // timestamp given is latest_timestamp_.
   int64_t latest_timestamp_ = 0;
 
-  // Stores incremental state and references to interned data, e.g. for track
-  // event protos.
-  std::unique_ptr<ProtoIncrementalState> incremental_state;
+  base::FlatHashMap<uint32_t, PacketSequenceStateBuilder>
+      packet_sequence_state_builders_;
 
   StringId skipped_packet_key_id_;
   StringId invalid_incremental_state_key_id_;
diff --git a/src/trace_processor/importers/proto/stack_profile_sequence_state.h b/src/trace_processor/importers/proto/stack_profile_sequence_state.h
index 518388d..e946e8d 100644
--- a/src/trace_processor/importers/proto/stack_profile_sequence_state.h
+++ b/src/trace_processor/importers/proto/stack_profile_sequence_state.h
@@ -23,7 +23,6 @@
 #include "perfetto/ext/base/flat_hash_map.h"
 #include "perfetto/ext/base/hash.h"
 #include "perfetto/ext/base/string_view.h"
-#include "src/trace_processor/importers/common/mapping_tracker.h"
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -35,7 +34,7 @@
 class VirtualMemoryMapping;
 
 class StackProfileSequenceState final
-    : public PacketSequenceStateGeneration::InternedDataTracker {
+    : public PacketSequenceStateGeneration::CustomState {
  public:
   explicit StackProfileSequenceState(TraceProcessorContext* context);
 
diff --git a/src/trace_processor/importers/proto/track_event_parser.cc b/src/trace_processor/importers/proto/track_event_parser.cc
index 09ef474..b2b4e93 100644
--- a/src/trace_processor/importers/proto/track_event_parser.cc
+++ b/src/trace_processor/importers/proto/track_event_parser.cc
@@ -31,6 +31,7 @@
 #include "src/trace_processor/importers/common/machine_tracker.h"
 #include "src/trace_processor/importers/common/process_tracker.h"
 #include "src/trace_processor/importers/common/track_tracker.h"
+#include "src/trace_processor/importers/common/virtual_memory_mapping.h"
 #include "src/trace_processor/importers/json/json_utils.h"
 #include "src/trace_processor/importers/proto/packet_analyzer.h"
 #include "src/trace_processor/importers/proto/profile_packet_utils.h"
@@ -197,7 +198,7 @@
   // Interned mapping_id loses it's meaning when the sequence ends. So we need
   // to get an id from stack_profile_mapping table.
   auto mapping = delegate.seq_state()
-                     ->GetOrCreate<StackProfileSequenceState>()
+                     ->GetCustomState<StackProfileSequenceState>()
                      ->FindOrInsertMapping(decoder->mapping_id());
   if (!mapping) {
     return std::nullopt;
diff --git a/src/trace_processor/importers/proto/track_event_sequence_state.cc b/src/trace_processor/importers/proto/track_event_sequence_state.cc
new file mode 100644
index 0000000..99bc93e
--- /dev/null
+++ b/src/trace_processor/importers/proto/track_event_sequence_state.cc
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "src/trace_processor/importers/proto/track_event_sequence_state.h"
+
+#include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+void TrackEventSequenceState::SetThreadDescriptor(
+    const protos::pbzero::ThreadDescriptor::Decoder& decoder) {
+  persistent_state_.pid_and_tid_valid = true;
+  persistent_state_.pid = decoder.pid();
+  persistent_state_.tid = decoder.tid();
+
+  timestamps_valid_ = true;
+  timestamp_ns_ = decoder.reference_timestamp_us() * 1000;
+  thread_timestamp_ns_ = decoder.reference_thread_time_us() * 1000;
+  thread_instruction_count_ = decoder.reference_thread_instruction_count();
+}
+
+}  // namespace trace_processor
+}  // namespace perfetto
diff --git a/src/trace_processor/importers/proto/track_event_sequence_state.h b/src/trace_processor/importers/proto/track_event_sequence_state.h
new file mode 100644
index 0000000..9291ef7
--- /dev/null
+++ b/src/trace_processor/importers/proto/track_event_sequence_state.h
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_SEQUENCE_STATE_H_
+#define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_SEQUENCE_STATE_H_
+
+#include <utility>
+
+#include "protos/perfetto/trace/track_event/thread_descriptor.pbzero.h"
+
+namespace perfetto {
+namespace trace_processor {
+
+class TrackEventSequenceState {
+ public:
+  static TrackEventSequenceState CreateFirst() {
+    return TrackEventSequenceState(PersistentState());
+  }
+
+  TrackEventSequenceState OnIncrementalStateCleared() {
+    return TrackEventSequenceState(persistent_state_);
+  }
+
+  void OnPacketLoss() { timestamps_valid_ = false; }
+
+  bool pid_and_tid_valid() const { return persistent_state_.pid_and_tid_valid; }
+
+  int32_t pid() const { return persistent_state_.pid; }
+  int32_t tid() const { return persistent_state_.tid; }
+
+  bool timestamps_valid() const { return timestamps_valid_; }
+
+  int64_t IncrementAndGetTrackEventTimeNs(int64_t delta_ns) {
+    PERFETTO_DCHECK(timestamps_valid());
+    timestamp_ns_ += delta_ns;
+    return timestamp_ns_;
+  }
+
+  int64_t IncrementAndGetTrackEventThreadTimeNs(int64_t delta_ns) {
+    PERFETTO_DCHECK(timestamps_valid());
+    thread_timestamp_ns_ += delta_ns;
+    return thread_timestamp_ns_;
+  }
+
+  int64_t IncrementAndGetTrackEventThreadInstructionCount(int64_t delta) {
+    PERFETTO_DCHECK(timestamps_valid());
+    thread_instruction_count_ += delta;
+    return thread_instruction_count_;
+  }
+
+  void SetThreadDescriptor(const protos::pbzero::ThreadDescriptor::Decoder&);
+
+ private:
+  // State that is never cleared.
+  struct PersistentState {
+    // |pid_| and |tid_| are only valid after we parsed at least one
+    // ThreadDescriptor packet on the sequence.
+    bool pid_and_tid_valid = false;
+
+    // Process/thread ID of the packet sequence set by a ThreadDescriptor
+    // packet. Used as default values for TrackEvents that don't specify a
+    // pid/tid override. Only valid after |pid_and_tid_valid_| is set to true.
+    int32_t pid = 0;
+    int32_t tid = 0;
+  };
+
+  explicit TrackEventSequenceState(PersistentState persistent_state)
+      : persistent_state_(std::move(persistent_state)) {}
+
+  // We can only consider TrackEvent delta timestamps to be correct after we
+  // have observed a thread descriptor (since the last packet loss).
+  bool timestamps_valid_ = false;
+
+  // Current wall/thread timestamps/counters used as reference for the next
+  // TrackEvent delta timestamp.
+  int64_t timestamp_ns_ = 0;
+  int64_t thread_timestamp_ns_ = 0;
+  int64_t thread_instruction_count_ = 0;
+
+  PersistentState persistent_state_;
+};
+
+}  // namespace trace_processor
+}  // namespace perfetto
+
+#endif  // SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_TRACK_EVENT_SEQUENCE_STATE_H_
diff --git a/src/trace_processor/importers/proto/track_event_tokenizer.cc b/src/trace_processor/importers/proto/track_event_tokenizer.cc
index d9ed336..c836052 100644
--- a/src/trace_processor/importers/proto/track_event_tokenizer.cc
+++ b/src/trace_processor/importers/proto/track_event_tokenizer.cc
@@ -212,10 +212,7 @@
     const protos::pbzero::ThreadDescriptor::Decoder& thread) {
   // TODO(eseckler): Remove support for legacy thread descriptor-based default
   // tracks and delta timestamps.
-  state.SetThreadDescriptor(thread.pid(), thread.tid(),
-                            thread.reference_timestamp_us() * 1000,
-                            thread.reference_thread_time_us() * 1000,
-                            thread.reference_thread_instruction_count());
+  state.SetThreadDescriptor(thread);
 }
 
 void TrackEventTokenizer::TokenizeTrackEventPacket(
diff --git a/src/trace_processor/importers/proto/v8_module.cc b/src/trace_processor/importers/proto/v8_module.cc
index 89360b6..88d8f53 100644
--- a/src/trace_processor/importers/proto/v8_module.cc
+++ b/src/trace_processor/importers/proto/v8_module.cc
@@ -142,7 +142,8 @@
 void V8Module::ParseV8JsCode(protozero::ConstBytes bytes,
                              int64_t ts,
                              const TracePacketData& data) {
-  V8SequenceState& state = *data.sequence_state->GetOrCreate<V8SequenceState>();
+  V8SequenceState& state =
+      *data.sequence_state->GetCustomState<V8SequenceState>();
 
   V8JsCode::Decoder code(bytes);
 
@@ -169,7 +170,8 @@
 void V8Module::ParseV8InternalCode(protozero::ConstBytes bytes,
                                    int64_t ts,
                                    const TracePacketData& data) {
-  V8SequenceState& state = *data.sequence_state->GetOrCreate<V8SequenceState>();
+  V8SequenceState& state =
+      *data.sequence_state->GetCustomState<V8SequenceState>();
 
   V8InternalCode::Decoder code(bytes);
 
@@ -190,7 +192,8 @@
 void V8Module::ParseV8WasmCode(protozero::ConstBytes bytes,
                                int64_t ts,
                                const TracePacketData& data) {
-  V8SequenceState& state = *data.sequence_state->GetOrCreate<V8SequenceState>();
+  V8SequenceState& state =
+      *data.sequence_state->GetCustomState<V8SequenceState>();
 
   V8WasmCode::Decoder code(bytes);
 
@@ -217,7 +220,8 @@
 void V8Module::ParseV8RegExpCode(protozero::ConstBytes bytes,
                                  int64_t ts,
                                  const TracePacketData& data) {
-  V8SequenceState& state = *data.sequence_state->GetOrCreate<V8SequenceState>();
+  V8SequenceState& state =
+      *data.sequence_state->GetCustomState<V8SequenceState>();
 
   V8RegExpCode::Decoder code(bytes);
 
@@ -238,7 +242,8 @@
 void V8Module::ParseV8CodeMove(protozero::ConstBytes bytes,
                                int64_t,
                                const TracePacketData& data) {
-  V8SequenceState& state = *data.sequence_state->GetOrCreate<V8SequenceState>();
+  V8SequenceState& state =
+      *data.sequence_state->GetCustomState<V8SequenceState>();
   protos::pbzero::V8CodeMove::Decoder v8_code_move(bytes);
 
   std::optional<IsolateId> isolate_id =
diff --git a/src/trace_processor/importers/proto/v8_sequence_state.h b/src/trace_processor/importers/proto/v8_sequence_state.h
index d2e008a..1b60ca0 100644
--- a/src/trace_processor/importers/proto/v8_sequence_state.h
+++ b/src/trace_processor/importers/proto/v8_sequence_state.h
@@ -24,7 +24,6 @@
 #include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/tables/v8_tables_py.h"
-#include "src/trace_processor/types/destructible.h"
 
 namespace perfetto {
 namespace trace_processor {
@@ -34,7 +33,7 @@
 
 // Helper class to deal with V8 related interned data.
 class V8SequenceState final
-    : public PacketSequenceStateGeneration::InternedDataTracker {
+    : public PacketSequenceStateGeneration::CustomState {
  public:
   explicit V8SequenceState(TraceProcessorContext* context);
 
diff --git a/src/trace_processor/importers/proto/vulkan_memory_tracker.h b/src/trace_processor/importers/proto/vulkan_memory_tracker.h
index 9d1afc5..dd44a89 100644
--- a/src/trace_processor/importers/proto/vulkan_memory_tracker.h
+++ b/src/trace_processor/importers/proto/vulkan_memory_tracker.h
@@ -17,19 +17,18 @@
 #ifndef SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_VULKAN_MEMORY_TRACKER_H_
 #define SRC_TRACE_PROCESSOR_IMPORTERS_PROTO_VULKAN_MEMORY_TRACKER_H_
 
-#include "protos/perfetto/trace/profiling/profile_common.pbzero.h"
-#include "src/trace_processor/importers/proto/proto_incremental_state.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/storage/trace_storage.h"
+#include "src/trace_processor/types/trace_processor_context.h"
 
 #include "protos/perfetto/trace/gpu/vulkan_memory_event.pbzero.h"
+#include "protos/perfetto/trace/profiling/profile_common.pbzero.h"
 
 namespace perfetto {
 namespace trace_processor {
 
 using protos::pbzero::VulkanMemoryEvent;
 
-class TraceProcessorContext;
-
 class VulkanMemoryTracker {
  public:
   enum class DeviceCounterType {
diff --git a/src/trace_processor/sorter/BUILD.gn b/src/trace_processor/sorter/BUILD.gn
index 5d27204..8d4c9f9 100644
--- a/src/trace_processor/sorter/BUILD.gn
+++ b/src/trace_processor/sorter/BUILD.gn
@@ -50,9 +50,11 @@
     "../../../gn:default_deps",
     "../../../gn:gtest_and_gmock",
     "../../../include/perfetto/trace_processor:storage",
+    "../../../include/perfetto/trace_processor:trace_processor",
     "../../base",
     "../importers/common:parser_types",
     "../importers/proto:minimal",
+    "../importers/proto:packet_sequence_state_generation_hdr",
     "../types",
   ]
 }
diff --git a/src/trace_processor/sorter/trace_sorter.h b/src/trace_processor/sorter/trace_sorter.h
index dd38504..5ea0e0d 100644
--- a/src/trace_processor/sorter/trace_sorter.h
+++ b/src/trace_processor/sorter/trace_sorter.h
@@ -28,7 +28,6 @@
 #include "perfetto/public/compiler.h"
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/common/parser_types.h"
 #include "src/trace_processor/importers/common/trace_parser.h"
 #include "src/trace_processor/importers/fuchsia/fuchsia_record.h"
 #include "src/trace_processor/importers/systrace/systrace_line.h"
diff --git a/src/trace_processor/sorter/trace_sorter_unittest.cc b/src/trace_processor/sorter/trace_sorter_unittest.cc
index 3fc8f76..42421df 100644
--- a/src/trace_processor/sorter/trace_sorter_unittest.cc
+++ b/src/trace_processor/sorter/trace_sorter_unittest.cc
@@ -13,8 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#include "perfetto/trace_processor/trace_blob_view.h"
-#include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
+#include "src/trace_processor/sorter/trace_sorter.h"
 
 #include <map>
 #include <random>
@@ -22,9 +21,10 @@
 
 #include "perfetto/trace_processor/basic_types.h"
 #include "perfetto/trace_processor/trace_blob.h"
+#include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
-#include "src/trace_processor/sorter/trace_sorter.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
+#include "src/trace_processor/importers/proto/proto_trace_parser_impl.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "test/gtest_and_gmock.h"
 
@@ -105,26 +105,25 @@
 };
 
 TEST_F(TraceSorterTest, TestFtrace) {
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
   TraceBlobView view = test_buffer_.slice_off(0, 1);
   EXPECT_CALL(*parser_,
               MOCK_ParseFtracePacket(0, 1000, view.data(), 1, kNullMachineId));
   context_.sorter->PushFtraceEvent(0 /*cpu*/, 1000 /*timestamp*/,
-                                   std::move(view), state.current_generation());
+                                   std::move(view), state);
   context_.sorter->ExtractEventsForced();
 }
 
 TEST_F(TraceSorterTest, TestTracePacket) {
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
   TraceBlobView view = test_buffer_.slice_off(0, 1);
   EXPECT_CALL(*parser_, MOCK_ParseTracePacket(1000, view.data(), 1));
-  context_.sorter->PushTracePacket(1000, state.current_generation(),
-                                   std::move(view));
+  context_.sorter->PushTracePacket(1000, state, std::move(view));
   context_.sorter->ExtractEventsForced();
 }
 
 TEST_F(TraceSorterTest, Ordering) {
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
   TraceBlobView view_1 = test_buffer_.slice_off(0, 1);
   TraceBlobView view_2 = test_buffer_.slice_off(0, 2);
   TraceBlobView view_3 = test_buffer_.slice_off(0, 3);
@@ -140,22 +139,18 @@
                                                kNullMachineId));
 
   context_.sorter->PushFtraceEvent(2 /*cpu*/, 1200 /*timestamp*/,
-                                   std::move(view_4),
-                                   state.current_generation());
-  context_.sorter->PushTracePacket(1001, state.current_generation(),
-                                   std::move(view_2));
-  context_.sorter->PushTracePacket(1100, state.current_generation(),
-                                   std::move(view_3));
+                                   std::move(view_4), state);
+  context_.sorter->PushTracePacket(1001, state, std::move(view_2));
+  context_.sorter->PushTracePacket(1100, state, std::move(view_3));
   context_.sorter->PushFtraceEvent(0 /*cpu*/, 1000 /*timestamp*/,
-                                   std::move(view_1),
-                                   state.current_generation());
+                                   std::move(view_1), state);
   context_.sorter->ExtractEventsForced();
 }
 
 TEST_F(TraceSorterTest, IncrementalExtraction) {
   CreateSorter(false);
 
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
 
   TraceBlobView view_1 = test_buffer_.slice_off(0, 1);
   TraceBlobView view_2 = test_buffer_.slice_off(0, 2);
@@ -166,10 +161,8 @@
   // Flush at the start of packet sequence to match behavior of the
   // service.
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1200, state.current_generation(),
-                                   std::move(view_2));
-  context_.sorter->PushTracePacket(1100, state.current_generation(),
-                                   std::move(view_1));
+  context_.sorter->PushTracePacket(1200, state, std::move(view_2));
+  context_.sorter->PushTracePacket(1100, state, std::move(view_1));
 
   // No data should be exttracted at this point because we haven't
   // seen two flushes yet.
@@ -182,10 +175,8 @@
 
   context_.sorter->NotifyFlushEvent();
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1400, state.current_generation(),
-                                   std::move(view_4));
-  context_.sorter->PushTracePacket(1300, state.current_generation(),
-                                   std::move(view_3));
+  context_.sorter->PushTracePacket(1400, state, std::move(view_4));
+  context_.sorter->PushTracePacket(1300, state, std::move(view_3));
 
   // This ReadBuffer call should finally extract until the first OnReadBuffer
   // call.
@@ -197,8 +188,7 @@
   context_.sorter->NotifyReadBufferEvent();
 
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1500, state.current_generation(),
-                                   std::move(view_5));
+  context_.sorter->PushTracePacket(1500, state, std::move(view_5));
 
   // Nothing should be extracted as we haven't seen the second flush.
   context_.sorter->NotifyReadBufferEvent();
@@ -222,7 +212,7 @@
 TEST_F(TraceSorterTest, OutOfOrder) {
   CreateSorter(false);
 
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
 
   TraceBlobView view_1 = test_buffer_.slice_off(0, 1);
   TraceBlobView view_2 = test_buffer_.slice_off(0, 2);
@@ -231,10 +221,8 @@
 
   context_.sorter->NotifyFlushEvent();
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1200, state.current_generation(),
-                                   std::move(view_2));
-  context_.sorter->PushTracePacket(1100, state.current_generation(),
-                                   std::move(view_1));
+  context_.sorter->PushTracePacket(1200, state, std::move(view_2));
+  context_.sorter->PushTracePacket(1100, state, std::move(view_1));
   context_.sorter->NotifyReadBufferEvent();
 
   // Both of the packets should have been pushed through.
@@ -250,8 +238,7 @@
   // Now, pass the third packet out of order.
   context_.sorter->NotifyFlushEvent();
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1150, state.current_generation(),
-                                   std::move(view_3));
+  context_.sorter->PushTracePacket(1150, state, std::move(view_3));
   context_.sorter->NotifyReadBufferEvent();
 
   // The third packet should still be pushed through.
@@ -268,8 +255,7 @@
   // Push the fourth packet also out of order but after third.
   context_.sorter->NotifyFlushEvent();
   context_.sorter->NotifyFlushEvent();
-  context_.sorter->PushTracePacket(1170, state.current_generation(),
-                                   std::move(view_4));
+  context_.sorter->PushTracePacket(1170, state, std::move(view_4));
   context_.sorter->NotifyReadBufferEvent();
 
   // The fourt packet should still be pushed through.
@@ -286,7 +272,7 @@
 // Tests that the output of the TraceSorter matches the timestamp order
 // (% events happening at the same time on different CPUs).
 TEST_F(TraceSorterTest, MultiQueueSorting) {
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
   std::minstd_rand0 rnd_engine(0);
   std::map<int64_t /*ts*/, std::vector<uint32_t /*cpu*/>> expectations;
 
@@ -320,8 +306,7 @@
     for (uint8_t j = 0; j < num_cpus; j++) {
       uint32_t cpu = static_cast<uint32_t>(rnd_engine() % 32);
       expectations[ts].push_back(cpu);
-      context_.sorter->PushFtraceEvent(cpu, ts, tbv.slice_off(i, 1),
-                                       state.current_generation());
+      context_.sorter->PushFtraceEvent(cpu, ts, tbv.slice_off(i, 1), state);
     }
   }
 
@@ -331,7 +316,7 @@
 
 // An generalized version of MultiQueueSorting with multiple machines.
 TEST_F(TraceSorterTest, MultiMachineSorting) {
-  PacketSequenceState state(&context_);
+  auto state = PacketSequenceStateGeneration::CreateFirst(&context_);
   std::minstd_rand0 rnd_engine(0);
 
   struct ExpectedMachineAndCpu {
@@ -426,9 +411,8 @@
       for (uint8_t j = 0; j < num_cpus; j++) {
         uint32_t cpu = static_cast<uint32_t>(rnd_engine() % 32);
         expectations[ts].push_back(ExpectedMachineAndCpu{machine, cpu});
-        context_.sorter->PushFtraceEvent(cpu, ts,
-                                         tbv.slice_off(m * alloc_size + i, 1),
-                                         state.current_generation(), machine);
+        context_.sorter->PushFtraceEvent(
+            cpu, ts, tbv.slice_off(m * alloc_size + i, 1), state, machine);
       }
     }
   }
diff --git a/src/trace_processor/sorter/trace_token_buffer_unittest.cc b/src/trace_processor/sorter/trace_token_buffer_unittest.cc
index a8657d2..59f7e93 100644
--- a/src/trace_processor/sorter/trace_token_buffer_unittest.cc
+++ b/src/trace_processor/sorter/trace_token_buffer_unittest.cc
@@ -19,10 +19,11 @@
 #include <optional>
 
 #include "perfetto/base/compiler.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "src/trace_processor/importers/common/parser_types.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/types/trace_processor_context.h"
 #include "test/gtest_and_gmock.h"
 
@@ -34,17 +35,18 @@
  protected:
   TraceTokenBuffer store;
   TraceProcessorContext context;
-  PacketSequenceState state{&context};
+  RefPtr<PacketSequenceStateGeneration> state =
+      PacketSequenceStateGeneration::CreateFirst(&context);
 };
 
 TEST_F(TraceTokenBufferUnittest, TracePacketDataInOut) {
   TraceBlobView tbv(TraceBlob::Allocate(1024));
-  TracePacketData tpd{tbv.copy(), state.current_generation()};
+  TracePacketData tpd{tbv.copy(), state};
 
   TraceTokenBuffer::Id id = store.Append(std::move(tpd));
   TracePacketData extracted = store.Extract<TracePacketData>(id);
   ASSERT_EQ(extracted.packet, tbv);
-  ASSERT_EQ(extracted.sequence_state, state.current_generation());
+  ASSERT_EQ(extracted.sequence_state, state);
 }
 
 TEST_F(TraceTokenBufferUnittest, PacketAppendMultipleBlobs) {
@@ -53,14 +55,14 @@
   TraceBlobView tbv_3(TraceBlob::Allocate(4096));
 
   TraceTokenBuffer::Id id_1 =
-      store.Append(TracePacketData{tbv_1.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_1.copy(), state});
   TraceTokenBuffer::Id id_2 =
-      store.Append(TracePacketData{tbv_2.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_2.copy(), state});
   ASSERT_EQ(store.Extract<TracePacketData>(id_1).packet, tbv_1);
   ASSERT_EQ(store.Extract<TracePacketData>(id_2).packet, tbv_2);
 
   TraceTokenBuffer::Id id_3 =
-      store.Append(TracePacketData{tbv_3.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_3.copy(), state});
   ASSERT_EQ(store.Extract<TracePacketData>(id_3).packet, tbv_3);
 }
 
@@ -71,14 +73,14 @@
   TraceBlobView tbv_3 = root.slice_off(1536, 512);
 
   TraceTokenBuffer::Id id_1 =
-      store.Append(TracePacketData{tbv_1.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_1.copy(), state});
   TraceTokenBuffer::Id id_2 =
-      store.Append(TracePacketData{tbv_2.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_2.copy(), state});
   ASSERT_EQ(store.Extract<TracePacketData>(id_1).packet, tbv_1);
   ASSERT_EQ(store.Extract<TracePacketData>(id_2).packet, tbv_2);
 
   TraceTokenBuffer::Id id_3 =
-      store.Append(TracePacketData{tbv_3.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_3.copy(), state});
   ASSERT_EQ(store.Extract<TracePacketData>(id_3).packet, tbv_3);
 }
 
@@ -88,13 +90,11 @@
   TraceBlobView tbv_2 = root.slice_off(1024, 512);
 
   TraceTokenBuffer::Id id_1 =
-      store.Append(TracePacketData{tbv_1.copy(), state.current_generation()});
+      store.Append(TracePacketData{tbv_1.copy(), state});
   TraceTokenBuffer::Id id_2 =
-      store.Append(TracePacketData{tbv_2.copy(), state.current_generation()});
-  ASSERT_EQ(store.Extract<TracePacketData>(id_1).sequence_state,
-            state.current_generation());
-  ASSERT_EQ(store.Extract<TracePacketData>(id_2).sequence_state,
-            state.current_generation());
+      store.Append(TracePacketData{tbv_2.copy(), state});
+  ASSERT_EQ(store.Extract<TracePacketData>(id_1).sequence_state, state);
+  ASSERT_EQ(store.Extract<TracePacketData>(id_2).sequence_state, state);
 }
 
 TEST_F(TraceTokenBufferUnittest, ManySequenceState) {
@@ -103,10 +103,9 @@
   std::array<TraceTokenBuffer::Id, 1024> ids;
   std::array<PacketSequenceStateGeneration*, 1024> refs;
   for (uint32_t i = 0; i < 1024; ++i) {
-    refs[i] = state.current_generation().get();
-    ids[i] = store.Append(
-        TracePacketData{root.slice_off(i, 1), state.current_generation()});
-    state.OnIncrementalStateCleared();
+    refs[i] = state.get();
+    ids[i] = store.Append(TracePacketData{root.slice_off(i, 1), state});
+    state = state->OnNewTracePacketDefaults(TraceBlobView());
   }
 
   for (uint32_t i = 0; i < 1024; ++i) {
@@ -120,22 +119,22 @@
 
   TraceBlobView slice_1 = tbv.slice_off(0, 1024ul);
   TraceTokenBuffer::Id id_1 =
-      store.Append(TracePacketData{slice_1.copy(), state.current_generation()});
+      store.Append(TracePacketData{slice_1.copy(), state});
   TracePacketData out_1 = store.Extract<TracePacketData>(id_1);
   ASSERT_EQ(out_1.packet, slice_1);
-  ASSERT_EQ(out_1.sequence_state, state.current_generation());
+  ASSERT_EQ(out_1.sequence_state, state);
 
   TraceBlobView slice_2 = tbv.slice_off(128ul * 1024, 1024ul);
   TraceTokenBuffer::Id id_2 =
-      store.Append(TracePacketData{slice_2.copy(), state.current_generation()});
+      store.Append(TracePacketData{slice_2.copy(), state});
   TracePacketData out_2 = store.Extract<TracePacketData>(id_2);
   ASSERT_EQ(out_2.packet, slice_2);
-  ASSERT_EQ(out_2.sequence_state, state.current_generation());
+  ASSERT_EQ(out_2.sequence_state, state);
 }
 
 TEST_F(TraceTokenBufferUnittest, TrackEventDataInOut) {
   TraceBlobView tbv(TraceBlob::Allocate(1234));
-  TrackEventData ted(tbv.copy(), state.current_generation());
+  TrackEventData ted(tbv.copy(), state);
   ted.thread_instruction_count = 123;
   ted.extra_counter_values = {10, 2, 0, 0, 0, 0, 0, 0};
   auto counter_array = ted.extra_counter_values;
@@ -143,8 +142,7 @@
   TraceTokenBuffer::Id id = store.Append(std::move(ted));
   TrackEventData extracted = store.Extract<TrackEventData>(id);
   ASSERT_EQ(extracted.trace_packet_data.packet, tbv);
-  ASSERT_EQ(extracted.trace_packet_data.sequence_state,
-            state.current_generation());
+  ASSERT_EQ(extracted.trace_packet_data.sequence_state, state);
   ASSERT_EQ(extracted.thread_instruction_count, 123);
   ASSERT_EQ(extracted.thread_timestamp, std::nullopt);
   ASSERT_DOUBLE_EQ(extracted.counter_value, 0.0);
diff --git a/src/trace_processor/util/BUILD.gn b/src/trace_processor/util/BUILD.gn
index 86ecf9c..5ca39a1 100644
--- a/src/trace_processor/util/BUILD.gn
+++ b/src/trace_processor/util/BUILD.gn
@@ -305,6 +305,7 @@
     "../../protozero:testing_messages_zero",
     "../importers/proto:gen_cc_track_event_descriptor",
     "../importers/proto:minimal",
+    "../importers/proto:packet_sequence_state_generation_hdr",
     "../storage",
     "../types",
   ]
diff --git a/src/trace_processor/util/debug_annotation_parser_unittest.cc b/src/trace_processor/util/debug_annotation_parser_unittest.cc
index d9a4168..902e9da 100644
--- a/src/trace_processor/util/debug_annotation_parser_unittest.cc
+++ b/src/trace_processor/util/debug_annotation_parser_unittest.cc
@@ -18,6 +18,7 @@
 
 #include "perfetto/ext/base/string_view.h"
 #include "perfetto/protozero/scattered_heap_buffer.h"
+#include "perfetto/trace_processor/ref_counted.h"
 #include "perfetto/trace_processor/trace_blob.h"
 #include "perfetto/trace_processor/trace_blob_view.h"
 #include "protos/perfetto/common/descriptor.pbzero.h"
@@ -27,7 +28,8 @@
 #include "protos/perfetto/trace/track_event/debug_annotation.pbzero.h"
 #include "protos/perfetto/trace/track_event/source_location.pbzero.h"
 #include "src/protozero/test/example_proto/test_messages.pbzero.h"
-#include "src/trace_processor/importers/proto/packet_sequence_state.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_builder.h"
+#include "src/trace_processor/importers/proto/packet_sequence_state_generation.h"
 #include "src/trace_processor/storage/trace_storage.h"
 #include "src/trace_processor/test_messages.descriptor.h"
 #include "src/trace_processor/types/trace_processor_context.h"
@@ -54,13 +56,13 @@
 class DebugAnnotationParserTest : public ::testing::Test,
                                   public ProtoToArgsParser::Delegate {
  protected:
-  DebugAnnotationParserTest() : sequence_state_(&context_) {
-    context_.storage.reset(new TraceStorage());
-  }
+  DebugAnnotationParserTest() { context_.storage.reset(new TraceStorage()); }
 
   const std::vector<std::string>& args() const { return args_; }
 
-  PacketSequenceState* mutable_seq_state() { return &sequence_state_; }
+  void InternMessage(uint32_t field_id, TraceBlobView message) {
+    state_builder_.InternMessage(field_id, std::move(message));
+  }
 
  private:
   using Key = ProtoToArgsParser::Key;
@@ -132,23 +134,19 @@
 
   InternedMessageView* GetInternedMessageView(uint32_t field_id,
                                               uint64_t iid) override {
-    if (field_id !=
-        protos::pbzero::InternedData::kDebugAnnotationStringValuesFieldNumber) {
-      return nullptr;
-    }
-    return sequence_state_.current_generation()->GetInternedMessageView(
-        field_id, iid);
+    return state_builder_.current_generation()->GetInternedMessageView(field_id,
+                                                                       iid);
   }
 
   PacketSequenceStateGeneration* seq_state() final {
-    return sequence_state_.current_generation().get();
+    return state_builder_.current_generation().get();
   }
 
   std::vector<std::string> args_;
   std::map<std::string, size_t> array_indices_;
 
   TraceProcessorContext context_;
-  PacketSequenceState sequence_state_;
+  PacketSequenceStateBuilder state_builder_{&context_};
 };
 
 // This test checks that in when an array is nested inside a dict which is
@@ -303,7 +301,7 @@
   string->set_str("foo");
   std::vector<uint8_t> data_serialized = string.SerializeAsArray();
 
-  mutable_seq_state()->InternMessage(
+  InternMessage(
       protos::pbzero::InternedData::kDebugAnnotationStringValuesFieldNumber,
       TraceBlobView(
           TraceBlob::CopyFrom(data_serialized.data(), data_serialized.size())));
diff --git a/src/trace_processor/util/interned_message_view.h b/src/trace_processor/util/interned_message_view.h
index f6a9a13..7ee28c3 100644
--- a/src/trace_processor/util/interned_message_view.h
+++ b/src/trace_processor/util/interned_message_view.h
@@ -103,7 +103,7 @@
     return submessage_view;
   }
 
-  const TraceBlobView& message() { return message_; }
+  const TraceBlobView& message() const { return message_; }
 
  private:
   using SubMessageViewMap =
diff --git a/src/tracing/internal/tracing_muxer_impl.cc b/src/tracing/internal/tracing_muxer_impl.cc
index b9efcf5..8fd1d8a 100644
--- a/src/tracing/internal/tracing_muxer_impl.cc
+++ b/src/tracing/internal/tracing_muxer_impl.cc
@@ -1855,7 +1855,10 @@
       rds.descriptor.set_no_flush(rds.no_flush);
     }
     rds.descriptor.set_will_notify_on_start(true);
-    rds.descriptor.set_will_notify_on_stop(true);
+    if (!rds.descriptor.has_will_notify_on_stop()) {
+        rds.descriptor.set_will_notify_on_stop(true);
+    }
+
     rds.descriptor.set_handles_incremental_state_clear(true);
     rds.descriptor.set_id(rds.static_state->id);
     if (is_registered) {
diff --git a/ui/release/channels.json b/ui/release/channels.json
index c80cab9..7e403f2 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -2,7 +2,7 @@
   "channels": [
     {
       "name": "stable",
-      "rev": "c7da51a07f6bee116c7e1c3b91fa7abfc2a084bb"
+      "rev": "444bb44f0712aeb12d6ab546035811d4541a8f93"
     },
     {
       "name": "canary",
diff --git a/ui/src/common/actions.ts b/ui/src/common/actions.ts
index 92cad97..4cebecb 100644
--- a/ui/src/common/actions.ts
+++ b/ui/src/common/actions.ts
@@ -15,7 +15,7 @@
 import {Draft} from 'immer';
 
 import {assertExists, assertTrue} from '../base/logging';
-import {duration, time} from '../base/time';
+import {duration, Time, time} from '../base/time';
 import {RecordConfig} from '../controller/record_config_types';
 import {
   GenericSliceDetailsTabConfig,
@@ -63,7 +63,6 @@
   State,
   Status,
   ThreadTrackSortKey,
-  TraceTime,
   TrackSortKey,
   UtidToTrackSortKey,
   VisibleState,
@@ -474,10 +473,6 @@
     state.permalink = {};
   },
 
-  setTraceTime(state: StateDraft, args: TraceTime): void {
-    state.traceTime = args;
-  },
-
   updateStatus(state: StateDraft, args: Status): void {
     if (statusTraceEvent) {
       traceEventEnd(statusTraceEvent);
@@ -673,7 +668,7 @@
     };
     this.openFlamegraph(state, {
       type: args.type,
-      start: state.traceTime.start as time, // TODO(stevegolton): Avoid type assertion here.
+      start: Time.ZERO,
       end: args.ts,
       upids: [args.upid],
       viewingOption: defaultViewingOption(args.type),
diff --git a/ui/src/common/empty_state.ts b/ui/src/common/empty_state.ts
index f866914..e3ed2b7 100644
--- a/ui/src/common/empty_state.ts
+++ b/ui/src/common/empty_state.ts
@@ -22,12 +22,7 @@
 } from '../frontend/record_config';
 import {SqlTables} from '../frontend/sql_table/well_known_tables';
 
-import {
-  defaultTraceTime,
-  NonSerializableState,
-  State,
-  STATE_VERSION,
-} from './state';
+import {NonSerializableState, State, STATE_VERSION} from './state';
 
 const AUTOLOAD_STARTED_CONFIG_FLAG = featureFlags.register({
   id: 'autoloadStartedConfig',
@@ -92,7 +87,6 @@
     version: STATE_VERSION,
     nextId: '-1',
     newEngineMode: 'USE_HTTP_RPC_IF_AVAILABLE',
-    traceTime: {...defaultTraceTime},
     tracks: {},
     utidToThreadSortKey: {},
     aggregatePreferences: {},
@@ -112,7 +106,8 @@
 
     frontendLocalState: {
       visibleState: {
-        ...defaultTraceTime,
+        start: Time.ZERO,
+        end: Time.ZERO,
         lastUpdate: 0,
         resolution: 0n,
       },
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 0f2b2b7..3a5899e 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -21,7 +21,6 @@
 import {
   Command,
   DetailsPanel,
-  EngineProxy,
   MetricVisualisation,
   Migrate,
   Plugin,
@@ -36,7 +35,7 @@
   GroupPredicate,
   TrackRef,
 } from '../public';
-import {Engine} from '../trace_processor/engine';
+import {EngineBase, Engine} from '../trace_processor/engine';
 
 import {Actions} from './actions';
 import {SCROLLING_TRACK_GROUP} from './state';
@@ -103,9 +102,12 @@
 class PluginContextTraceImpl implements PluginContextTrace, Disposable {
   private trash = new Trash();
   private alive = true;
+  readonly engine: Engine;
 
-  constructor(private ctx: PluginContext, readonly engine: EngineProxy) {
-    this.trash.add(engine);
+  constructor(private ctx: PluginContext, engine: EngineBase) {
+    const engineProxy = engine.getProxy(ctx.pluginId);
+    this.trash.add(engineProxy);
+    this.engine = engineProxy;
   }
 
   registerCommand(cmd: Command): void {
@@ -380,7 +382,7 @@
 export class PluginManager {
   private registry: PluginRegistry;
   private _plugins: Map<string, PluginDetails>;
-  private engine?: Engine;
+  private engine?: EngineBase;
   private flags = new Map<string, Flag>();
 
   constructor(registry: PluginRegistry) {
@@ -466,7 +468,7 @@
     // If a trace is already loaded when plugin is activated, make sure to
     // call onTraceLoad().
     if (this.engine) {
-      await doPluginTraceLoad(pluginDetails, this.engine, id);
+      await doPluginTraceLoad(pluginDetails, this.engine);
     }
 
     this._plugins.set(id, pluginDetails);
@@ -528,7 +530,7 @@
   }
 
   async onTraceLoad(
-    engine: Engine,
+    engine: EngineBase,
     beforeEach?: (id: string) => void,
   ): Promise<void> {
     this.engine = engine;
@@ -546,7 +548,7 @@
     // time.
     for (const {id, plugin} of pluginsShuffled) {
       beforeEach?.(id);
-      await doPluginTraceLoad(plugin, engine, id);
+      await doPluginTraceLoad(plugin, engine);
     }
   }
 
@@ -571,14 +573,11 @@
 
 async function doPluginTraceLoad(
   pluginDetails: PluginDetails,
-  engine: Engine,
-  pluginId: string,
+  engine: EngineBase,
 ): Promise<void> {
   const {plugin, context} = pluginDetails;
 
-  const engineProxy = engine.getProxy(pluginId);
-
-  const traceCtx = new PluginContextTraceImpl(context, engineProxy);
+  const traceCtx = new PluginContextTraceImpl(context, engine);
   pluginDetails.traceContext = traceCtx;
 
   const startTime = performance.now();
diff --git a/ui/src/common/plugins_unittest.ts b/ui/src/common/plugins_unittest.ts
index 55fc82d..7efccda 100644
--- a/ui/src/common/plugins_unittest.ts
+++ b/ui/src/common/plugins_unittest.ts
@@ -14,12 +14,12 @@
 
 import {globals} from '../frontend/globals';
 import {Plugin} from '../public';
-import {Engine} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
 
 import {createEmptyState} from './empty_state';
 import {PluginManager, PluginRegistry} from './plugins';
 
-class FakeEngine extends Engine {
+class FakeEngine extends EngineBase {
   id: string = 'TestEngine';
 
   rpcSendRequestBytes(_data: Uint8Array) {}
diff --git a/ui/src/common/queries.ts b/ui/src/common/queries.ts
index 857d8e9..a6c461b 100644
--- a/ui/src/common/queries.ts
+++ b/ui/src/common/queries.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {Row} from '../trace_processor/query_result';
 
 const MAX_DISPLAY_ROWS = 10000;
@@ -36,7 +36,7 @@
 
 export async function runQuery(
   sqlQuery: string,
-  engine: EngineProxy,
+  engine: Engine,
   params?: QueryRunParams,
 ): Promise<QueryResponse> {
   const startMs = performance.now();
diff --git a/ui/src/common/schema.ts b/ui/src/common/schema.ts
index 75a4b2f..c3f33e4 100644
--- a/ui/src/common/schema.ts
+++ b/ui/src/common/schema.ts
@@ -12,10 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {STR} from '../trace_processor/query_result';
 
-const CACHED_SCHEMAS = new WeakMap<EngineProxy, DatabaseSchema>();
+const CACHED_SCHEMAS = new WeakMap<Engine, DatabaseSchema>();
 
 export class SchemaError extends Error {
   constructor(message: string) {
@@ -40,7 +40,7 @@
 }
 
 async function getColumns(
-  engine: EngineProxy,
+  engine: Engine,
   table: string,
 ): Promise<ColumnInfo[]> {
   const result = await engine.query(`PRAGMA table_info(${table});`);
@@ -83,7 +83,7 @@
 
 // Deliberately not exported. Users should call getSchema below and
 // participate in cacheing.
-async function createSchema(engine: EngineProxy): Promise<DatabaseSchema> {
+async function createSchema(engine: Engine): Promise<DatabaseSchema> {
   const tables: TableInfo[] = [];
   const result = await engine.query(`SELECT name from perfetto_tables;`);
   const it = result.iter({
@@ -108,7 +108,7 @@
 // The schemas are per-engine (i.e. they can't be statically determined
 // at build time) since we might be in httpd mode and not-running
 // against the version of trace_processor we build with.
-export async function getSchema(engine: EngineProxy): Promise<DatabaseSchema> {
+export async function getSchema(engine: Engine): Promise<DatabaseSchema> {
   const schema = CACHED_SCHEMAS.get(engine);
   if (schema === undefined) {
     const newSchema = await createSchema(engine);
diff --git a/ui/src/common/state.ts b/ui/src/common/state.ts
index d0af73b..669f539 100644
--- a/ui/src/common/state.ts
+++ b/ui/src/common/state.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {BigintMath} from '../base/bigint_math';
-import {duration, Time, time} from '../base/time';
+import {duration, time} from '../base/time';
 import {RecordConfig} from '../controller/record_config_types';
 import {
   Aggregation,
@@ -150,7 +150,8 @@
 // 51. Changed structure of FlamegraphState.expandedCallsiteByViewingOption.
 // 52. Update track group state - don't make the summary track the first track.
 // 53. Remove android log state.
-export const STATE_VERSION = 53;
+// 54. Remove traceTime.
+export const STATE_VERSION = 54;
 
 export const SCROLLING_TRACK_GROUP = 'ScrollingTracks';
 
@@ -315,11 +316,6 @@
   isRecordingConfig?: boolean; // this permalink request is for a recording config only
 }
 
-export interface TraceTime {
-  start: time;
-  end: time;
-}
-
 export interface FrontendLocalState {
   visibleState: VisibleState;
 }
@@ -479,7 +475,6 @@
    */
   newEngineMode: NewEngineMode;
   engine?: EngineConfig;
-  traceTime: TraceTime;
   traceUuid?: string;
   trackGroups: ObjectById<TrackGroupState>;
   tracks: ObjectByKey<TrackState>;
@@ -558,11 +553,6 @@
   plugins: {[key: string]: any};
 }
 
-export const defaultTraceTime = {
-  start: Time.ZERO,
-  end: Time.fromSeconds(10),
-};
-
 export declare type RecordMode =
   | 'STOP_WHEN_FULL'
   | 'RING_BUFFER'
@@ -645,11 +635,12 @@
 }
 
 export function getBuiltinChromeCategoryList(): string[] {
-  // List of static Chrome categories, last updated at 2023-05-30 from HEAD of
+  // List of static Chrome categories, last updated at 2024-04-15 from HEAD of
   // Chromium's //base/trace_event/builtin_categories.h.
   return [
     'accessibility',
     'AccountFetcherService',
+    'android.ui.jank',
     'android_webview',
     'android_webview.timeline',
     'aogh',
@@ -686,6 +677,7 @@
     'compositor',
     'content',
     'content_capture',
+    'interactions',
     'delegated_ink_trails',
     'device',
     'devtools',
@@ -713,6 +705,7 @@
     'gpu.angle',
     'gpu.angle.texture_metrics',
     'gpu.capture',
+    'graphics.pipeline',
     'headless',
     'history',
     'hwoverlays',
@@ -720,6 +713,7 @@
     'ime',
     'IndexedDB',
     'input',
+    'input.scrolling',
     'io',
     'ipc',
     'Java',
@@ -739,7 +733,9 @@
     'mus',
     'native',
     'navigation',
+    'navigation.debug',
     'net',
+    'network.scheduler',
     'netlog',
     'offline_pages',
     'omnibox',
@@ -802,19 +798,19 @@
     'webengine.fidl',
     'weblayer',
     'WebCore',
+    'webnn',
     'webrtc',
     'webrtc_stats',
     'xr',
     'disabled-by-default-android_view_hierarchy',
     'disabled-by-default-animation-worklet',
     'disabled-by-default-audio',
-    'disabled-by-default-audio-worklet',
     'disabled-by-default-audio.latency',
+    'disabled-by-default-audio-worklet',
     'disabled-by-default-base',
     'disabled-by-default-blink.debug',
     'disabled-by-default-blink.debug.display_lock',
     'disabled-by-default-blink.debug.layout',
-    'disabled-by-default-blink.debug.layout.scrollbars',
     'disabled-by-default-blink.debug.layout.trees',
     'disabled-by-default-blink.feature_usage',
     'disabled-by-default-blink.image_decoding',
@@ -842,6 +838,9 @@
     'disabled-by-default-devtools.timeline.layers',
     'disabled-by-default-devtools.timeline.picture',
     'disabled-by-default-devtools.timeline.stack',
+    'disabled-by-default-devtools.target-rundown',
+    'disabled-by-default-devtools.v8-source-rundown',
+    'disabled-by-default-devtools.v8-source-rundown-sources',
     'disabled-by-default-file',
     'disabled-by-default-fonts',
     'disabled-by-default-gpu_cmd_queue',
@@ -849,6 +848,7 @@
     'disabled-by-default-gpu.debug',
     'disabled-by-default-gpu.decoder',
     'disabled-by-default-gpu.device',
+    'disabled-by-default-gpu.graphite.dawn',
     'disabled-by-default-gpu.service',
     'disabled-by-default-gpu.vulkan.vma',
     'disabled-by-default-histogram_samples',
@@ -874,7 +874,9 @@
     'disabled-by-default-skia.gpu',
     'disabled-by-default-skia.gpu.cache',
     'disabled-by-default-skia.shaders',
+    'disabled-by-default-skottie',
     'disabled-by-default-SyncFileSystem',
+    'disabled-by-default-system_power',
     'disabled-by-default-system_stats',
     'disabled-by-default-thread_pool_diagnostics',
     'disabled-by-default-toplevel.ipc',
@@ -893,6 +895,7 @@
     'disabled-by-default-v8.wasm.detailed',
     'disabled-by-default-v8.wasm.turbofan',
     'disabled-by-default-video_and_image_capture',
+    'disabled-by-default-display.framedisplayed',
     'disabled-by-default-viz.gpu_composite_time',
     'disabled-by-default-viz.debug.overlay_planes',
     'disabled-by-default-viz.hit_testing_flow',
@@ -901,8 +904,10 @@
     'disabled-by-default-viz.surface_id_flow',
     'disabled-by-default-viz.surface_lifetime',
     'disabled-by-default-viz.triangles',
+    'disabled-by-default-viz.visual_debugger',
     'disabled-by-default-webaudio.audionode',
     'disabled-by-default-webgpu',
+    'disabled-by-default-webnn',
     'disabled-by-default-webrtc',
     'disabled-by-default-worker.scheduler',
     'disabled-by-default-xr.debug',
diff --git a/ui/src/common/track_helper.ts b/ui/src/common/track_helper.ts
index 84808df..396b955 100644
--- a/ui/src/common/track_helper.ts
+++ b/ui/src/common/track_helper.ts
@@ -19,16 +19,6 @@
 import {raf} from '../core/raf_scheduler';
 import {globals} from '../frontend/globals';
 
-export {EngineProxy} from '../trace_processor/engine';
-export {
-  LONG,
-  LONG_NULL,
-  NUM,
-  NUM_NULL,
-  STR,
-  STR_NULL,
-} from '../trace_processor/query_result';
-
 type FetchTimeline<Data> = (
   start: time,
   end: time,
diff --git a/ui/src/controller/selection_controller.ts b/ui/src/controller/selection_controller.ts
index dc905ea..1b6dd0f 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.state.traceTime.end;
+    const endTs = rightTs !== -1n ? rightTs : globals.traceTime.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 c1f0d6e..445bd4a 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -26,25 +26,26 @@
   isMetatracingEnabled,
 } from '../common/metatracing';
 import {pluginManager} from '../common/plugins';
+import {EngineMode, PendingDeeplinkState, ProfileType} from '../common/state';
+import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
 import {
   defaultTraceTime,
-  EngineMode,
-  PendingDeeplinkState,
-  ProfileType,
-} from '../common/state';
-import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags';
-import {globals, QuantizedLoad, ThreadDesc} from '../frontend/globals';
+  globals,
+  QuantizedLoad,
+  ThreadDesc,
+  TraceTime,
+} from '../frontend/globals';
 import {
   clearOverviewData,
   publishHasFtrace,
   publishMetricError,
   publishOverviewData,
-  publishRealtimeOffset,
   publishThreads,
+  publishTraceDetails,
 } from '../frontend/publish';
 import {addQueryResultsTab} from '../frontend/query_result_tab';
 import {Router} from '../frontend/router';
-import {Engine} from '../trace_processor/engine';
+import {Engine, EngineBase} from '../trace_processor/engine';
 import {HttpRpcEngine} from '../trace_processor/http_rpc_engine';
 import {
   LONG,
@@ -224,7 +225,7 @@
 // trace opened in the UI (for now only one trace is supported).
 export class TraceController extends Controller<States> {
   private readonly engineId: string;
-  private engine?: Engine;
+  private engine?: EngineBase;
 
   constructor(engineId: string) {
     super('init');
@@ -450,13 +451,8 @@
     // traceUuid will be '' if the trace is not cacheable (URL or RPC).
     const traceUuid = await this.cacheCurrentTrace();
 
-    const traceTime = await this.engine.getTraceTimeBounds();
-    const start = traceTime.start;
-    const end = traceTime.end;
-    const traceTimeState = {
-      start,
-      end,
-    };
+    const traceDetails = await getTraceTimeDetails(this.engine);
+    publishTraceDetails(traceDetails);
 
     const shownJsonWarning =
       window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
@@ -485,12 +481,11 @@
     const actions: DeferredAction[] = [
       Actions.setOmnibox(emptyOmniboxState),
       Actions.setTraceUuid({traceUuid}),
-      Actions.setTraceTime(traceTimeState),
     ];
 
     const visibleTimeSpan = await computeVisibleTime(
-      traceTime.start,
-      traceTime.end,
+      traceDetails.start,
+      traceDetails.end,
       isJsonTrace,
       this.engine,
     );
@@ -530,7 +525,9 @@
     this.decideTabs();
 
     await this.listThreads();
-    await this.loadTimelineOverview(traceTime);
+    await this.loadTimelineOverview(
+      new TimeSpan(traceDetails.start, traceDetails.end),
+    );
 
     {
       // Check if we have any ftrace events at all
@@ -544,82 +541,12 @@
       publishHasFtrace(res.numRows() > 0);
     }
 
-    {
-      // Find the first REALTIME or REALTIME_COARSE clock snapshot.
-      // Prioritize REALTIME over REALTIME_COARSE.
-      const query = `select
-            ts,
-            clock_value as clockValue,
-            clock_name as clockName
-          from clock_snapshot
-          where
-            snapshot_id = 0 AND
-            clock_name in ('REALTIME', 'REALTIME_COARSE')
-          `;
-      const result = await assertExists(this.engine).query(query);
-      const it = result.iter({
-        ts: LONG,
-        clockValue: LONG,
-        clockName: STR,
-      });
-
-      let snapshot = {
-        clockName: '',
-        ts: Time.ZERO,
-        clockValue: Time.ZERO,
-      };
-
-      // Find the most suitable snapshot
-      for (let row = 0; it.valid(); it.next(), row++) {
-        if (it.clockName === 'REALTIME') {
-          snapshot = {
-            clockName: it.clockName,
-            ts: Time.fromRaw(it.ts),
-            clockValue: Time.fromRaw(it.clockValue),
-          };
-          break;
-        } else if (it.clockName === 'REALTIME_COARSE') {
-          if (snapshot.clockName !== 'REALTIME') {
-            snapshot = {
-              clockName: it.clockName,
-              ts: Time.fromRaw(it.ts),
-              clockValue: Time.fromRaw(it.clockValue),
-            };
-          }
-        }
-      }
-
-      // The max() is so the query returns NULL if the tz info doesn't exist.
-      const queryTz = `select max(int_value) as tzOffMin from metadata
-          where name = 'timezone_off_mins'`;
-      const resTz = await assertExists(this.engine).query(queryTz);
-      const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0;
-
-      // This is the offset between the unix epoch and ts in the ts domain.
-      // I.e. the value of ts at the time of the unix epoch - usually some large
-      // negative value.
-      const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue);
-
-      // Find the previous closest midnight from the trace start time.
-      const utcOffset = Time.getLatestMidnight(
-        globals.state.traceTime.start,
-        realtimeOffset,
-      );
-
-      const traceTzOffset = Time.getLatestMidnight(
-        globals.state.traceTime.start,
-        Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)),
-      );
-
-      publishRealtimeOffset(realtimeOffset, utcOffset, traceTzOffset);
-    }
-
     globals.dispatch(Actions.sortThreadTracks({}));
     globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
 
     await this.selectFirstHeapProfile();
     if (PERF_SAMPLE_FLAG.get()) {
-      await this.selectPerfSample();
+      await this.selectPerfSample(traceDetails);
     }
 
     const pendingDeeplink = globals.state.pendingDeeplink;
@@ -663,7 +590,7 @@
     return engineMode;
   }
 
-  private async selectPerfSample() {
+  private async selectPerfSample(traceTime: {start: time; end: time}) {
     const query = `select upid
         from perf_sample
         join thread using (utid)
@@ -673,8 +600,8 @@
     if (profile.numRows() !== 1) return;
     const row = profile.firstRow({upid: NUM});
     const upid = row.upid;
-    const leftTs = globals.state.traceTime.start;
-    const rightTs = globals.state.traceTime.end;
+    const leftTs = traceTime.start;
+    const rightTs = traceTime.end;
     globals.dispatch(
       Actions.selectPerfSamples({
         id: 0,
@@ -766,7 +693,7 @@
 
   private async listTracks() {
     this.updateStatus('Loading tracks');
-    const engine = assertExists<Engine>(this.engine);
+    const engine = assertExists(this.engine);
     const actions = await decideTracks(engine);
     globals.dispatchMultiple(actions);
   }
@@ -918,7 +845,7 @@
   }
 
   async initialiseHelperViews() {
-    const engine = assertExists<Engine>(this.engine);
+    const engine = assertExists(this.engine);
 
     this.updateStatus('Creating annotation counter track table');
     // Create the helper tables for all the annotations related data.
@@ -1217,3 +1144,77 @@
   }
   return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd);
 }
+
+async function getTraceTimeDetails(engine: EngineBase): Promise<TraceTime> {
+  const traceTime = await engine.getTraceTimeBounds();
+
+  // Find the first REALTIME or REALTIME_COARSE clock snapshot.
+  // Prioritize REALTIME over REALTIME_COARSE.
+  const query = `select
+          ts,
+          clock_value as clockValue,
+          clock_name as clockName
+        from clock_snapshot
+        where
+          snapshot_id = 0 AND
+          clock_name in ('REALTIME', 'REALTIME_COARSE')
+        `;
+  const result = await engine.query(query);
+  const it = result.iter({
+    ts: LONG,
+    clockValue: LONG,
+    clockName: STR,
+  });
+
+  let snapshot = {
+    clockName: '',
+    ts: Time.ZERO,
+    clockValue: Time.ZERO,
+  };
+
+  // Find the most suitable snapshot
+  for (let row = 0; it.valid(); it.next(), row++) {
+    if (it.clockName === 'REALTIME') {
+      snapshot = {
+        clockName: it.clockName,
+        ts: Time.fromRaw(it.ts),
+        clockValue: Time.fromRaw(it.clockValue),
+      };
+      break;
+    } else if (it.clockName === 'REALTIME_COARSE') {
+      if (snapshot.clockName !== 'REALTIME') {
+        snapshot = {
+          clockName: it.clockName,
+          ts: Time.fromRaw(it.ts),
+          clockValue: Time.fromRaw(it.clockValue),
+        };
+      }
+    }
+  }
+
+  // The max() is so the query returns NULL if the tz info doesn't exist.
+  const queryTz = `select max(int_value) as tzOffMin from metadata
+        where name = 'timezone_off_mins'`;
+  const resTz = await assertExists(engine).query(queryTz);
+  const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0;
+
+  // This is the offset between the unix epoch and ts in the ts domain.
+  // I.e. the value of ts at the time of the unix epoch - usually some large
+  // negative value.
+  const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue);
+
+  // Find the previous closest midnight from the trace start time.
+  const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset);
+
+  const traceTzOffset = Time.getLatestMidnight(
+    traceTime.start,
+    Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)),
+  );
+
+  return {
+    ...traceTime,
+    realtimeOffset,
+    utcOffset,
+    traceTzOffset,
+  };
+}
diff --git a/ui/src/controller/track_decider.ts b/ui/src/controller/track_decider.ts
index f0c7ef4..e1b907c 100644
--- a/ui/src/controller/track_decider.ts
+++ b/ui/src/controller/track_decider.ts
@@ -27,7 +27,7 @@
 import {PERF_SAMPLE_FLAG} from '../core/feature_flags';
 import {PrimaryTrackSortKey} from '../public';
 import {getTrackName} from '../public/utils';
-import {Engine, EngineProxy} from '../trace_processor/engine';
+import {Engine, EngineBase} from '../trace_processor/engine';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../trace_processor/query_result';
 import {ASYNC_SLICE_TRACK_KIND} from '../core_plugins/async_slices';
 import {
@@ -76,18 +76,20 @@
 const CHROME_TRACK_GROUP = 'Chrome Global Tracks';
 const MISC_GROUP = 'Misc Global Tracks';
 
-export async function decideTracks(engine: Engine): Promise<DeferredAction[]> {
+export async function decideTracks(
+  engine: EngineBase,
+): Promise<DeferredAction[]> {
   return new TrackDecider(engine).decideTracks();
 }
 
 class TrackDecider {
-  private engine: Engine;
+  private engine: EngineBase;
   private upidToUuid = new Map<number, string>();
   private utidToUuid = new Map<number, string>();
   private tracksToAdd: AddTrackArgs[] = [];
   private addTrackGroupActions: DeferredAction[] = [];
 
-  constructor(engine: Engine) {
+  constructor(engine: EngineBase) {
     this.engine = engine;
   }
 
@@ -131,7 +133,7 @@
     }
   }
 
-  async addCpuFreqTracks(engine: EngineProxy): Promise<void> {
+  async addCpuFreqTracks(engine: Engine): Promise<void> {
     const cpus = await this.engine.getCpus();
 
     for (const cpu of cpus) {
@@ -165,7 +167,7 @@
     }
   }
 
-  async addGlobalAsyncTracks(engine: EngineProxy): Promise<void> {
+  async addGlobalAsyncTracks(engine: Engine): Promise<void> {
     const rawGlobalAsyncTracks = await engine.query(`
       with global_tracks_grouped as (
         select distinct t.parent_id, t.name
@@ -226,7 +228,7 @@
     }
   }
 
-  async addGpuFreqTracks(engine: EngineProxy): Promise<void> {
+  async addGpuFreqTracks(engine: Engine): Promise<void> {
     const numGpus = await this.engine.getNumberOfGpus();
     for (let gpu = 0; gpu < numGpus; gpu++) {
       // Only add a gpu freq track if we have
@@ -248,7 +250,7 @@
     }
   }
 
-  async addCpuFreqLimitCounterTracks(engine: EngineProxy): Promise<void> {
+  async addCpuFreqLimitCounterTracks(engine: Engine): Promise<void> {
     const cpuFreqLimitCounterTracksSql = `
       select name, id
       from cpu_counter_track
@@ -259,7 +261,7 @@
     this.addCpuCounterTracks(engine, cpuFreqLimitCounterTracksSql);
   }
 
-  async addCpuPerfCounterTracks(engine: EngineProxy): Promise<void> {
+  async addCpuPerfCounterTracks(engine: Engine): Promise<void> {
     // Perf counter tracks are bound to CPUs, follow the scheduling and
     // frequency track naming convention ("Cpu N ...").
     // Note: we might not have a track for a given cpu if no data was seen from
@@ -274,7 +276,7 @@
     this.addCpuCounterTracks(engine, addCpuPerfCounterTracksSql);
   }
 
-  async addCpuCounterTracks(engine: EngineProxy, sql: string): Promise<void> {
+  async addCpuCounterTracks(engine: Engine, sql: string): Promise<void> {
     const result = await engine.query(sql);
 
     const it = result.iter({
@@ -516,7 +518,7 @@
     }
   }
 
-  async addAnnotationTracks(engine: EngineProxy): Promise<void> {
+  async addAnnotationTracks(engine: Engine): Promise<void> {
     const sliceResult = await engine.query(`
       select id, name, upid, group_name
       from annotation_slice_track
@@ -607,7 +609,7 @@
     }
   }
 
-  async addThreadStateTracks(engine: EngineProxy): Promise<void> {
+  async addThreadStateTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         utid,
@@ -657,7 +659,7 @@
     }
   }
 
-  async addThreadCpuSampleTracks(engine: EngineProxy): Promise<void> {
+  async addThreadCpuSampleTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       with thread_cpu_sample as (
         select distinct utid
@@ -695,7 +697,7 @@
     }
   }
 
-  async addThreadCounterTracks(engine: EngineProxy): Promise<void> {
+  async addThreadCounterTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         thread_counter_track.name as trackName,
@@ -745,7 +747,7 @@
     }
   }
 
-  async addProcessAsyncSliceTracks(engine: EngineProxy): Promise<void> {
+  async addProcessAsyncSliceTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         upid,
@@ -790,7 +792,7 @@
     }
   }
 
-  async addUserAsyncSliceTracks(engine: EngineProxy): Promise<void> {
+  async addUserAsyncSliceTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       with grouped_packages as materialized (
         select
@@ -848,7 +850,7 @@
     }
   }
 
-  async addActualFramesTracks(engine: EngineProxy): Promise<void> {
+  async addActualFramesTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         upid,
@@ -891,7 +893,7 @@
     }
   }
 
-  async addExpectedFramesTracks(engine: EngineProxy): Promise<void> {
+  async addExpectedFramesTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         upid,
@@ -935,7 +937,7 @@
     }
   }
 
-  async addThreadSliceTracks(engine: EngineProxy): Promise<void> {
+  async addThreadSliceTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         thread_track.utid as utid,
@@ -990,7 +992,7 @@
     }
   }
 
-  async addProcessCounterTracks(engine: EngineProxy): Promise<void> {
+  async addProcessCounterTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select
         process_counter_track.id as trackId,
@@ -1034,7 +1036,7 @@
     }
   }
 
-  async addProcessHeapProfileTracks(engine: EngineProxy): Promise<void> {
+  async addProcessHeapProfileTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select upid
       from _process_available_info_summary
@@ -1052,7 +1054,7 @@
     }
   }
 
-  async addProcessPerfSamplesTracks(engine: EngineProxy): Promise<void> {
+  async addProcessPerfSamplesTracks(engine: Engine): Promise<void> {
     const result = await engine.query(`
       select upid, pid
       from _process_available_info_summary
@@ -1099,7 +1101,7 @@
     this.upidToUuid.set(upid, uuid);
   }
 
-  async addKernelThreadGrouping(engine: EngineProxy): Promise<void> {
+  async addKernelThreadGrouping(engine: Engine): Promise<void> {
     // Identify kernel threads if this is a linux system trace, and sufficient
     // process information is available. Kernel threads are identified by being
     // children of kthreadd (always pid 2).
@@ -1162,7 +1164,7 @@
     }
   }
 
-  async addProcessTrackGroups(engine: EngineProxy): Promise<void> {
+  async addProcessTrackGroups(engine: Engine): Promise<void> {
     // We want to create groups of tracks in a specific order.
     // The tracks should be grouped:
     //    by upid
diff --git a/ui/src/core_plugins/android_log/logs_panel.ts b/ui/src/core_plugins/android_log/logs_panel.ts
index e326a38..ecb1a3b 100644
--- a/ui/src/core_plugins/android_log/logs_panel.ts
+++ b/ui/src/core_plugins/android_log/logs_panel.ts
@@ -21,7 +21,7 @@
 
 import {globals} from '../../frontend/globals';
 import {Timestamp} from '../../frontend/widgets/timestamp';
-import {EngineProxy, LONG, NUM, NUM_NULL, Store, STR} from '../../public';
+import {Engine, LONG, NUM, NUM_NULL, Store, STR} from '../../public';
 import {Monitor} from '../../base/monitor';
 import {AsyncLimiter} from '../../base/async_limiter';
 import {escapeGlob, escapeQuery} from '../../trace_processor/query_utils';
@@ -43,7 +43,7 @@
 
 export interface LogPanelAttrs {
   filterStore: Store<LogFilteringCriteria>;
-  engine: EngineProxy;
+  engine: Engine;
 }
 
 interface Pagination {
@@ -384,7 +384,7 @@
 }
 
 async function updateLogEntries(
-  engine: EngineProxy,
+  engine: Engine,
   span: Span<time, duration>,
   pagination: Pagination,
 ): Promise<LogEntries> {
@@ -450,10 +450,7 @@
   };
 }
 
-async function updateLogView(
-  engine: EngineProxy,
-  filter: LogFilteringCriteria,
-) {
+async function updateLogView(engine: Engine, filter: LogFilteringCriteria) {
   await engine.query('drop view if exists filtered_logs');
 
   const globMatch = composeGlobMatch(filter.hideNonMatching, filter.textEntry);
diff --git a/ui/src/core_plugins/android_log/logs_track.ts b/ui/src/core_plugins/android_log/logs_track.ts
index 1c5b3c6..7027a65 100644
--- a/ui/src/core_plugins/android_log/logs_track.ts
+++ b/ui/src/core_plugins/android_log/logs_track.ts
@@ -14,11 +14,11 @@
 
 import {Time, duration, time} from '../../base/time';
 import {LIMIT, TrackData} from '../../common/track_data';
-import {LONG, NUM, TimelineFetcher} from '../../common/track_helper';
+import {TimelineFetcher} from '../../common/track_helper';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
 import {PanelSize} from '../../frontend/panel';
-import {EngineProxy, Track} from '../../public';
+import {Engine, LONG, NUM, Track} from '../../public';
 
 export interface Data extends TrackData {
   // Total number of log events within [start, end], before any quantization.
@@ -52,7 +52,7 @@
 export class AndroidLogTrack implements Track {
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
 
-  constructor(private engine: EngineProxy) {}
+  constructor(private engine: Engine) {}
 
   async onUpdate(): Promise<void> {
     await this.fetcher.requestDataForCurrentTime();
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 1feb5cc..4267f4c 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -25,7 +25,7 @@
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
-import {Engine, EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 
 import {ChromeTasksScrollJankTrack} from './chrome_tasks_scroll_jank_track';
 import {EventLatencySliceDetailsPanel} from './event_latency_details_panel';
@@ -329,7 +329,7 @@
   }
 }
 
-async function isChromeTrace(engine: EngineProxy) {
+async function isChromeTrace(engine: Engine) {
   const queryResult = await engine.query(`
       select utid, upid
       from thread
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
index d476019..a2f644b 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_delta_graph.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 
 import {duration, Time, time} from '../../base/time';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM} from '../../trace_processor/query_result';
 import {VegaView} from '../../widgets/vega_view';
 
@@ -45,7 +45,7 @@
 }
 
 export async function getUserScrollDeltas(
-  engine: EngineProxy,
+  engine: Engine,
   startTs: time,
   dur: duration,
 ): Promise<ScrollDeltaDetails[]> {
@@ -82,7 +82,7 @@
 }
 
 export async function getAppliedScrollDeltas(
-  engine: EngineProxy,
+  engine: Engine,
   startTs: time,
   dur: duration,
 ): Promise<ScrollDeltaDetails[]> {
@@ -123,7 +123,7 @@
 }
 
 export async function getJankIntervals(
-  engine: EngineProxy,
+  engine: Engine,
   startTs: time,
   dur: duration,
 ): Promise<JankIntervalPlotDetails[]> {
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
index 64e8784..80ee9e2 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_cause_link_utils.ts
@@ -24,7 +24,7 @@
   verticalScrollToTrack,
 } from '../../frontend/scroll_helper';
 import {SliceSqlId} from '../../frontend/sql_types';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {Anchor} from '../../widgets/anchor';
 
@@ -53,7 +53,7 @@
 }
 
 export async function getScrollJankCauseStage(
-  engine: EngineProxy,
+  engine: Engine,
   eventLatencyId: SliceSqlId,
 ): Promise<EventLatencyStage | undefined> {
   const queryResult = await engine.query(`
@@ -95,7 +95,7 @@
 }
 
 export async function getEventLatencyCauseTracks(
-  engine: EngineProxy,
+  engine: Engine,
   scrollJankCauseStage: EventLatencyStage,
 ): Promise<EventLatencyCauseThreadTracks[]> {
   const threadTracks: EventLatencyCauseThreadTracks[] = [];
@@ -130,7 +130,7 @@
 }
 
 async function getChromeCauseTracks(
-  engine: EngineProxy,
+  engine: Engine,
   eventLatencySliceId: number,
   processName: CauseProcess,
   threadName: CauseThread,
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
index c05ac43..6dab538 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_slice.ts
@@ -24,7 +24,7 @@
   constraintsToQuerySuffix,
   SQLConstraints,
 } from '../../frontend/sql_utils';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM} from '../../trace_processor/query_result';
 import {Anchor} from '../../widgets/anchor';
 
@@ -45,7 +45,7 @@
 }
 
 async function getSlicesFromTrack(
-  engine: EngineProxy,
+  engine: Engine,
   track: ScrollJankTrackSpec,
   constraints: SQLConstraints,
 ): Promise<BasicSlice[]> {
@@ -75,7 +75,7 @@
 
 export type ScrollJankSlice = BasicSlice;
 export async function getScrollJankSlices(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<ScrollJankSlice[]> {
   const track = ScrollJankPluginState.getInstance().getTrack(
@@ -93,7 +93,7 @@
 
 export type EventLatencySlice = BasicSlice;
 export async function getEventLatencySlice(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<EventLatencySlice | undefined> {
   const track = ScrollJankPluginState.getInstance().getTrack(
@@ -112,7 +112,7 @@
 }
 
 export async function getEventLatencyDescendantSlice(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
   descendant: string | undefined,
 ): Promise<EventLatencySlice | undefined> {
diff --git a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
index b117b08..646b7c6 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/scroll_jank_v3_details_panel.ts
@@ -24,7 +24,7 @@
 import {sqlValueToString} from '../../frontend/sql_utils';
 import {DurationWidget} from '../../frontend/widgets/duration';
 import {Timestamp} from '../../frontend/widgets/timestamp';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {DetailsShell} from '../../widgets/details_shell';
 import {GridLayout, GridLayoutColumn} from '../../widgets/grid_layout';
@@ -61,7 +61,7 @@
 }
 
 async function getSliceDetails(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<SliceDetails | undefined> {
   return getSlice(engine, asSliceSqlId(id));
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index b5fa5fa..07b9b1d 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -24,7 +24,7 @@
 import {globals} from '../../frontend/globals';
 import {PanelSize} from '../../frontend/panel';
 import {
-  EngineProxy,
+  Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -62,11 +62,11 @@
   private hoveredIdle: number | undefined = undefined;
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
 
-  private engine: EngineProxy;
+  private engine: Engine;
   private config: Config;
   private trackUuid = uuidv4Sql();
 
-  constructor(config: Config, engine: EngineProxy) {
+  constructor(config: Config, engine: Engine) {
     this.config = config;
     this.engine = engine;
   }
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 4d3e87f..364225d 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -24,7 +24,7 @@
 import {PanelSize} from '../../frontend/panel';
 import {TimeScale} from '../../frontend/time_scale';
 import {
-  EngineProxy,
+  Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -54,10 +54,10 @@
   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: EngineProxy;
+  private engine: Engine;
   private utid: number;
 
-  constructor(engine: EngineProxy, utid: number) {
+  constructor(engine: Engine, utid: number) {
     this.engine = engine;
     this.utid = utid;
   }
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index 139bac5..1fc1e67 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -33,7 +33,7 @@
 import {PanelSize} from '../../frontend/panel';
 import {SliceDetailsPanel} from '../../frontend/slice_details_panel';
 import {
-  EngineProxy,
+  Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -67,12 +67,12 @@
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
 
   private lastRowId = -1;
-  private engine: EngineProxy;
+  private engine: Engine;
   private cpu: number;
   private trackKey: string;
   private trackUuid = uuidv4Sql();
 
-  constructor(engine: EngineProxy, trackKey: string, cpu: number) {
+  constructor(engine: Engine, trackKey: string, cpu: number) {
     this.engine = engine;
     this.trackKey = trackKey;
     this.cpu = cpu;
@@ -490,7 +490,7 @@
     });
   }
 
-  async guessCpuSizes(engine: EngineProxy): Promise<Map<number, string>> {
+  async guessCpuSizes(engine: Engine): Promise<Map<number, string>> {
     const cpuToSize = new Map<number, string>();
     await engine.query(`
       INCLUDE PERFETTO MODULE cpu.size;
diff --git a/ui/src/core_plugins/debug/add_debug_track_menu.ts b/ui/src/core_plugins/debug/add_debug_track_menu.ts
index e03c9f2..a0c5b31 100644
--- a/ui/src/core_plugins/debug/add_debug_track_menu.ts
+++ b/ui/src/core_plugins/debug/add_debug_track_menu.ts
@@ -16,7 +16,7 @@
 
 import {findRef} from '../../base/dom_utils';
 import {raf} from '../../core/raf_scheduler';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {Form, FormLabel} from '../../widgets/form';
 import {Select} from '../../widgets/select';
 import {TextInput} from '../../widgets/text_input';
@@ -36,7 +36,7 @@
 
 interface AddDebugTrackMenuAttrs {
   dataSource: Required<SqlDataSource>;
-  engine: EngineProxy;
+  engine: Engine;
 }
 
 const TRACK_NAME_FIELD_REF = 'TRACK_NAME_FIELD';
diff --git a/ui/src/core_plugins/debug/counter_track.ts b/ui/src/core_plugins/debug/counter_track.ts
index 5b37e3d..56274d3 100644
--- a/ui/src/core_plugins/debug/counter_track.ts
+++ b/ui/src/core_plugins/debug/counter_track.ts
@@ -16,7 +16,7 @@
 
 import {BaseCounterTrack} from '../../frontend/base_counter_track';
 import {TrackContext} from '../../public';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {CounterDebugTrackConfig} from '../../frontend/debug_tracks';
 import {Disposable, DisposableCallback} from '../../base/disposable';
 import {uuidv4Sql} from '../../base/uuid';
@@ -25,7 +25,7 @@
   private config: CounterDebugTrackConfig;
   private sqlTableName: string;
 
-  constructor(engine: EngineProxy, ctx: TrackContext) {
+  constructor(engine: Engine, ctx: TrackContext) {
     super({
       engine,
       trackKey: ctx.trackKey,
diff --git a/ui/src/core_plugins/debug/slice_track.ts b/ui/src/core_plugins/debug/slice_track.ts
index a53e4dc..49e5142 100644
--- a/ui/src/core_plugins/debug/slice_track.ts
+++ b/ui/src/core_plugins/debug/slice_track.ts
@@ -14,7 +14,7 @@
 
 import {NamedSliceTrackTypes} from '../../frontend/named_slice_track';
 import {TrackContext} from '../../public';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -40,7 +40,7 @@
   private config: DebugTrackV2Config;
   private sqlTableName: string;
 
-  constructor(engine: EngineProxy, ctx: TrackContext) {
+  constructor(engine: Engine, ctx: TrackContext) {
     super({
       engine,
       trackKey: ctx.trackKey,
diff --git a/ui/src/core_plugins/frames/actual_frames_track_v2.ts b/ui/src/core_plugins/frames/actual_frames_track_v2.ts
index 6ce0e3d..10b7d72 100644
--- a/ui/src/core_plugins/frames/actual_frames_track_v2.ts
+++ b/ui/src/core_plugins/frames/actual_frames_track_v2.ts
@@ -20,7 +20,7 @@
   NamedSliceTrackTypes,
 } from '../../frontend/named_slice_track';
 import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {EngineProxy, Slice, STR_NULL} from '../../public';
+import {Engine, Slice, STR_NULL} from '../../public';
 
 // color named and defined based on Material Design color palettes
 // 500 colors indicate a timeline slice is not a partial jank (not a jank or
@@ -54,7 +54,7 @@
 
 export class ActualFramesTrack extends NamedSliceTrack<ActualFrameTrackTypes> {
   constructor(
-    engine: EngineProxy,
+    engine: Engine,
     maxDepth: number,
     trackKey: string,
     private trackIds: number[],
diff --git a/ui/src/core_plugins/frames/expected_frames_track_v2.ts b/ui/src/core_plugins/frames/expected_frames_track_v2.ts
index ce602a1..e9cce12 100644
--- a/ui/src/core_plugins/frames/expected_frames_track_v2.ts
+++ b/ui/src/core_plugins/frames/expected_frames_track_v2.ts
@@ -16,13 +16,13 @@
 import {makeColorScheme} from '../../core/colorizer';
 import {NamedRow, NamedSliceTrack} from '../../frontend/named_slice_track';
 import {SLICE_LAYOUT_FIT_CONTENT_DEFAULTS} from '../../frontend/slice_layout';
-import {EngineProxy, Slice} from '../../public';
+import {Engine, Slice} from '../../public';
 
 const GREEN = makeColorScheme(new HSLColor('#4CAF50')); // Green 500
 
 export class ExpectedFramesTrack extends NamedSliceTrack {
   constructor(
-    engine: EngineProxy,
+    engine: Engine,
     maxDepth: number,
     trackKey: string,
     private trackIds: number[],
diff --git a/ui/src/core_plugins/ftrace/ftrace_explorer.ts b/ui/src/core_plugins/ftrace/ftrace_explorer.ts
index d8cad78..e034183 100644
--- a/ui/src/core_plugins/ftrace/ftrace_explorer.ts
+++ b/ui/src/core_plugins/ftrace/ftrace_explorer.ts
@@ -28,7 +28,7 @@
 import {globals} from '../../frontend/globals';
 import {Timestamp} from '../../frontend/widgets/timestamp';
 import {FtraceFilter, FtraceStat} from './common';
-import {EngineProxy, LONG, NUM, Store, STR, STR_NULL} from '../../public';
+import {Engine, LONG, NUM, Store, STR, STR_NULL} from '../../public';
 import {raf} from '../../core/raf_scheduler';
 import {AsyncLimiter} from '../../base/async_limiter';
 import {Monitor} from '../../base/monitor';
@@ -40,7 +40,7 @@
 interface FtraceExplorerAttrs {
   cache: FtraceExplorerCache;
   filterStore: Store<FtraceFilter>;
-  engine: EngineProxy;
+  engine: Engine;
 }
 
 interface FtraceEvent {
@@ -69,7 +69,7 @@
   counters: FtraceStat[];
 }
 
-async function getFtraceCounters(engine: EngineProxy): Promise<FtraceStat[]> {
+async function getFtraceCounters(engine: Engine): Promise<FtraceStat[]> {
   // TODO(stevegolton): this is an extraordinarily slow query on large traces
   // as it goes through every ftrace event which can be a lot on big traces.
   // Consider if we can have some different UX which avoids needing these
@@ -264,7 +264,7 @@
 }
 
 async function lookupFtraceEvents(
-  engine: EngineProxy,
+  engine: Engine,
   offset: number,
   count: number,
   filter: FtraceFilter,
diff --git a/ui/src/core_plugins/ftrace/ftrace_track.ts b/ui/src/core_plugins/ftrace/ftrace_track.ts
index 6950eca..ed55cf6 100644
--- a/ui/src/core_plugins/ftrace/ftrace_track.ts
+++ b/ui/src/core_plugins/ftrace/ftrace_track.ts
@@ -20,7 +20,7 @@
 import {globals} from '../../frontend/globals';
 import {TrackData} from '../../common/track_data';
 import {PanelSize} from '../../frontend/panel';
-import {EngineProxy, Track} from '../../public';
+import {Engine, Track} from '../../public';
 import {LONG, STR} from '../../trace_processor/query_result';
 import {FtraceFilter} from './common';
 import {Store} from '../../public';
@@ -41,12 +41,12 @@
 
 export class FtraceRawTrack implements Track {
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
-  private engine: EngineProxy;
+  private engine: Engine;
   private cpu: number;
   private store: Store<FtraceFilter>;
   private readonly monitor: Monitor;
 
-  constructor(engine: EngineProxy, cpu: number, store: Store<FtraceFilter>) {
+  constructor(engine: Engine, cpu: number, store: Store<FtraceFilter>) {
     this.engine = engine;
     this.cpu = cpu;
     this.store = store;
diff --git a/ui/src/core_plugins/ftrace/index.ts b/ui/src/core_plugins/ftrace/index.ts
index 3cc0616..9539c78 100644
--- a/ui/src/core_plugins/ftrace/index.ts
+++ b/ui/src/core_plugins/ftrace/index.ts
@@ -16,7 +16,7 @@
 
 import {FtraceExplorer, FtraceExplorerCache} from './ftrace_explorer';
 import {
-  EngineProxy,
+  Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -108,7 +108,7 @@
     this.trash.dispose();
   }
 
-  private async lookupCpuCores(engine: EngineProxy): Promise<number[]> {
+  private async lookupCpuCores(engine: Engine): Promise<number[]> {
     const query = 'select distinct cpu from ftrace_event';
 
     const result = await engine.query(query);
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 752684a..319fb25 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -24,7 +24,7 @@
 import {PanelSize} from '../../frontend/panel';
 import {TimeScale} from '../../frontend/time_scale';
 import {
-  EngineProxy,
+  Engine,
   Plugin,
   PluginContextTrace,
   PluginDescriptor,
@@ -50,9 +50,9 @@
   private hoveredTs: time | undefined = undefined;
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
   private upid: number;
-  private engine: EngineProxy;
+  private engine: Engine;
 
-  constructor(engine: EngineProxy, upid: number) {
+  constructor(engine: Engine, upid: number) {
     this.upid = upid;
     this.engine = engine;
   }
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 9725c29..28b536f 100644
--- a/ui/src/core_plugins/process_summary/process_scheduling_track.ts
+++ b/ui/src/core_plugins/process_summary/process_scheduling_track.ts
@@ -25,7 +25,7 @@
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
 import {PanelSize} from '../../frontend/panel';
-import {EngineProxy, Track} from '../../public';
+import {Engine, Track} from '../../public';
 import {LONG, NUM, QueryResult} from '../../trace_processor/query_result';
 import {uuidv4Sql} from '../../base/uuid';
 
@@ -57,11 +57,11 @@
   private utidHoveredInThisTrack = -1;
   private fetcher = new TimelineFetcher(this.onBoundsChange.bind(this));
   private maxCpu = 0;
-  private engine: EngineProxy;
+  private engine: Engine;
   private trackUuid = uuidv4Sql();
   private config: Config;
 
-  constructor(engine: EngineProxy, config: Config) {
+  constructor(engine: Engine, config: Config) {
     this.engine = engine;
     this.config = config;
   }
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 7acf36b..5fa31de 100644
--- a/ui/src/core_plugins/process_summary/process_summary_track.ts
+++ b/ui/src/core_plugins/process_summary/process_summary_track.ts
@@ -19,11 +19,11 @@
 import {duration, Time, time} from '../../base/time';
 import {colorForTid} from '../../core/colorizer';
 import {LIMIT, TrackData} from '../../common/track_data';
-import {EngineProxy, TimelineFetcher} from '../../common/track_helper';
+import {TimelineFetcher} from '../../common/track_helper';
 import {checkerboardExcept} from '../../frontend/checkerboard';
 import {globals} from '../../frontend/globals';
 import {PanelSize} from '../../frontend/panel';
-import {Track} from '../../public';
+import {Engine, Track} from '../../public';
 import {NUM} from '../../trace_processor/query_result';
 
 export const PROCESS_SUMMARY_TRACK = 'ProcessSummaryTrack';
@@ -47,11 +47,11 @@
 
 export class ProcessSummaryTrack implements Track {
   private fetcher = new TimelineFetcher<Data>(this.onBoundsChange.bind(this));
-  private engine: EngineProxy;
+  private engine: Engine;
   private uuid = uuidv4();
   private config: Config;
 
-  constructor(engine: EngineProxy, config: Config) {
+  constructor(engine: Engine, config: Config) {
     this.engine = engine;
     this.config = config;
   }
diff --git a/ui/src/core_plugins/sched/active_cpu_count.ts b/ui/src/core_plugins/sched/active_cpu_count.ts
index 721a884..2ab49bc 100644
--- a/ui/src/core_plugins/sched/active_cpu_count.ts
+++ b/ui/src/core_plugins/sched/active_cpu_count.ts
@@ -25,7 +25,7 @@
 } from '../../frontend/base_counter_track';
 import {CloseTrackButton} from '../../frontend/close_track_button';
 import {globals} from '../../frontend/globals';
-import {EngineProxy, PrimaryTrackSortKey, TrackContext} from '../../public';
+import {Engine, PrimaryTrackSortKey, TrackContext} from '../../public';
 
 export function addActiveCPUCountTrack(cpuType?: string) {
   const cpuTypeName = cpuType === undefined ? '' : ` ${cpuType} `;
@@ -56,7 +56,7 @@
 
   static readonly kind = 'dev.perfetto.Sched.ActiveCPUCount';
 
-  constructor(ctx: TrackContext, engine: EngineProxy) {
+  constructor(ctx: TrackContext, engine: Engine) {
     super({
       engine,
       trackKey: ctx.trackKey,
diff --git a/ui/src/core_plugins/screenshots/screenshot_panel.ts b/ui/src/core_plugins/screenshots/screenshot_panel.ts
index 066d14c..387ebf6 100644
--- a/ui/src/core_plugins/screenshots/screenshot_panel.ts
+++ b/ui/src/core_plugins/screenshots/screenshot_panel.ts
@@ -20,10 +20,10 @@
 import {GenericSliceDetailsTabConfig} from '../../frontend/generic_slice_details_tab';
 import {getSlice, SliceDetails} from '../../frontend/sql/slice';
 import {asSliceSqlId} from '../../frontend/sql_types';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 
 async function getSliceDetails(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<SliceDetails | undefined> {
   return getSlice(engine, asSliceSqlId(id));
diff --git a/ui/src/frontend/app.ts b/ui/src/frontend/app.ts
index 92301b7..09b1036 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -32,7 +32,7 @@
 } from '../core/timestamp_format';
 import {raf} from '../core/raf_scheduler';
 import {Command} from '../public';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
 import {HotkeyConfig, HotkeyContext} from '../widgets/hotkey_context';
 import {HotkeyGlyphs} from '../widgets/hotkey_glyphs';
@@ -155,7 +155,7 @@
     this.trash.add(new AggregationsTabs());
   }
 
-  private getEngine(): EngineProxy | undefined {
+  private getEngine(): Engine | undefined {
     const engineId = globals.getCurrentEngine()?.id;
     if (engineId === undefined) {
       return undefined;
@@ -620,8 +620,8 @@
         if (selection !== null && selection.kind === 'AREA') {
           const area = globals.state.areas[selection.areaId];
           const coversEntireTimeRange =
-            globals.state.traceTime.start === area.start &&
-            globals.state.traceTime.end === area.end;
+            globals.traceTime.start === area.start &&
+            globals.traceTime.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 +636,7 @@
           // If the current selection is not an area, select all.
           tracksToSelect = Object.keys(globals.state.tracks);
         }
-        const {start, end} = globals.state.traceTime;
+        const {start, end} = globals.traceTime;
         globals.dispatch(
           Actions.selectArea({
             area: {
diff --git a/ui/src/frontend/base_counter_track.ts b/ui/src/frontend/base_counter_track.ts
index 990e6a3..9a37a6a 100644
--- a/ui/src/frontend/base_counter_track.ts
+++ b/ui/src/frontend/base_counter_track.ts
@@ -20,7 +20,7 @@
 import {Time, time} from '../base/time';
 import {drawTrackHoverTooltip} from '../common/canvas_utils';
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy, LONG, NUM, Track} from '../public';
+import {Engine, LONG, NUM, Track} from '../public';
 import {Button} from '../widgets/button';
 import {MenuItem, MenuDivider, PopupMenu2} from '../widgets/menu';
 
@@ -194,7 +194,7 @@
 };
 
 export abstract class BaseCounterTrack implements Track {
-  protected engine: EngineProxy;
+  protected engine: Engine;
   protected trackKey: string;
   protected trackUuid = uuidv4Sql();
 
diff --git a/ui/src/frontend/base_slice_track.ts b/ui/src/frontend/base_slice_track.ts
index 7e013cc..720ace2 100644
--- a/ui/src/frontend/base_slice_track.ts
+++ b/ui/src/frontend/base_slice_track.ts
@@ -32,7 +32,7 @@
 } from '../common/state';
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy, Slice, SliceRect, Track} from '../public';
+import {Engine, Slice, SliceRect, Track} from '../public';
 import {LONG, NUM} from '../trace_processor/query_result';
 
 import {checkerboardExcept} from './checkerboard';
@@ -175,7 +175,7 @@
 > implements Track
 {
   protected sliceLayout: SliceLayout = {...DEFAULT_SLICE_LAYOUT};
-  protected engine: EngineProxy;
+  protected engine: Engine;
   protected trackKey: string;
   protected trackUuid = uuidv4Sql();
 
diff --git a/ui/src/frontend/bottom_tab.ts b/ui/src/frontend/bottom_tab.ts
index d8ed117..9868a60 100644
--- a/ui/src/frontend/bottom_tab.ts
+++ b/ui/src/frontend/bottom_tab.ts
@@ -14,10 +14,10 @@
 
 import m from 'mithril';
 
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 
 export interface NewBottomTabArgs<Config> {
-  engine: EngineProxy;
+  engine: Engine;
   tag?: string;
   uuid: string;
   config: Config;
@@ -43,7 +43,7 @@
   // Config for this details panel. Should be serializable.
   protected readonly config: Config;
   // Engine for running queries and fetching additional data.
-  protected readonly engine: EngineProxy;
+  protected readonly engine: Engine;
   // Optional tag, which is used to ensure that only one tab
   // with the same tag can exist - adding a new tab with the same tag
   // (e.g. 'current_selection') would close the previous one. This
diff --git a/ui/src/frontend/chrome_slice_details_tab.ts b/ui/src/frontend/chrome_slice_details_tab.ts
index 49196fa..91cf54f 100644
--- a/ui/src/frontend/chrome_slice_details_tab.ts
+++ b/ui/src/frontend/chrome_slice_details_tab.ts
@@ -19,7 +19,7 @@
 import {exists} from '../base/utils';
 import {runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {LONG, LONG_NULL, NUM, STR_NULL} from '../trace_processor/query_result';
 import {Button} from '../widgets/button';
 import {DetailsShell} from '../widgets/details_shell';
@@ -167,7 +167,7 @@
   return ITEMS.filter((item) => item.shouldDisplay(slice));
 }
 
-function getEngine(): EngineProxy | undefined {
+function getEngine(): Engine | undefined {
   const engineId = globals.getCurrentEngine()?.id;
   if (engineId === undefined) {
     return undefined;
@@ -177,7 +177,7 @@
 }
 
 async function getAnnotationSlice(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<SliceDetails | undefined> {
   const query = await engine.query(`
@@ -218,7 +218,7 @@
 }
 
 async function getSliceDetails(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
   table: string,
 ): Promise<SliceDetails | undefined> {
@@ -300,7 +300,7 @@
     return !exists(this.sliceDetails);
   }
 
-  private renderRhs(engine: EngineProxy, slice: SliceDetails): m.Children {
+  private renderRhs(engine: Engine, slice: SliceDetails): m.Children {
     const precFlows = this.renderPrecedingFlows(slice);
     const followingFlows = this.renderFollowingFlows(slice);
     const args =
diff --git a/ui/src/frontend/debug_tracks.ts b/ui/src/frontend/debug_tracks.ts
index ed77bf0..7b3fe11 100644
--- a/ui/src/frontend/debug_tracks.ts
+++ b/ui/src/frontend/debug_tracks.ts
@@ -16,7 +16,7 @@
 import {Actions, DeferredAction} from '../common/actions';
 import {SCROLLING_TRACK_GROUP} from '../common/state';
 import {globals} from './globals';
-import {EngineProxy, PrimaryTrackSortKey} from '../public';
+import {Engine, PrimaryTrackSortKey} from '../public';
 import {DebugTrackV2Config} from '../core_plugins/debug/slice_track';
 
 export const ARG_PREFIX = 'arg_';
@@ -55,7 +55,7 @@
 // once or want to tweak the actions once produced. Otherwise, use
 // addDebugSliceTrack().
 export async function createDebugSliceTrackActions(
-  _engine: EngineProxy,
+  _engine: Engine,
   data: SqlDataSource,
   trackName: string,
   sliceColumns: SliceColumns,
@@ -90,7 +90,7 @@
 }
 
 export async function addPivotDebugSliceTracks(
-  engine: EngineProxy,
+  engine: Engine,
   data: SqlDataSource,
   trackName: string,
   sliceColumns: SliceColumns,
@@ -129,7 +129,7 @@
 // Adds a debug track immediately. Use createDebugSliceTrackActions() if you
 // want to create many tracks at once.
 export async function addDebugSliceTrack(
-  engine: EngineProxy,
+  engine: Engine,
   data: SqlDataSource,
   trackName: string,
   sliceColumns: SliceColumns,
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index 50b2286..04cdc17 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -44,7 +44,7 @@
 import {TrackManager} from '../common/track_cache';
 import {setPerfHooks} from '../core/perf';
 import {raf} from '../core/raf_scheduler';
-import {Engine} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
 import {HttpRpcState} from '../trace_processor/http_rpc_engine';
 
 import {Analytics, initAnalytics} from './analytics';
@@ -221,6 +221,32 @@
   pendingScrollId: number | undefined;
 }
 
+export interface TraceTime {
+  readonly start: time;
+  readonly end: time;
+
+  // This is the ts value at the time of the Unix epoch.
+  // Normally some large negative value, because the unix epoch is normally in
+  // the past compared to ts=0.
+  readonly realtimeOffset: time;
+
+  // This is the timestamp that we should use for our offset when in UTC mode.
+  // Usually the most recent UTC midnight compared to the trace start time.
+  readonly utcOffset: time;
+
+  // 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;
+}
+
+export const defaultTraceTime: TraceTime = {
+  start: Time.ZERO,
+  end: Time.fromSeconds(10),
+  realtimeOffset: Time.ZERO,
+  utcOffset: Time.ZERO,
+  traceTzOffset: Time.ZERO,
+};
+
 /**
  * Global accessors for state/dispatch in the frontend.
  */
@@ -260,9 +286,6 @@
   private _embeddedMode?: boolean = undefined;
   private _hideSidebar?: boolean = undefined;
   private _cmdManager = new CommandManager();
-  private _realtimeOffset = Time.ZERO;
-  private _utcOffset = Time.ZERO;
-  private _traceTzOffset = Time.ZERO;
   private _tabManager = new TabManager();
   private _trackManager = new TrackManager(this._store);
   private _selectionManager = new SelectionManager(this._store);
@@ -273,6 +296,8 @@
   newVersionAvailable = false;
   showPanningHint = false;
 
+  traceTime = defaultTraceTime;
+
   // TODO(hjd): Remove once we no longer need to update UUID on redraw.
   private _publishRedraw?: () => void = undefined;
 
@@ -290,7 +315,7 @@
     count: new Uint8Array(0),
   };
 
-  engines = new Map<string, Engine>();
+  engines = new Map<string, EngineBase>();
 
   initialize(dispatch: Dispatch, router: Router) {
     this._dispatch = dispatch;
@@ -691,19 +716,19 @@
 
   // Get a timescale that covers the entire trace
   getTraceTimeScale(pxSpan: PxSpan): TimeScale {
-    const {start, end} = this.state.traceTime;
+    const {start, end} = this.traceTime;
     const traceTime = HighPrecisionTimeSpan.fromTime(start, end);
     return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
   }
 
   // Get the trace time bounds
   stateTraceTime(): Span<HighPrecisionTime> {
-    const {start, end} = this.state.traceTime;
+    const {start, end} = this.traceTime;
     return HighPrecisionTimeSpan.fromTime(start, end);
   }
 
   stateTraceTimeTP(): Span<time, duration> {
-    const {start, end} = this.state.traceTime;
+    const {start, end} = this.traceTime;
     return new TimeSpan(start, end);
   }
 
@@ -723,37 +748,6 @@
     return assertExists(this._cmdManager);
   }
 
-  // This is the ts value at the time of the Unix epoch.
-  // Normally some large negative value, because the unix epoch is normally in
-  // the past compared to ts=0.
-  get realtimeOffset(): time {
-    return this._realtimeOffset;
-  }
-
-  set realtimeOffset(time: time) {
-    this._realtimeOffset = time;
-  }
-
-  // This is the timestamp that we should use for our offset when in UTC mode.
-  // Usually the most recent UTC midnight compared to the trace start time.
-  get utcOffset(): time {
-    return this._utcOffset;
-  }
-
-  set utcOffset(offset: time) {
-    this._utcOffset = offset;
-  }
-
-  // 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.
-  get traceTzOffset(): time {
-    return this._traceTzOffset;
-  }
-
-  set traceTzOffset(offset: time) {
-    this._traceTzOffset = offset;
-  }
-
   get tabManager() {
     return this._tabManager;
   }
@@ -768,14 +762,14 @@
     switch (fmt) {
       case TimestampFormat.Timecode:
       case TimestampFormat.Seconds:
-        return this.state.traceTime.start;
+        return this.traceTime.start;
       case TimestampFormat.Raw:
       case TimestampFormat.RawLocale:
         return Time.ZERO;
       case TimestampFormat.UTC:
-        return this.utcOffset;
+        return this.traceTime.utcOffset;
       case TimestampFormat.TraceTz:
-        return this.traceTzOffset;
+        return this.traceTime.traceTzOffset;
       default:
         const x: never = fmt;
         throw new Error(`Unsupported format ${x}`);
diff --git a/ui/src/frontend/metrics_page.ts b/ui/src/frontend/metrics_page.ts
index 6b7ffce..124cb84 100644
--- a/ui/src/frontend/metrics_page.ts
+++ b/ui/src/frontend/metrics_page.ts
@@ -25,7 +25,7 @@
 import {pluginManager, PluginManager} from '../common/plugins';
 import {raf} from '../core/raf_scheduler';
 import {MetricVisualisation} from '../public';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {STR} from '../trace_processor/query_result';
 import {Select} from '../widgets/select';
 import {Spinner} from '../widgets/spinner';
@@ -37,7 +37,7 @@
 type Format = 'json' | 'prototext' | 'proto';
 const FORMATS: Format[] = ['json', 'prototext', 'proto'];
 
-function getEngine(): EngineProxy | undefined {
+function getEngine(): Engine | undefined {
   const engineId = globals.getCurrentEngine()?.id;
   if (engineId === undefined) {
     return undefined;
@@ -46,7 +46,7 @@
   return engine;
 }
 
-async function getMetrics(engine: EngineProxy): Promise<string[]> {
+async function getMetrics(engine: Engine): Promise<string[]> {
   const metrics: string[] = [];
   const metricsResult = await engine.query('select name from trace_metrics');
   for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
@@ -56,7 +56,7 @@
 }
 
 async function getMetric(
-  engine: EngineProxy,
+  engine: Engine,
   metric: string,
   format: Format,
 ): Promise<string> {
@@ -69,7 +69,7 @@
 }
 
 class MetricsController {
-  engine: EngineProxy;
+  engine: Engine;
   plugins: PluginManager;
   private _metrics: string[];
   private _selected?: string;
@@ -78,7 +78,7 @@
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
   private _json: any;
 
-  constructor(plugins: PluginManager, engine: EngineProxy) {
+  constructor(plugins: PluginManager, engine: Engine) {
     this.plugins = plugins;
     this.engine = engine;
     this._metrics = [];
diff --git a/ui/src/frontend/publish.ts b/ui/src/frontend/publish.ts
index f8c36cb..bfb34ca 100644
--- a/ui/src/frontend/publish.ts
+++ b/ui/src/frontend/publish.ts
@@ -12,7 +12,6 @@
 // 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 {AggregateData} from '../common/aggregation_data';
 import {ConversionJobStatusUpdate} from '../common/conversion_jobs';
@@ -32,6 +31,7 @@
   SliceDetails,
   ThreadDesc,
   ThreadStateDetails,
+  TraceTime,
 } from './globals';
 import {findCurrentSelection} from './keyboard_event_handler';
 
@@ -96,14 +96,8 @@
   globals.publishRedraw();
 }
 
-export function publishRealtimeOffset(
-  offset: time,
-  utcOffset: time,
-  traceTzOffset: time,
-) {
-  globals.realtimeOffset = offset;
-  globals.utcOffset = utcOffset;
-  globals.traceTzOffset = traceTzOffset;
+export function publishTraceDetails(details: TraceTime): void {
+  globals.traceTime = details;
   globals.publishRedraw();
 }
 
diff --git a/ui/src/frontend/query_page.ts b/ui/src/frontend/query_page.ts
index 7afcbaf..80b8858 100644
--- a/ui/src/frontend/query_page.ts
+++ b/ui/src/frontend/query_page.ts
@@ -19,7 +19,7 @@
 import {undoCommonChatAppReplacements} from '../base/string_utils';
 import {QueryResponse, runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {Callout} from '../widgets/callout';
 import {Editor} from '../widgets/editor';
 
@@ -71,7 +71,7 @@
   raf.scheduleDelayedFullRedraw();
 }
 
-function getEngine(): EngineProxy | undefined {
+function getEngine(): Engine | undefined {
   const engineId = globals.getCurrentEngine()?.id;
   if (engineId === undefined) {
     return undefined;
diff --git a/ui/src/frontend/query_result_tab.ts b/ui/src/frontend/query_result_tab.ts
index 7fe7b2d..e6feda3 100644
--- a/ui/src/frontend/query_result_tab.ts
+++ b/ui/src/frontend/query_result_tab.ts
@@ -32,7 +32,7 @@
 import {globals} from './globals';
 import {Actions} from '../common/actions';
 import {BottomTabToTabAdapter} from '../public/utils';
-import {EngineProxy} from '../public';
+import {Engine} from '../public';
 
 interface QueryResultTabConfig {
   readonly query: string;
@@ -66,7 +66,7 @@
 }
 
 // TODO(stevegolton): Find a way to make this more elegant.
-function getEngine(): EngineProxy {
+function getEngine(): Engine {
   const engConfig = globals.getCurrentEngine();
   const engineId = assertExists(engConfig).id;
   return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
diff --git a/ui/src/frontend/sidebar.ts b/ui/src/frontend/sidebar.ts
index be1d20e..1cedf6a 100644
--- a/ui/src/frontend/sidebar.ts
+++ b/ui/src/frontend/sidebar.ts
@@ -29,7 +29,7 @@
 import {featureFlags} from '../core/feature_flags';
 import {raf} from '../core/raf_scheduler';
 import {SCM_REVISION, VERSION} from '../gen/perfetto_version';
-import {Engine} from '../trace_processor/engine';
+import {EngineBase} from '../trace_processor/engine';
 import {showModal} from '../widgets/modal';
 
 import {Animation} from './animation';
@@ -566,7 +566,7 @@
   downloadUrl(fileName, url);
 }
 
-function getCurrentEngine(): Engine | undefined {
+function getCurrentEngine(): EngineBase | undefined {
   const engineId = globals.getCurrentEngine()?.id;
   if (engineId === undefined) return undefined;
   return globals.engines.get(engineId);
diff --git a/ui/src/frontend/simple_counter_track.ts b/ui/src/frontend/simple_counter_track.ts
index 361480b..5b21ded 100644
--- a/ui/src/frontend/simple_counter_track.ts
+++ b/ui/src/frontend/simple_counter_track.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import m from 'mithril';
-import {EngineProxy, TrackContext} from '../public';
+import {Engine, TrackContext} from '../public';
 import {BaseCounterTrack, CounterOptions} from './base_counter_track';
 import {CounterColumns, SqlDataSource} from './debug_tracks';
 import {Disposable, DisposableCallback} from '../base/disposable';
@@ -30,7 +30,7 @@
   private sqlTableName: string;
 
   constructor(
-    engine: EngineProxy,
+    engine: Engine,
     ctx: TrackContext,
     config: SimpleCounterTrackConfig,
   ) {
diff --git a/ui/src/frontend/simple_slice_track.ts b/ui/src/frontend/simple_slice_track.ts
index f46812f..c292fe6 100644
--- a/ui/src/frontend/simple_slice_track.ts
+++ b/ui/src/frontend/simple_slice_track.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy, TrackContext} from '../public';
+import {Engine, TrackContext} from '../public';
 import {
   CustomSqlDetailsPanelConfig,
   CustomSqlTableDefConfig,
@@ -35,7 +35,7 @@
   private sqlTableName: string;
 
   constructor(
-    engine: EngineProxy,
+    engine: Engine,
     ctx: TrackContext,
     config: SimpleSliceTrackConfig,
   ) {
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 97b3b6f..ec9d609 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -22,7 +22,7 @@
 import {Actions, AddTrackArgs} from '../common/actions';
 import {InThreadTrackSortKey} from '../common/state';
 import {ArgNode, convertArgsToTree, Key} from '../controller/args_parser';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {NUM} from '../trace_processor/query_result';
 import {
   VISUALISED_ARGS_SLICE_TRACK_URI,
@@ -39,7 +39,7 @@
 import {assertExists} from '../base/logging';
 
 // Renders slice arguments (key/value pairs) as a subtree.
-export function renderArguments(engine: EngineProxy, args: Arg[]): m.Children {
+export function renderArguments(engine: Engine, args: Arg[]): m.Children {
   if (args.length > 0) {
     const tree = convertArgsToTree(args);
     return renderArgTreeNodes(engine, tree);
@@ -52,10 +52,7 @@
   return exists(args) && args.length > 0;
 }
 
-function renderArgTreeNodes(
-  engine: EngineProxy,
-  args: ArgNode<Arg>[],
-): m.Children {
+function renderArgTreeNodes(engine: Engine, args: ArgNode<Arg>[]): m.Children {
   return args.map((arg) => {
     const {key, value, children} = arg;
     if (children && children.length === 1) {
@@ -80,11 +77,7 @@
   });
 }
 
-function renderArgKey(
-  engine: EngineProxy,
-  key: string,
-  value?: Arg,
-): m.Children {
+function renderArgKey(engine: Engine, key: string, value?: Arg): m.Children {
   if (value === undefined) {
     return key;
   } else {
@@ -125,7 +118,7 @@
   }
 }
 
-async function addVisualisedArg(engine: EngineProxy, argName: string) {
+async function addVisualisedArg(engine: Engine, argName: string) {
   const escapedArgName = argName.replace(/[^a-zA-Z]/g, '_');
   const tableName = `__arg_visualisation_helper_${escapedArgName}_slice`;
 
diff --git a/ui/src/frontend/sql/args.ts b/ui/src/frontend/sql/args.ts
index 2cf051c..f36f253 100644
--- a/ui/src/frontend/sql/args.ts
+++ b/ui/src/frontend/sql/args.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {
   LONG_NULL,
   NUM,
@@ -42,7 +42,7 @@
 }
 
 export async function getArgs(
-  engine: EngineProxy,
+  engine: Engine,
   argSetId: ArgSetId,
 ): Promise<Arg[]> {
   const query = await engine.query(`
diff --git a/ui/src/frontend/sql/details/details.ts b/ui/src/frontend/sql/details/details.ts
index 42b500d..d244747 100644
--- a/ui/src/frontend/sql/details/details.ts
+++ b/ui/src/frontend/sql/details/details.ts
@@ -18,7 +18,7 @@
 import {Time} from '../../../base/time';
 import {exists} from '../../../base/utils';
 import {raf} from '../../../core/raf_scheduler';
-import {EngineProxy} from '../../../public';
+import {Engine} from '../../../public';
 import {Row, SqlValue} from '../../../trace_processor/query_result';
 import {Anchor} from '../../../widgets/anchor';
 import {renderError} from '../../../widgets/error';
@@ -202,7 +202,7 @@
 // Class responsible for fetching the data and rendering the data.
 export class Details {
   constructor(
-    private engine: EngineProxy,
+    private engine: Engine,
     private sqlTable: string,
     private id: number,
     schema: {[key: string]: ValueDesc},
@@ -278,14 +278,14 @@
 // async `fetch` step for fetching data and sync `render` step for generating
 // the vdom.
 export type SqlIdRefRenderer = {
-  fetch: (engine: EngineProxy, id: bigint) => Promise<{} | undefined>;
+  fetch: (engine: Engine, id: bigint) => Promise<{} | undefined>;
   render: (data: {}) => RenderedValue;
 };
 
 // Type-safe helper to create a SqlIdRefRenderer, which ensures that the
 // type returned from the fetch is the same type that renderer takes.
 export function createSqlIdRefRenderer<Data extends {}>(
-  fetch: (engine: EngineProxy, id: bigint) => Promise<Data>,
+  fetch: (engine: Engine, id: bigint) => Promise<Data>,
   render: (data: Data) => RenderedValue,
 ): SqlIdRefRenderer {
   return {fetch, render: render as (data: {}) => RenderedValue};
@@ -451,7 +451,7 @@
   data?: Data;
 
   constructor(
-    private engine: EngineProxy,
+    private engine: Engine,
     private sqlTable: string,
     private id: number,
     public sqlIdRefRenderers: {[table: string]: SqlIdRefRenderer},
@@ -652,7 +652,7 @@
 
 // Generate the vdom for a given value using the fetched `data`.
 function renderValue(
-  engine: EngineProxy,
+  engine: Engine,
   key: string,
   value: ResolvedValue,
   data: Data,
diff --git a/ui/src/frontend/sql/slice.ts b/ui/src/frontend/sql/slice.ts
index 050ec4e..bf35774 100644
--- a/ui/src/frontend/sql/slice.ts
+++ b/ui/src/frontend/sql/slice.ts
@@ -18,7 +18,7 @@
 import {Icons} from '../../base/semantic_icons';
 import {duration, Time, time} from '../../base/time';
 import {exists} from '../../base/utils';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {
   LONG,
   LONG_NULL,
@@ -68,7 +68,7 @@
 }
 
 async function getUtidAndUpid(
-  engine: EngineProxy,
+  engine: Engine,
   sqlTrackId: number,
 ): Promise<{utid?: Utid; upid?: Upid}> {
   const columnInfo = (
@@ -118,7 +118,7 @@
 }
 
 export async function getSliceFromConstraints(
-  engine: EngineProxy,
+  engine: Engine,
   constraints: SQLConstraints,
 ): Promise<SliceDetails[]> {
   const query = await engine.query(`
@@ -186,7 +186,7 @@
 }
 
 export async function getSlice(
-  engine: EngineProxy,
+  engine: Engine,
   id: SliceSqlId,
 ): Promise<SliceDetails | undefined> {
   const result = await getSliceFromConstraints(engine, {
@@ -272,7 +272,7 @@
 
 // Get all descendants for a given slice in a tree form.
 export async function getDescendantSliceTree(
-  engine: EngineProxy,
+  engine: Engine,
   id: SliceSqlId,
 ): Promise<SliceTreeNode | undefined> {
   const slice = await getSlice(engine, id);
diff --git a/ui/src/frontend/sql/thread_state.ts b/ui/src/frontend/sql/thread_state.ts
index e47a5c1..7009a6f 100644
--- a/ui/src/frontend/sql/thread_state.ts
+++ b/ui/src/frontend/sql/thread_state.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 
 import {duration, TimeSpan} from '../../base/time';
-import {EngineProxy} from '../../public';
+import {Engine} from '../../public';
 import {
   LONG,
   NUM_NULL,
@@ -67,7 +67,7 @@
 // Compute a breakdown of thread states for a given thread for a given time
 // interval.
 export async function breakDownIntervalByThreadState(
-  engine: EngineProxy,
+  engine: Engine,
   range: TimeSpan,
   utid: Utid,
 ): Promise<BreakdownByThreadState> {
diff --git a/ui/src/frontend/sql_table/argument_selector.ts b/ui/src/frontend/sql_table/argument_selector.ts
index 29b4281..039cc0a 100644
--- a/ui/src/frontend/sql_table/argument_selector.ts
+++ b/ui/src/frontend/sql_table/argument_selector.ts
@@ -15,7 +15,7 @@
 import m from 'mithril';
 
 import {raf} from '../../core/raf_scheduler';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {STR} from '../../trace_processor/query_result';
 import {FilterableSelect} from '../../widgets/select';
 import {Spinner} from '../../widgets/spinner';
@@ -31,7 +31,7 @@
 const MAX_ARGS_TO_DISPLAY = 15;
 
 interface ArgumentSelectorAttrs {
-  engine: EngineProxy;
+  engine: Engine;
   argSetId: ArgSetIdColumn;
   tableName: string;
   constraints: SQLConstraints;
diff --git a/ui/src/frontend/sql_table/state.ts b/ui/src/frontend/sql_table/state.ts
index 96118f5..2093dad 100644
--- a/ui/src/frontend/sql_table/state.ts
+++ b/ui/src/frontend/sql_table/state.ts
@@ -17,7 +17,7 @@
 import {isString} from '../../base/object_utils';
 import {sqliteString} from '../../base/string_utils';
 import {raf} from '../../core/raf_scheduler';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {NUM, Row} from '../../trace_processor/query_result';
 import {
   constraintsToQueryPrefix,
@@ -76,7 +76,7 @@
 }
 
 export class SqlTableState {
-  private readonly engine_: EngineProxy;
+  private readonly engine_: Engine;
   private readonly table_: SqlTableDescription;
   private readonly additionalImports: string[];
 
@@ -95,7 +95,7 @@
   private rowCount?: RowCount;
 
   constructor(
-    engine: EngineProxy,
+    engine: Engine,
     table: SqlTableDescription,
     filters?: Filter[],
     imports?: string[],
diff --git a/ui/src/frontend/sql_table/state_unittest.ts b/ui/src/frontend/sql_table/state_unittest.ts
index 91d96a2..ebb20b5 100644
--- a/ui/src/frontend/sql_table/state_unittest.ts
+++ b/ui/src/frontend/sql_table/state_unittest.ts
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {Engine, EngineProxy} from '../../trace_processor/engine';
+import {EngineBase} from '../../trace_processor/engine';
 
 import {Column} from './column';
 import {SqlTableState} from './state';
@@ -44,14 +44,14 @@
   ],
 };
 
-class FakeEngine extends Engine {
+class FakeEngine extends EngineBase {
   id: string = 'TestEngine';
 
   rpcSendRequestBytes(_data: Uint8Array) {}
 }
 
 test('sqlTableState: columnManupulation', () => {
-  const engine = new EngineProxy(new FakeEngine(), 'test');
+  const engine = new FakeEngine();
   const state = new SqlTableState(engine, table);
 
   const idColumn = {
@@ -88,7 +88,7 @@
 });
 
 test('sqlTableState: sortedColumns', () => {
-  const engine = new EngineProxy(new FakeEngine(), 'test');
+  const engine = new FakeEngine();
   const state = new SqlTableState(engine, table);
 
   // Verify that we have two columns: "id" and "name" and
@@ -139,7 +139,7 @@
 }
 
 test('sqlTableState: sqlStatement', () => {
-  const engine = new EngineProxy(new FakeEngine(), 'test');
+  const engine = new FakeEngine();
   const state = new SqlTableState(engine, table);
 
   // Check the generated SQL statement.
diff --git a/ui/src/frontend/sql_table/tab.ts b/ui/src/frontend/sql_table/tab.ts
index 4267331..a91eee4 100644
--- a/ui/src/frontend/sql_table/tab.ts
+++ b/ui/src/frontend/sql_table/tab.ts
@@ -25,7 +25,7 @@
 import {Filter, SqlTableState} from './state';
 import {SqlTable} from './table';
 import {SqlTableDescription, tableDisplayName} from './table_description';
-import {EngineProxy} from '../../public';
+import {Engine} from '../../public';
 import {globals} from '../globals';
 import {assertExists} from '../../base/logging';
 import {uuidv4} from '../../base/uuid';
@@ -58,7 +58,7 @@
 }
 
 // TODO(stevegolton): Find a way to make this more elegant.
-function getEngine(): EngineProxy {
+function getEngine(): Engine {
   const engConfig = globals.getCurrentEngine();
   const engineId = assertExists(engConfig).id;
   return assertExists(globals.engines.get(engineId)).getProxy('QueryResult');
diff --git a/ui/src/frontend/sql_table/table.ts b/ui/src/frontend/sql_table/table.ts
index d0db696..35e620f 100644
--- a/ui/src/frontend/sql_table/table.ts
+++ b/ui/src/frontend/sql_table/table.ts
@@ -16,7 +16,7 @@
 
 import {isString} from '../../base/object_utils';
 import {Icons} from '../../base/semantic_icons';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {Row} from '../../trace_processor/query_result';
 import {Anchor} from '../../widgets/anchor';
 import {BasicTable} from '../../widgets/basic_table';
@@ -37,7 +37,7 @@
 
 export class SqlTable implements m.ClassComponent<SqlTableConfig> {
   private readonly table: SqlTableDescription;
-  private readonly engine: EngineProxy;
+  private readonly engine: Engine;
 
   private state: SqlTableState;
 
diff --git a/ui/src/frontend/sql_utils.ts b/ui/src/frontend/sql_utils.ts
index 891a876..a78a88a 100644
--- a/ui/src/frontend/sql_utils.ts
+++ b/ui/src/frontend/sql_utils.ts
@@ -14,7 +14,7 @@
 
 import {isString} from '../base/object_utils';
 import {SortDirection} from '../common/state';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {ColumnType, NUM} from '../trace_processor/query_result';
 
 export interface OrderClause {
@@ -111,7 +111,7 @@
 }
 
 export async function getTableRowCount(
-  engine: EngineProxy,
+  engine: Engine,
   tableName: string,
 ): Promise<number | undefined> {
   const result = await engine.query(
diff --git a/ui/src/frontend/thread_and_process_info.ts b/ui/src/frontend/thread_and_process_info.ts
index b54cb45..2e70b29 100644
--- a/ui/src/frontend/thread_and_process_info.ts
+++ b/ui/src/frontend/thread_and_process_info.ts
@@ -17,7 +17,7 @@
 import {copyToClipboard} from '../base/clipboard';
 import {Icons} from '../base/semantic_icons';
 import {exists} from '../base/utils';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../trace_processor/query_result';
 import {Anchor} from '../widgets/anchor';
 import {MenuItem, PopupMenu2} from '../widgets/menu';
@@ -43,7 +43,7 @@
 }
 
 export async function getProcessInfo(
-  engine: EngineProxy,
+  engine: Engine,
   upid: Upid,
 ): Promise<ProcessInfo> {
   const it = (
@@ -137,7 +137,7 @@
 }
 
 export async function getThreadInfo(
-  engine: EngineProxy,
+  engine: Engine,
   utid: Utid,
 ): Promise<ThreadInfo> {
   const it = (
diff --git a/ui/src/frontend/thread_state.ts b/ui/src/frontend/thread_state.ts
index e912a7e..8a3d0ec 100644
--- a/ui/src/frontend/thread_state.ts
+++ b/ui/src/frontend/thread_state.ts
@@ -19,7 +19,7 @@
 import {exists} from '../base/utils';
 import {Actions} from '../common/actions';
 import {translateState} from '../common/thread_state';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {LONG, NUM, NUM_NULL, STR_NULL} from '../trace_processor/query_result';
 import {CPU_SLICE_TRACK_KIND} from '../core_plugins/cpu_slices';
 import {THREAD_STATE_TRACK_KIND} from '../core_plugins/thread_state';
@@ -59,7 +59,7 @@
 // Gets a list of thread state objects from Trace Processor with given
 // constraints.
 export async function getThreadStateFromConstraints(
-  engine: EngineProxy,
+  engine: Engine,
   constraints: SQLConstraints,
 ): Promise<ThreadState[]> {
   const query = await engine.query(`
@@ -120,7 +120,7 @@
 }
 
 export async function getThreadState(
-  engine: EngineProxy,
+  engine: Engine,
   id: number,
 ): Promise<ThreadState | undefined> {
   const result = await getThreadStateFromConstraints(engine, {
diff --git a/ui/src/frontend/time_axis_panel.ts b/ui/src/frontend/time_axis_panel.ts
index d20341f..af1f3df 100644
--- a/ui/src/frontend/time_axis_panel.ts
+++ b/ui/src/frontend/time_axis_panel.ts
@@ -57,16 +57,16 @@
         break;
       case TimestampFormat.UTC:
         const offsetDate = Time.toDate(
-          globals.utcOffset,
-          globals.realtimeOffset,
+          globals.traceTime.utcOffset,
+          globals.traceTime.realtimeOffset,
         );
         const dateStr = toISODateOnly(offsetDate);
         ctx.fillText(`UTC ${dateStr}`, 6, 10);
         break;
       case TimestampFormat.TraceTz:
         const offsetTzDate = Time.toDate(
-          globals.traceTzOffset,
-          globals.realtimeOffset,
+          globals.traceTime.traceTzOffset,
+          globals.traceTime.realtimeOffset,
         );
         const dateTzStr = toISODateOnly(offsetTzDate);
         ctx.fillText(dateTzStr, 6, 10);
diff --git a/ui/src/frontend/trace_info_page.ts b/ui/src/frontend/trace_info_page.ts
index 485146a..c8d7547 100644
--- a/ui/src/frontend/trace_info_page.ts
+++ b/ui/src/frontend/trace_info_page.ts
@@ -16,7 +16,7 @@
 
 import {QueryResponse, runQuery} from '../common/queries';
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 
 import {globals} from './globals';
 import {createPage} from './pages';
@@ -29,7 +29,7 @@
   queryId: string;
 }
 
-function getEngine(name: string): EngineProxy | undefined {
+function getEngine(name: string): Engine | undefined {
   const currentEngine = globals.getCurrentEngine();
   if (currentEngine === undefined) return undefined;
   const engineId = currentEngine.id;
diff --git a/ui/src/frontend/track.ts b/ui/src/frontend/track.ts
index cd11ce3..c6462b0 100644
--- a/ui/src/frontend/track.ts
+++ b/ui/src/frontend/track.ts
@@ -12,9 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 
 export interface NewTrackArgs {
   trackKey: string;
-  engine: EngineProxy;
+  engine: Engine;
 }
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index b599dc7..b67f2e5 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -128,7 +128,7 @@
         currentY: number,
         editing: boolean,
       ) => {
-        const traceTime = globals.state.traceTime;
+        const traceTime = globals.traceTime;
         const {visibleTimeScale} = timeline;
         this.keepCurrentSelection = true;
         if (editing) {
diff --git a/ui/src/frontend/viz_page.ts b/ui/src/frontend/viz_page.ts
index dadb34a..6145763 100644
--- a/ui/src/frontend/viz_page.ts
+++ b/ui/src/frontend/viz_page.ts
@@ -15,14 +15,14 @@
 import m from 'mithril';
 
 import {raf} from '../core/raf_scheduler';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {Editor} from '../widgets/editor';
 import {VegaView} from '../widgets/vega_view';
 
 import {globals} from './globals';
 import {createPage} from './pages';
 
-function getEngine(): EngineProxy | undefined {
+function getEngine(): Engine | undefined {
   const engineId = globals.getCurrentEngine()?.id;
   if (engineId === undefined) {
     return undefined;
@@ -32,7 +32,7 @@
 }
 
 let SPEC = '';
-let ENGINE: EngineProxy | undefined = undefined;
+let ENGINE: Engine | undefined = undefined;
 
 export const VizPage = createPage({
   oncreate() {
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 2a22aff..8f360b4 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 import {
   SimpleSliceTrack,
   SimpleSliceTrackConfig,
@@ -1694,7 +1694,7 @@
     );
   }
 
-  async findFeatures(e: EngineProxy): Promise<Set<string>> {
+  async findFeatures(e: Engine): Promise<Set<string>> {
     const features = new Set<string>();
 
     const addFeatures = async (q: string) => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index 9cb8405..34fd087 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -14,14 +14,14 @@
 
 import {Plugin, PluginContextTrace, PluginDescriptor} from '../../public';
 import {addDebugSliceTrack} from '../../public';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 
 class AndroidNetwork implements Plugin {
   // Adds a debug track using the provided query and given columns. The columns
   // must be start with ts, dur, and a name column. The name column and all
   // following columns are shown as arguments in slice details.
   async addSimpleTrack(
-    engine: EngineProxy,
+    engine: Engine,
     trackName: string,
     tableOrQuery: string,
     columns: string[],
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index a53ff20..25f11f4 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -18,11 +18,11 @@
   PluginContextTrace,
   PluginDescriptor,
 } from '../../public';
-import {EngineProxy} from '../../trace_processor/engine';
+import {Engine} from '../../trace_processor/engine';
 
 class AndroidPerf implements Plugin {
   async addAppProcessStartsDebugTrack(
-    engine: EngineProxy,
+    engine: Engine,
     reason: string,
     sliceName: string,
   ): Promise<void> {
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index f3e4338..7fb2e44 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -20,10 +20,10 @@
 import {ColorScheme} from '../core/colorizer';
 import {LegacySelection} from '../common/state';
 import {PanelSize} from '../frontend/panel';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {UntypedEventSet} from '../core/event_set';
 
-export {EngineProxy} from '../trace_processor/engine';
+export {Engine} from '../trace_processor/engine';
 export {
   LONG,
   LONG_NULL,
@@ -344,7 +344,7 @@
 // currently loaded trace. Passed to trace-relevant hooks on a plugin instead of
 // PluginContext.
 export interface PluginContextTrace extends PluginContext {
-  readonly engine: EngineProxy;
+  readonly engine: Engine;
 
   // Control over the main timeline.
   timeline: {
diff --git a/ui/src/trace_processor/engine.ts b/ui/src/trace_processor/engine.ts
index b234c16..90901c5 100644
--- a/ui/src/trace_processor/engine.ts
+++ b/ui/src/trace_processor/engine.ts
@@ -13,7 +13,6 @@
 // limitations under the License.
 
 import {defer, Deferred} from '../base/deferred';
-import {Disposable} from '../base/disposable';
 import {assertExists, assertTrue} from '../base/logging';
 import {duration, Span, Time, time, TimeSpan} from '../base/time';
 import {
@@ -42,6 +41,7 @@
 } from './query_result';
 
 import TPM = TraceProcessorRpc.TraceProcessorMethod;
+import {Disposable} from '../base/disposable';
 
 export interface LoadingTracker {
   beginLoading(): void;
@@ -66,6 +66,19 @@
   ftraceDropUntilAllCpusValid: boolean;
 }
 
+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>>;
+  computeMetric(
+    metrics: string[],
+    format: 'json' | 'prototext' | 'proto',
+  ): Promise<string | Uint8Array>;
+  readonly isAlive: boolean;
+}
+
 // Abstract interface of a trace proccessor.
 // This is the TypeScript equivalent of src/trace_processor/rpc.h.
 // There are two concrete implementations:
@@ -77,7 +90,7 @@
 // 1. Implement the abstract rpcSendRequestBytes() function, sending the
 //    proto-encoded TraceProcessorRpc requests to the TraceProcessor instance.
 // 2. Call onRpcResponseBytes() when response data is received.
-export abstract class Engine {
+export abstract class EngineBase implements Engine {
   abstract readonly id: string;
   private _cpus?: number[];
   private _numGpus?: number;
@@ -93,6 +106,7 @@
   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();
@@ -502,10 +516,9 @@
   }
 }
 
-// Lightweight wrapper over Engine exposing only `query` method and annotating
-// all queries going through it with a tag.
-export class EngineProxy implements Disposable {
-  private engine: Engine;
+// Lightweight engine proxy which annotates all queries with a tag
+export class EngineProxy implements Engine, Disposable {
+  private engine: EngineBase;
   private tag: string;
   private _isAlive: boolean;
 
@@ -513,7 +526,7 @@
     return this._isAlive;
   }
 
-  constructor(engine: Engine, tag: string) {
+  constructor(engine: EngineBase, tag: string) {
     this.engine = engine;
     this.tag = tag;
     this._isAlive = true;
@@ -557,6 +570,10 @@
     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/http_rpc_engine.ts b/ui/src/trace_processor/http_rpc_engine.ts
index dfd9bc8..720e2b6 100644
--- a/ui/src/trace_processor/http_rpc_engine.ts
+++ b/ui/src/trace_processor/http_rpc_engine.ts
@@ -15,7 +15,7 @@
 import {fetchWithTimeout} from '../base/http_utils';
 import {assertExists} from '../base/logging';
 import {StatusResult} from '../protos';
-import {Engine, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase, LoadingTracker} from '../trace_processor/engine';
 
 const RPC_CONNECT_TIMEOUT_MS = 2000;
 
@@ -25,7 +25,7 @@
   failure?: string;
 }
 
-export class HttpRpcEngine extends Engine {
+export class HttpRpcEngine extends EngineBase {
   readonly id: string;
   errorHandler: (err: string) => void = () => {};
   private requestQueue = new Array<Uint8Array>();
diff --git a/ui/src/trace_processor/wasm_engine_proxy.ts b/ui/src/trace_processor/wasm_engine_proxy.ts
index 42740e2..163fac5 100644
--- a/ui/src/trace_processor/wasm_engine_proxy.ts
+++ b/ui/src/trace_processor/wasm_engine_proxy.ts
@@ -13,7 +13,7 @@
 // limitations under the License.
 
 import {assertExists, assertTrue} from '../base/logging';
-import {Engine, LoadingTracker} from '../trace_processor/engine';
+import {EngineBase, LoadingTracker} from '../trace_processor/engine';
 
 let bundlePath: string;
 let idleWasmWorker: Worker;
@@ -47,7 +47,7 @@
  * This implementation of Engine uses a WASM backend hosted in a separate
  * worker thread.
  */
-export class WasmEngineProxy extends Engine {
+export class WasmEngineProxy extends EngineBase {
   readonly id: string;
   private port: MessagePort;
 
diff --git a/ui/src/widgets/vega_view.ts b/ui/src/widgets/vega_view.ts
index 97d9826..39be606 100644
--- a/ui/src/widgets/vega_view.ts
+++ b/ui/src/widgets/vega_view.ts
@@ -20,7 +20,7 @@
 import {getErrorMessage} from '../base/errors';
 import {isString, shallowEquals} from '../base/object_utils';
 import {SimpleResizeObserver} from '../base/resize_observer';
-import {EngineProxy} from '../trace_processor/engine';
+import {Engine} from '../trace_processor/engine';
 import {QueryError} from '../trace_processor/query_result';
 import {scheduleFullRedraw} from '../widgets/raf';
 import {Spinner} from '../widgets/spinner';
@@ -45,7 +45,7 @@
 interface VegaViewAttrs {
   spec: string;
   data: VegaViewData;
-  engine?: EngineProxy;
+  engine?: Engine;
 }
 
 // VegaWrapper is in exactly one of these states:
@@ -62,10 +62,10 @@
 }
 
 class EngineLoader implements vega.Loader {
-  private engine?: EngineProxy;
+  private engine?: Engine;
   private loader: vega.Loader;
 
-  constructor(engine: EngineProxy | undefined) {
+  constructor(engine: Engine | undefined) {
     this.engine = engine;
     this.loader = vega.loader();
   }
@@ -125,7 +125,7 @@
   private pending?: Promise<vega.View>;
   private _status: Status;
   private _error?: string;
-  private _engine?: EngineProxy;
+  private _engine?: Engine;
 
   constructor(dom: Element) {
     this.dom = dom;
@@ -155,7 +155,7 @@
     this.updateView();
   }
 
-  set engine(engine: EngineProxy | undefined) {
+  set engine(engine: Engine | undefined) {
     this._engine = engine;
   }