Merge "add will_notify_on_stop to DataSourceParams" into main
diff --git a/BUILD b/BUILD
index 55ad910..bf86ecc 100644
--- a/BUILD
+++ b/BUILD
@@ -6548,3 +6548,8 @@
     main = "tools/write_version_header.py",
     python_version = "PY3",
 )
+
+exports_files(
+    ["ui/src/assets/favicon.png"],
+    visibility = PERFETTO_CONFIG.public_visibility,
+)
diff --git a/BUILD.extras b/BUILD.extras
index 6bb9928..c166ab9 100644
--- a/BUILD.extras
+++ b/BUILD.extras
@@ -88,3 +88,8 @@
     main = "tools/write_version_header.py",
     python_version = "PY3",
 )
+
+exports_files(
+    ["ui/src/assets/favicon.png"],
+    visibility = PERFETTO_CONFIG.public_visibility,
+)
diff --git a/include/perfetto/tracing/track.h b/include/perfetto/tracing/track.h
index f17ffbb..37f02e5 100644
--- a/include/perfetto/tracing/track.h
+++ b/include/perfetto/tracing/track.h
@@ -120,11 +120,7 @@
   // Construct a track using |ptr| as identifier within thread-scope.
   // Shorthand for `Track::FromPointer(ptr, ThreadTrack::Current())`
   // Usage: TRACE_EVENT_BEGIN("...", "...", perfetto::Track::ThreadScoped(this))
-  static Track ThreadScoped(
-      const void* ptr,
-      Track parent = MakeThreadTrack(base::GetThreadId())) {
-    return Track::FromPointer(ptr, parent);
-  }
+  static Track ThreadScoped(const void* ptr, Track parent = Track());
 
  protected:
   constexpr Track(uint64_t uuid_, uint64_t parent_uuid_)
diff --git a/infra/ui.perfetto.dev/README.md b/infra/ui.perfetto.dev/README.md
index 6006a7f..889402e 100644
--- a/infra/ui.perfetto.dev/README.md
+++ b/infra/ui.perfetto.dev/README.md
@@ -22,17 +22,28 @@
 Cloud Build invokes the equivalent of:
 
 ```bash
-docker run gcr.io/perfetto-ui/perfetto-ui-builder \
-    ui/release/builder_entrypoint.sh
+docker run europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder \
+    /ui_builder_entrypoint.sh
 ```
 
-NOTE: the `builder_entrypoint.sh` script is not bundled in the docker container
-and is taken from the HEAD if the checked out repo.
+NOTE: the `ui_builder_entrypoint.sh` script is bundled in the docker container.
+The container needs to be re-built and re-pushed if the script changes.
 
 To update the container:
 
+Prerequisite:
+Install the Google Cloud SDK from https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz 
+
+
 ```bash
-cd infra/ui.perfetto.dev/builder
-docker build -t gcr.io/perfetto-ui/perfetto-ui-builder .
-docker push gcr.io/perfetto-ui/perfetto-ui-builder .
+# Obtain a temporary token to impersonate the service account as per
+# https://cloud.google.com/artifact-registry/docs/docker/authentication
+# You need to be a member of perfetto-cloud-infra.prod to do this.
+gcloud auth print-access-token \
+    --impersonate-service-account perfetto-ui-dev@perfetto-ui.iam.gserviceaccount.com | docker login \
+    -u oauth2accesstoken \
+    --password-stdin https://europe-docker.pkg.dev
+
+docker build -t europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder infra/ui.perfetto.dev/builder
+docker push europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
 ```
diff --git a/infra/ui.perfetto.dev/builder/Dockerfile b/infra/ui.perfetto.dev/builder/Dockerfile
index 07944c1..8c726ee 100644
--- a/infra/ui.perfetto.dev/builder/Dockerfile
+++ b/infra/ui.perfetto.dev/builder/Dockerfile
@@ -15,16 +15,14 @@
 # The image that builds the Perfetto UI and deploys to GCS.
 # See go/perfetto-ui-autopush for docs on how this works end-to-end.
 
-FROM debian:buster-slim
+FROM debian:bookworm-slim
 
 ENV PATH=/builder/google-cloud-sdk/bin/:$PATH
 RUN set -ex; \
     export DEBIAN_FRONTEND=noninteractive; \
     apt-get update; \
     apt-get -y install python3 python3-distutils python3-pip git curl tar tini \
-            pkg-config zip libc-dev libgcc-8-dev; \
-    update-alternatives --install /usr/bin/python python /usr/bin/python3.7 1; \
-    pip3 install --quiet protobuf crcmod; \
+            pkg-config zip libc-dev python3-protobuf python3-crcmod; \
     mkdir -p /builder && \
     curl -s -o - https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz | tar -zx -C /builder; \
     /builder/google-cloud-sdk/install.sh \
@@ -35,6 +33,10 @@
     git config --system credential.helper gcloud.sh; \
     useradd -d /home/perfetto perfetto; \
     apt-get -y autoremove; \
-    rm -rf /var/lib/apt/lists/* /usr/share/man/* /usr/share/doc/*;
+    rm -rf /var/lib/apt/lists/* /usr/share/man/* /usr/share/doc/*; \
+    rm -rf /builder/google-cloud-sdk/.install/.backup;
+
+ADD ui_builder_entrypoint.sh /ui_builder_entrypoint.sh
+RUN chmod 755 /ui_builder_entrypoint.sh
 
 ENTRYPOINT [ "tini", "-g", "--" ]
diff --git a/ui/release/builder_entrypoint.sh b/infra/ui.perfetto.dev/builder/ui_builder_entrypoint.sh
similarity index 90%
rename from ui/release/builder_entrypoint.sh
rename to infra/ui.perfetto.dev/builder/ui_builder_entrypoint.sh
index 27340ea..c1f70d3 100755
--- a/ui/release/builder_entrypoint.sh
+++ b/infra/ui.perfetto.dev/builder/ui_builder_entrypoint.sh
@@ -17,8 +17,6 @@
 
 set -exu
 
-CUR_DUR=$(dirname ${BASH_SOURCE[0]})
-
 env
 pwd
 mount
@@ -30,6 +28,7 @@
 # support yet triggering from Gerrit.
 
 cd /workspace/
+mkdir /workspace/tmp
 
 ls -A1 | xargs rm -rf
 UPSTREAM="https://android.googlesource.com/platform/external/perfetto.git"
@@ -38,7 +37,8 @@
 cd upstream/
 
 # infra/ui.perfetto.dev/cloudbuild_release.yaml sets $1 to the branch
-# name.
+# name when triggering from a release branch. Otherwise $1 is "" when triggering
+# from main.
 EXTRA_ARGS=""
 if [[ ! -z $1 ]]; then
   git checkout $1
@@ -46,6 +46,5 @@
 fi
 
 git rev-parse HEAD
-mkdir /workspace/tmp
-python3 -u "$CUR_DUR/build_all_channels.py" \
+python3 -u "ui/release/build_all_channels.py" \
         --upload --tmp=/workspace/tmp $EXTRA_ARGS
diff --git a/infra/ui.perfetto.dev/cloudbuild.yaml b/infra/ui.perfetto.dev/cloudbuild.yaml
index b8905f6..8097d1e 100644
--- a/infra/ui.perfetto.dev/cloudbuild.yaml
+++ b/infra/ui.perfetto.dev/cloudbuild.yaml
@@ -1,10 +1,13 @@
 # See go/perfetto-ui-autopush for docs on how this works end-to-end.
+# If this file is modified, the inline YAML must be copy-pasted
+# FROM: infra/ui.perfetto.dev/cloudbuild.yaml
+# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build.
 steps:
-- name: gcr.io/$PROJECT_ID/perfetto-ui-builder
+- name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
   args:
-  - 'ui/release/builder_entrypoint.sh'
+  - /ui_builder_entrypoint.sh
   - ''
-  # The extra arg above is load baring. The builder_entrypoint.sh
+  # The empty arg above is load bearing. The builder_entrypoint.sh
   # script can't handle $1 sometimes being defined (as in
   # cloudbuild.yaml) and sometimes not.
 
diff --git a/infra/ui.perfetto.dev/cloudbuild_release.yaml b/infra/ui.perfetto.dev/cloudbuild_release.yaml
index bcf6995..1dfe79a 100644
--- a/infra/ui.perfetto.dev/cloudbuild_release.yaml
+++ b/infra/ui.perfetto.dev/cloudbuild_release.yaml
@@ -1,8 +1,11 @@
 # See go/perfetto-ui-autopush for docs on how this works end-to-end.
+# If this file is modified, the inline YAML must be copy-pasted
+# FROM: infra/ui.perfetto.dev/cloudbuild.yaml
+# TO: TWO trigger configs inline YAML in Google Cloud Console > Cloud Build.
 steps:
-- name: gcr.io/$PROJECT_ID/perfetto-ui-builder
+- name: europe-docker.pkg.dev/perfetto-ui/builder/perfetto-ui-builder
   args:
-  - 'ui/release/builder_entrypoint.sh'
+  - /ui_builder_entrypoint.sh
   - $BRANCH_NAME
 # Timeout = 30 min (last measured time in Feb 2021: 12 min)
 timeout: 1800s
diff --git a/protos/perfetto/metrics/android/startup_metric.proto b/protos/perfetto/metrics/android/startup_metric.proto
index e2ca850..16a0c3e 100644
--- a/protos/perfetto/metrics/android/startup_metric.proto
+++ b/protos/perfetto/metrics/android/startup_metric.proto
@@ -234,6 +234,112 @@
     optional string details = 2;
   }
 
+  // Contains information for slow startup causes.
+  message SlowStartReason {
+    // Points to reason description and solution.
+    enum ReasonId {
+      REASON_ID_UNSPECIFIED = 0;
+      NO_BASELINE_OR_CLOUD_PROFILES = 1;
+      RUN_FROM_APK = 2;
+      UNLOCK_RUNNING = 3;
+      APP_IN_DEBUGGABLE_MODE = 4;
+      GC_ACTIVITY = 5;
+      DEX2OAT_RUNNING = 6;
+      INSTALLD_RUNNING = 7;
+      MAIN_THREAD_TIME_SPENT_IN_RUNNABLE = 8;
+      MAIN_THREAD_TIME_SPENT_IN_INTERRUPTIBLE_SLEEP = 9;
+      MAIN_THREAD_TIME_SPENT_IN_BLOCKING_IO = 10;
+      MAIN_THREAD_TIME_SPENT_IN_OPEN_DEX_FILES_FROM_OAT = 11;
+      TIME_SPENT_IN_BIND_APPLICATION = 12;
+      TIME_SPENT_IN_VIEW_INFLATION = 13;
+      TIME_SPENT_IN_RESOURCES_MANAGER_GET_RESOURCES = 14;
+      TIME_SPENT_VERIFYING_CLASSES = 15;
+      POTENTIAL_CPU_CONTENTION_WITH_ANOTHER_PROCESS = 16;
+      JIT_ACTIVITY = 17;
+      MAIN_THREAD_LOCK_CONTENTION = 18;
+      MAIN_THREAD_MONITOR_CONTENTION = 19;
+      JIT_COMPILED_METHODS = 20;
+      BROADCAST_DISPATCHED_COUNT = 21;
+      BROADCAST_RECEIVED_COUNT = 22;
+      STARTUP_RUNNING_CONCURRENT = 23;
+      MAIN_THREAD_BINDER_TRANSCATIONS_BLOCKED = 24;
+    }
+    optional ReasonId reason_id = 1;
+
+    // Brief description for human readability.
+    optional string reason = 2;
+
+    // Expected value (inherited from threshold definition).
+    optional ThresholdValue expected_value = 3;
+
+    // Actual value, can be used to decide severity level.
+    optional ActualValue actual_value = 4;
+
+    // Launch duration
+    optional int64 launch_dur = 5;
+
+    // Sum of durations of slices and thread states in trace_slices_or_threads.
+    // Can be used to decide if a couple of top slices or threads caused the issue.
+    optional int64 duration = 6;
+
+    // Information of a subset of slice and thread sections to focused on,
+    // sorted by the duration in descending order.
+    // By checking out the top slices/threads, developers can identify specific
+    // slices or threads for further investigation.
+    repeated TraceSliceSection trace_slice_sections = 7;
+    repeated TraceThreadSection trace_thread_sections = 8;
+
+    // Details specific for a reason.
+    optional string additional_info = 9;
+  }
+
+  message ThresholdValue {
+    // Expected value. 1 for true and 0 for false for booleans.
+    optional int64  value = 1;
+
+    // Expected value unit. Enum, e.g. "ns", "%"
+    enum ThresholdUnit {
+      THRESHOLD_UNIT_UNSPECIFIED = 0;
+      NS = 1;
+      PERCENTAGE = 2;
+      TRUE_OR_FALSE = 3;
+    }
+    optional ThresholdUnit unit = 2;
+
+    // For numeric threshold values only. When higher_expected is true,
+    // an app startup is considered performant if actual value is higher
+    // than the threshold.
+    optional bool higher_expected = 3;
+  }
+
+  message ActualValue {
+    // Actual value. 1 for true and 0 for false for booleans.
+    optional int64 value = 1;
+
+    // Actual duration for percentage thresholds only.
+    // E.g. if the threashold is 20% and the launch_duration is 1000ms,
+    // then the actual duration is more than 200ms.
+    optional int64 dur = 2;
+  }
+
+  // Contains information for a section of a slice.
+  message TraceSliceSection {
+    optional int64 start_timestamp = 1;
+
+    optional int64 end_timestamp = 2;
+
+    optional uint32 slice_id = 3;
+  }
+
+  // Contains information for a section of a thread.
+  message TraceThreadSection {
+     optional int64 start_timestamp = 1;
+
+     optional int64 end_timestamp = 2;
+
+     optional uint32 thread_utid = 3;
+  }
+
   // Next id: 22
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
diff --git a/protos/perfetto/metrics/perfetto_merged_metrics.proto b/protos/perfetto/metrics/perfetto_merged_metrics.proto
index 8f736b3..1db8a09 100644
--- a/protos/perfetto/metrics/perfetto_merged_metrics.proto
+++ b/protos/perfetto/metrics/perfetto_merged_metrics.proto
@@ -2350,6 +2350,112 @@
     optional string details = 2;
   }
 
+  // Contains information for slow startup causes.
+  message SlowStartReason {
+    // Points to reason description and solution.
+    enum ReasonId {
+      REASON_ID_UNSPECIFIED = 0;
+      NO_BASELINE_OR_CLOUD_PROFILES = 1;
+      RUN_FROM_APK = 2;
+      UNLOCK_RUNNING = 3;
+      APP_IN_DEBUGGABLE_MODE = 4;
+      GC_ACTIVITY = 5;
+      DEX2OAT_RUNNING = 6;
+      INSTALLD_RUNNING = 7;
+      MAIN_THREAD_TIME_SPENT_IN_RUNNABLE = 8;
+      MAIN_THREAD_TIME_SPENT_IN_INTERRUPTIBLE_SLEEP = 9;
+      MAIN_THREAD_TIME_SPENT_IN_BLOCKING_IO = 10;
+      MAIN_THREAD_TIME_SPENT_IN_OPEN_DEX_FILES_FROM_OAT = 11;
+      TIME_SPENT_IN_BIND_APPLICATION = 12;
+      TIME_SPENT_IN_VIEW_INFLATION = 13;
+      TIME_SPENT_IN_RESOURCES_MANAGER_GET_RESOURCES = 14;
+      TIME_SPENT_VERIFYING_CLASSES = 15;
+      POTENTIAL_CPU_CONTENTION_WITH_ANOTHER_PROCESS = 16;
+      JIT_ACTIVITY = 17;
+      MAIN_THREAD_LOCK_CONTENTION = 18;
+      MAIN_THREAD_MONITOR_CONTENTION = 19;
+      JIT_COMPILED_METHODS = 20;
+      BROADCAST_DISPATCHED_COUNT = 21;
+      BROADCAST_RECEIVED_COUNT = 22;
+      STARTUP_RUNNING_CONCURRENT = 23;
+      MAIN_THREAD_BINDER_TRANSCATIONS_BLOCKED = 24;
+    }
+    optional ReasonId reason_id = 1;
+
+    // Brief description for human readability.
+    optional string reason = 2;
+
+    // Expected value (inherited from threshold definition).
+    optional ThresholdValue expected_value = 3;
+
+    // Actual value, can be used to decide severity level.
+    optional ActualValue actual_value = 4;
+
+    // Launch duration
+    optional int64 launch_dur = 5;
+
+    // Sum of durations of slices and thread states in trace_slices_or_threads.
+    // Can be used to decide if a couple of top slices or threads caused the issue.
+    optional int64 duration = 6;
+
+    // Information of a subset of slice and thread sections to focused on,
+    // sorted by the duration in descending order.
+    // By checking out the top slices/threads, developers can identify specific
+    // slices or threads for further investigation.
+    repeated TraceSliceSection trace_slice_sections = 7;
+    repeated TraceThreadSection trace_thread_sections = 8;
+
+    // Details specific for a reason.
+    optional string additional_info = 9;
+  }
+
+  message ThresholdValue {
+    // Expected value. 1 for true and 0 for false for booleans.
+    optional int64  value = 1;
+
+    // Expected value unit. Enum, e.g. "ns", "%"
+    enum ThresholdUnit {
+      THRESHOLD_UNIT_UNSPECIFIED = 0;
+      NS = 1;
+      PERCENTAGE = 2;
+      TRUE_OR_FALSE = 3;
+    }
+    optional ThresholdUnit unit = 2;
+
+    // For numeric threshold values only. When higher_expected is true,
+    // an app startup is considered performant if actual value is higher
+    // than the threshold.
+    optional bool higher_expected = 3;
+  }
+
+  message ActualValue {
+    // Actual value. 1 for true and 0 for false for booleans.
+    optional int64 value = 1;
+
+    // Actual duration for percentage thresholds only.
+    // E.g. if the threashold is 20% and the launch_duration is 1000ms,
+    // then the actual duration is more than 200ms.
+    optional int64 dur = 2;
+  }
+
+  // Contains information for a section of a slice.
+  message TraceSliceSection {
+    optional int64 start_timestamp = 1;
+
+    optional int64 end_timestamp = 2;
+
+    optional uint32 slice_id = 3;
+  }
+
+  // Contains information for a section of a thread.
+  message TraceThreadSection {
+     optional int64 start_timestamp = 1;
+
+     optional int64 end_timestamp = 2;
+
+     optional uint32 thread_utid = 3;
+  }
+
   // Next id: 22
   message Startup {
     // Random id uniquely identifying an app startup in this trace.
diff --git a/protos/perfetto/trace/android/BUILD.gn b/protos/perfetto/trace/android/BUILD.gn
index 97e9d52..900a64b 100644
--- a/protos/perfetto/trace/android/BUILD.gn
+++ b/protos/perfetto/trace/android/BUILD.gn
@@ -58,6 +58,10 @@
 
 # Winscope messages added to TracePacket as extensions
 perfetto_proto_library("winscope_extensions_@TYPE@") {
+  proto_generators = [
+    "zero",
+    "source_set",
+  ]
   public_deps = [ ":winscope_common_@TYPE@" ]
   sources = [
     "inputmethodeditor.proto",
diff --git a/protos/third_party/chromium/chrome_track_event.proto b/protos/third_party/chromium/chrome_track_event.proto
index 8b766cb..e43f424 100644
--- a/protos/third_party/chromium/chrome_track_event.proto
+++ b/protos/third_party/chromium/chrome_track_event.proto
@@ -1632,6 +1632,7 @@
     DEADLINE_MODE_REGULAR = 3;
     DEADLINE_MODE_LATE = 4;
     DEADLINE_MODE_BLOCKED = 5;
+    DEADLINE_MODE_WAIT_FOR_SCROLL = 6;
   }
   optional ChromeCompositorStateMachineV2 state_machine = 1;
   optional bool observing_begin_frame_source = 2;
@@ -1857,9 +1858,36 @@
   optional CallSite call_site = 2;
 }
 
+message AnimationFrameTimingInfo {
+  optional int64 blocking_duration_ms = 1;
+  optional int64 duration_ms = 2;
+  optional int64 num_scripts = 3;
+}
+
+message AnimationFrameScriptTimingInfo {
+  optional int64 style_duration_ms = 1;
+  optional int64 layout_duration_ms = 2;
+  optional int64 pause_duration_ms = 3;
+  optional string class_like_name = 4;
+  optional string property_like_name = 5;
+  optional string source_location_url = 6;
+  optional string source_location_function_name = 7;
+  optional int64 source_location_char_position = 8;
+  enum InvokerType {
+    UNDEFINED = 0;
+    CLASSIC_SCRIPT = 1;
+    MODULE_SCRIPT = 2;
+    USER_CALLBACK = 3;
+    EVENT_HANDLER = 4;
+    PROMISE_RESOLVE = 5;
+    PROMISE_REJECT = 6;
+  }
+  optional InvokerType invoker_type = 9;
+}
+
 message ChromeTrackEvent {
   // Extension range for Chrome: 1000-1999
-  // Next ID: 1064
+  // Next ID: 1066
   extend TrackEvent {
     optional ChromeAppState chrome_app_state = 1000;
 
@@ -1994,5 +2022,10 @@
     optional ChromeCompositorSchedulerStateV2 cc_scheduler_state = 1062;
 
     optional WebViewStartup webview_startup = 1063;
+
+    optional AnimationFrameTimingInfo animation_frame_timing_info = 1064;
+
+    optional AnimationFrameScriptTimingInfo animation_frame_script_timing_info =
+        1065;
   }
 }
diff --git a/python/perfetto/trace_processor/metrics.descriptor b/python/perfetto/trace_processor/metrics.descriptor
index 9c1dfa0..4b307a8 100644
--- a/python/perfetto/trace_processor/metrics.descriptor
+++ b/python/perfetto/trace_processor/metrics.descriptor
Binary files differ
diff --git a/src/trace_processor/importers/proto/profile_module.cc b/src/trace_processor/importers/proto/profile_module.cc
index 6064d33..2bfb78a 100644
--- a/src/trace_processor/importers/proto/profile_module.cc
+++ b/src/trace_processor/importers/proto/profile_module.cc
@@ -408,7 +408,10 @@
         src_allocation.heap_name =
             context_->storage->InternString(entry.heap_name());
       } else {
-        src_allocation.heap_name = context_->storage->InternString("malloc");
+        // After aosp/1348782 there should be a heap name associated with all
+        // allocations - absence of one is likely a bug (for traces captured
+        // in older builds, this was the native heap profiler (libc.malloc)).
+        src_allocation.heap_name = context_->storage->InternString("unknown");
       }
       src_allocation.timestamp = timestamp;
       src_allocation.callstack_id = sample.callstack_id();
diff --git a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
index 3faf091..071968a 100644
--- a/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
+++ b/src/trace_processor/perfetto_sql/stdlib/android/startup/startups_maxsdk28.sql
@@ -37,11 +37,12 @@
     sl.ts,
     rs.ts + rs.dur AS ts_end,
     -- We use the process name as the package as we have no better option.
-    process_name AS package,
+    COALESCE(process_name, thread_name, 'unknown') AS package,
     "hot" AS startup_type
   FROM thread_slice sl
   JOIN android_first_frame_after(sl.ts) rs
   WHERE name = 'activityResume'
+  AND sl.is_main_thread
   -- Remove any launches here where the activityResume slices happens during
   -- a warm/cold startup.
   AND NOT EXISTS (
diff --git a/src/trace_redaction/collect_frame_cookies.cc b/src/trace_redaction/collect_frame_cookies.cc
index 13e5539..04c5eb3 100644
--- a/src/trace_redaction/collect_frame_cookies.cc
+++ b/src/trace_redaction/collect_frame_cookies.cc
@@ -148,18 +148,12 @@
     return base::OkStatus();
   }
 
-  const auto* timeline = context->timeline.get();
-  auto uid = context->package_uid.value();
-
-  auto& package_frame_cookies = context->package_frame_cookies;
-
   // Filter the global cookies down to cookies that belong to the target package
   // (uid).
   for (const auto& cookie : context->global_frame_cookies) {
-    auto cookie_slice = timeline->Search(cookie.ts, cookie.pid);
-
-    if (cookie_slice.uid == uid) {
-      package_frame_cookies.insert(cookie.cookie);
+    if (context->timeline->PidConnectsToUid(cookie.ts, cookie.pid,
+                                            *context->package_uid)) {
+      context->package_frame_cookies.insert(cookie.cookie);
     }
   }
 
diff --git a/src/trace_redaction/collect_timeline_events.cc b/src/trace_redaction/collect_timeline_events.cc
index aac9752..70f92f5 100644
--- a/src/trace_redaction/collect_timeline_events.cc
+++ b/src/trace_redaction/collect_timeline_events.cc
@@ -37,7 +37,7 @@
 using TaskNewtaskFtraceEvent = protos::pbzero::TaskNewtaskFtraceEvent;
 
 void MarkOpen(uint64_t ts,
-              ProcessTree::Process::Decoder process,
+              const ProcessTree::Process::Decoder& process,
               ProcessThreadTimeline* timeline) {
   // The uid in the process tree is a int32_t, but in the package list, the uid
   // is a uint64_t.
@@ -48,14 +48,14 @@
 }
 
 void MarkOpen(uint64_t ts,
-              ProcessTree::Thread::Decoder thread,
+              const ProcessTree::Thread::Decoder& thread,
               ProcessThreadTimeline* timeline) {
   auto e = ProcessThreadTimeline::Event::Open(ts, thread.tid(), thread.tgid());
   timeline->Append(e);
 }
 
 void MarkClose(const FtraceEvent::Decoder& event,
-               SchedProcessFreeFtraceEvent::Decoder process_free,
+               const SchedProcessFreeFtraceEvent::Decoder process_free,
                ProcessThreadTimeline* timeline) {
   auto e = ProcessThreadTimeline::Event::Close(event.timestamp(),
                                                process_free.pid());
@@ -63,7 +63,7 @@
 }
 
 void MarkOpen(const FtraceEvent::Decoder& event,
-              TaskNewtaskFtraceEvent::Decoder new_task,
+              const TaskNewtaskFtraceEvent::Decoder new_task,
               ProcessThreadTimeline* timeline) {
   // Event though pid() is uint32_t. all other pid values use int32_t, so it's
   // assumed to be safe to narrow-cast it.
@@ -74,7 +74,7 @@
 }
 
 void AppendEvents(uint64_t ts,
-                  ProcessTree::Decoder tree,
+                  const ProcessTree::Decoder& tree,
                   ProcessThreadTimeline* timeline) {
   for (auto it = tree.processes(); it; ++it) {
     MarkOpen(ts, ProcessTree::Process::Decoder(*it), timeline);
@@ -85,7 +85,7 @@
   }
 }
 
-void AppendEvents(FtraceEventBundle::Decoder ftrace_events,
+void AppendEvents(const FtraceEventBundle::Decoder& ftrace_events,
                   ProcessThreadTimeline* timeline) {
   for (auto it = ftrace_events.event(); it; ++it) {
     FtraceEvent::Decoder event(*it);
diff --git a/src/trace_redaction/filter_print_events.cc b/src/trace_redaction/filter_print_events.cc
index 6c1843b..869c7a0 100644
--- a/src/trace_redaction/filter_print_events.cc
+++ b/src/trace_redaction/filter_print_events.cc
@@ -41,9 +41,6 @@
   PERFETTO_DCHECK(context.timeline);
   PERFETTO_DCHECK(context.package_uid.has_value());
 
-  const auto* timeline = context.timeline.get();
-  auto package_uid = context.package_uid;
-
   protozero::ProtoDecoder event(bytes);
 
   // This is not a print packet. Keep the packet.
@@ -56,9 +53,9 @@
       event.FindField(protos::pbzero::FtraceEvent::kTimestampFieldNumber);
   auto pid = event.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
 
-  // Pid + Time --> UID, if the uid matches the target package, keep the event.
   return pid.valid() && time.valid() &&
-         timeline->Search(time.as_uint64(), pid.as_int32()).uid == package_uid;
+         context.timeline->PidConnectsToUid(time.as_uint64(), pid.as_int32(),
+                                            *context.package_uid);
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_sched_waking_events.cc b/src/trace_redaction/filter_sched_waking_events.cc
index e08a9d6..287090d 100644
--- a/src/trace_redaction/filter_sched_waking_events.cc
+++ b/src/trace_redaction/filter_sched_waking_events.cc
@@ -59,14 +59,9 @@
   auto outer_pid =
       event_decoder.FindField(protos::pbzero::FtraceEvent::kPidFieldNumber);
 
-  if (!outer_pid.valid()) {
-    return false;  // Remove
-  }
-
-  auto outer_slice = context.timeline->Search(
-      timestamp.as_uint64(), static_cast<int32_t>(outer_pid.as_uint32()));
-
-  if (outer_slice.uid != context.package_uid.value()) {
+  if (!outer_pid.valid() ||
+      !context.timeline->PidConnectsToUid(
+          timestamp.as_uint64(), outer_pid.as_int32(), *context.package_uid)) {
     return false;  // Remove
   }
 
@@ -75,13 +70,13 @@
   auto inner_pid = waking_decoder.FindField(
       protos::pbzero::SchedWakingFtraceEvent::kPidFieldNumber);
 
-  if (!inner_pid.valid()) {
+  if (!inner_pid.valid() ||
+      !context.timeline->PidConnectsToUid(
+          timestamp.as_uint64(), inner_pid.as_int32(), *context.package_uid)) {
     return false;  // Remove
   }
 
-  auto inner_slice =
-      context.timeline->Search(timestamp.as_uint64(), inner_pid.as_int32());
-  return inner_slice.uid == context.package_uid.value();
+  return true;
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/filter_task_rename.cc b/src/trace_redaction/filter_task_rename.cc
index 65f84fb..467e5b5 100644
--- a/src/trace_redaction/filter_task_rename.cc
+++ b/src/trace_redaction/filter_task_rename.cc
@@ -67,8 +67,8 @@
     return false;
   }
 
-  auto slice = context.timeline->Search(timestamp.as_uint64(), pid.as_int32());
-  return slice.uid == context.package_uid.value();
+  return context.timeline->PidConnectsToUid(
+      timestamp.as_uint64(), pid.as_int32(), *context.package_uid);
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/process_thread_timeline.cc b/src/trace_redaction/process_thread_timeline.cc
index c1c3984..43d3637 100644
--- a/src/trace_redaction/process_thread_timeline.cc
+++ b/src/trace_redaction/process_thread_timeline.cc
@@ -45,8 +45,9 @@
   mode_ = Mode::kRead;
 }
 
-ProcessThreadTimeline::Slice ProcessThreadTimeline::Search(uint64_t ts,
-                                                           int32_t pid) const {
+bool ProcessThreadTimeline::PidConnectsToUid(uint64_t ts,
+                                             int32_t pid,
+                                             uint64_t uid) const {
   PERFETTO_DCHECK(mode_ == Mode::kRead);
 
   auto event = FindPreviousEvent(ts, pid);
@@ -54,26 +55,23 @@
   for (size_t d = 0; d < kMaxSearchDepth; ++d) {
     // The thread/process was freed. It won't exist until a new open event.
     if (event.type != Event::Type::kOpen) {
-      return {pid, Event::kUnknownUid};
+      return false;
     }
 
-    // System processes all have uids equal to zero, so everything eventually
-    // has a uid. This means that all threads should find a process and a uid.
-    // However, if a thread does not have a process (this should not happen)
-    // that thread will be treated as invalid.
-    if (event.uid != Event::kUnknownUid) {
-      return {pid, event.uid};
+    // TODO(vaage): Normalize the uid values.
+    if (event.uid == uid) {
+      return true;
     }
 
-    // If there is no parent, there is no reason to keep searching.
+    // If there is no parent, there is no way to keep searching.
     if (event.ppid == Event::kUnknownPid) {
-      return {pid, Event::kUnknownUid};
+      return false;
     }
 
     event = FindPreviousEvent(ts, event.ppid);
   }
 
-  return {pid, Event::kUnknownUid};
+  return false;
 }
 
 ProcessThreadTimeline::Event ProcessThreadTimeline::FindPreviousEvent(
diff --git a/src/trace_redaction/process_thread_timeline.h b/src/trace_redaction/process_thread_timeline.h
index b176121..fdb70ba 100644
--- a/src/trace_redaction/process_thread_timeline.h
+++ b/src/trace_redaction/process_thread_timeline.h
@@ -82,15 +82,6 @@
     uint64_t uid = kUnknownUid;
   };
 
-  // The state of a process at a specific point in time.
-  struct Slice {
-    int32_t pid = -1;
-
-    // It is safe to use 0 as the invalid value because that's effectively
-    // what happening in the trace.
-    uint64_t uid = 0;
-  };
-
   ProcessThreadTimeline() = default;
 
   ProcessThreadTimeline(const ProcessThreadTimeline&) = delete;
@@ -105,12 +96,8 @@
   // subset of events will, on average, be trivally small.
   void Sort();
 
-  // Returns a snapshot that contains a process's pid and ppid, but contains the
-  // first uid found in its parent-child chain. If a uid cannot be found, uid=0
-  // is returned.
-  //
-  // `Sort()` must be called before this.
-  Slice Search(uint64_t ts, int32_t pid) const;
+  // Returns true if a process/thread is connected to a package.
+  bool PidConnectsToUid(uint64_t ts, int32_t pid, uint64_t uid) const;
 
   // Finds the pid's last event before ts.
   Event FindPreviousEvent(uint64_t ts, int32_t pid) const;
diff --git a/src/trace_redaction/process_thread_timeline_unittest.cc b/src/trace_redaction/process_thread_timeline_unittest.cc
index 23a9727..4e86a8a 100644
--- a/src/trace_redaction/process_thread_timeline_unittest.cc
+++ b/src/trace_redaction/process_thread_timeline_unittest.cc
@@ -193,35 +193,29 @@
 
 // PID A is directly connected to UID A.
 TEST_F(ProcessThreadTimelineIsConnectedTest, DirectPidAndUid) {
-  auto slice = timeline_.Search(kTimeB, kPidA);
-
-  ASSERT_EQ(slice.pid, kPidA);
-  ASSERT_EQ(slice.uid, kUidA);
+  ASSERT_TRUE(timeline_.PidConnectsToUid(kTimeB, kPidA, kUidA));
 }
 
 // PID B is indirectly connected to UID A through PID A.
 TEST_F(ProcessThreadTimelineIsConnectedTest, IndirectPidAndUid) {
-  auto slice = timeline_.Search(kTimeB, kPidB);
+  ASSERT_TRUE(timeline_.PidConnectsToUid(kTimeB, kPidB, kUidA));
+}
 
-  ASSERT_EQ(slice.pid, kPidB);
-  ASSERT_EQ(slice.uid, kUidA);
+// UID A and UID C are valid packages. However, PID B is connected to UID A, not
+// UID C.
+TEST_F(ProcessThreadTimelineIsConnectedTest, NotConnectedToOtherUid) {
+  ASSERT_FALSE(timeline_.PidConnectsToUid(kTimeB, kPidB, kUidC));
 }
 
 // PID D is not in the timeline, so it shouldn't be connected to anything.
 TEST_F(ProcessThreadTimelineIsConnectedTest, MissingPid) {
-  auto slice = timeline_.Search(kTimeB, kPidD);
-
-  ASSERT_EQ(slice.pid, kPidD);
-  ASSERT_EQ(slice.uid, ProcessThreadTimeline::Event::kUnknownUid);
+  ASSERT_FALSE(timeline_.PidConnectsToUid(kTimeB, kPidD, kUidA));
 }
 
 // Even through there is a connection between PID A and UID A, the query is too
 // soon (events are at TIME B, but the query is at TIME A).
 TEST_F(ProcessThreadTimelineIsConnectedTest, PrematureDirectPidAndUid) {
-  auto slice = timeline_.Search(kTimeA, kPidA);
-
-  ASSERT_EQ(slice.pid, kPidA);
-  ASSERT_EQ(slice.uid, ProcessThreadTimeline::Event::kUnknownUid);
+  ASSERT_FALSE(timeline_.PidConnectsToUid(kTimeA, kPidA, kUidA));
 }
 
 }  // namespace perfetto::trace_redaction
diff --git a/src/trace_redaction/redact_sched_switch.cc b/src/trace_redaction/redact_sched_switch.cc
index 8dca030..83cb36f 100644
--- a/src/trace_redaction/redact_sched_switch.cc
+++ b/src/trace_redaction/redact_sched_switch.cc
@@ -26,11 +26,13 @@
 
 namespace {
 
-protozero::ConstChars SanitizeCommValue(const Context& context,
-                                        ProcessThreadTimeline::Slice slice,
-                                        protozero::Field field) {
-  if (NormalizeUid(slice.uid) == NormalizeUid(context.package_uid.value())) {
-    return field.as_string();
+// TODO(vaage): Merge with RedactComm in redact_task_newtask.cc.
+protozero::ConstChars RedactComm(const Context& context,
+                                 uint64_t ts,
+                                 int32_t pid,
+                                 protozero::ConstChars comm) {
+  if (context.timeline->PidConnectsToUid(ts, pid, *context.package_uid)) {
+    return comm;
   }
 
   return {};
@@ -105,22 +107,19 @@
   // Avoid making the message until we know that we have prev and next pids.
   auto sched_switch_message = event_message->set_sched_switch();
 
-  auto prev_slice =
-      context.timeline->Search(timestamp.as_uint64(), prev_pid.as_int32());
-  auto next_slice =
-      context.timeline->Search(timestamp.as_uint64(), next_pid.as_int32());
-
   for (auto field = sched_switch_decoder.ReadField(); field.valid();
        field = sched_switch_decoder.ReadField()) {
     switch (field.id()) {
       case protos::pbzero::SchedSwitchFtraceEvent::kNextCommFieldNumber:
         sched_switch_message->set_next_comm(
-            SanitizeCommValue(context, next_slice, field));
+            RedactComm(context, timestamp.as_uint64(), next_pid.as_int32(),
+                       field.as_string()));
         break;
 
       case protos::pbzero::SchedSwitchFtraceEvent::kPrevCommFieldNumber:
         sched_switch_message->set_prev_comm(
-            SanitizeCommValue(context, prev_slice, field));
+            RedactComm(context, timestamp.as_uint64(), prev_pid.as_int32(),
+                       field.as_string()));
         break;
 
       default:
diff --git a/src/trace_redaction/redact_task_newtask.cc b/src/trace_redaction/redact_task_newtask.cc
index c942612..fb0cfe8 100644
--- a/src/trace_redaction/redact_task_newtask.cc
+++ b/src/trace_redaction/redact_task_newtask.cc
@@ -26,18 +26,19 @@
 
 namespace {
 
-protozero::ConstChars SanitizeCommValue(const Context& context,
-                                        ProcessThreadTimeline::Slice slice,
-                                        protozero::Field field) {
-  if (NormalizeUid(slice.uid) == NormalizeUid(context.package_uid.value())) {
-    return field.as_string();
+// TODO(vaage): Merge with RedactComm in redact_sched_switch.cc.
+protozero::ConstChars RedactComm(const Context& context,
+                                 uint64_t ts,
+                                 int32_t pid,
+                                 protozero::ConstChars comm) {
+  if (context.timeline->PidConnectsToUid(ts, pid, *context.package_uid)) {
+    return comm;
   }
 
   return {};
 }
 
 }  // namespace
-
 // Redact sched switch trace events in an ftrace event bundle:
 //
 // event {
@@ -93,8 +94,6 @@
   // Avoid making the message until we know that we have prev and next pids.
   auto* new_task_message = event_message->set_task_newtask();
 
-  auto slice = context.timeline->Search(timestamp.as_uint64(), pid.as_int32());
-
   for (auto field = new_task_decoder.ReadField(); field.valid();
        field = new_task_decoder.ReadField()) {
     // Perfetto view (ui.perfetto.dev) crashes if the comm value is missing.
@@ -102,7 +101,8 @@
     // This appears to work.
     if (field.id() ==
         protos::pbzero::TaskNewtaskFtraceEvent::kCommFieldNumber) {
-      new_task_message->set_comm(SanitizeCommValue(context, slice, field));
+      new_task_message->set_comm(RedactComm(context, timestamp.as_uint64(),
+                                            pid.as_int32(), field.as_string()));
     } else {
       proto_util::AppendField(field, new_task_message);
     }
diff --git a/src/trace_redaction/remap_scheduling_events.cc b/src/trace_redaction/remap_scheduling_events.cc
index 4817f19..be0973e 100644
--- a/src/trace_redaction/remap_scheduling_events.cc
+++ b/src/trace_redaction/remap_scheduling_events.cc
@@ -32,14 +32,18 @@
   PERFETTO_DCHECK(context.package_uid.value());
   PERFETTO_DCHECK(cpu < context.synthetic_threads->tids.size());
 
-  auto slice = context.timeline->Search(timestamp, pid);
+  // PID 0 is used for CPU idle. If it was to get re-mapped, threading
+  // information get corrupted.
+  if (pid == 0) {
+    return 0;
+  }
 
-  auto expected_uid = NormalizeUid(slice.uid);
-  auto actual_uid = NormalizeUid(context.package_uid.value());
+  if (context.timeline->PidConnectsToUid(timestamp, pid,
+                                         *context.package_uid)) {
+    return pid;
+  }
 
-  return !pid || expected_uid == actual_uid
-             ? pid
-             : context.synthetic_threads->tids[cpu];
+  return context.synthetic_threads->tids[cpu];
 }
 }  // namespace
 
diff --git a/src/trace_redaction/scrub_process_stats.cc b/src/trace_redaction/scrub_process_stats.cc
index 19d4048..78c983a 100644
--- a/src/trace_redaction/scrub_process_stats.cc
+++ b/src/trace_redaction/scrub_process_stats.cc
@@ -57,9 +57,6 @@
   PERFETTO_DCHECK(time_field.valid());
   auto time = time_field.as_uint64();
 
-  auto* timeline = context.timeline.get();
-  auto uid = context.package_uid.value();
-
   for (auto packet_field = packet_decoder.ReadField(); packet_field.valid();
        packet_field = packet_decoder.ReadField()) {
     if (packet_field.id() !=
@@ -83,8 +80,9 @@
         protozero::ProtoDecoder process_decoder(process_stats_field.as_bytes());
         auto pid = process_decoder.FindField(
             protos::pbzero::ProcessStats::Process::kPidFieldNumber);
-        keep_field =
-            pid.valid() && timeline->Search(time, pid.as_int32()).uid == uid;
+
+        keep_field = context.timeline->PidConnectsToUid(time, pid.as_int32(),
+                                                        *context.package_uid);
       } else {
         keep_field = true;
       }
diff --git a/src/trace_redaction/scrub_process_trees.cc b/src/trace_redaction/scrub_process_trees.cc
index 989d094..a3f1f3b 100644
--- a/src/trace_redaction/scrub_process_trees.cc
+++ b/src/trace_redaction/scrub_process_trees.cc
@@ -43,14 +43,10 @@
     return;
   }
 
-  auto slice = context.timeline->Search(timestamp.as_uint64(), pid.as_int32());
-
-  // Only keep the target process cmdline.
-  if (NormalizeUid(slice.uid) != NormalizeUid(context.package_uid.value())) {
-    return;
+  if (context.timeline->PidConnectsToUid(timestamp.as_uint64(), pid.as_int32(),
+                                         *context.package_uid)) {
+    proto_util::AppendField(value, message);
   }
-
-  proto_util::AppendField(value, message);
 }
 
 }  // namespace
diff --git a/src/tracing/track.cc b/src/tracing/track.cc
index b7d9795..02e6e03 100644
--- a/src/tracing/track.cc
+++ b/src/tracing/track.cc
@@ -49,6 +49,13 @@
   desc->AppendRawProtoBytes(bytes.data(), bytes.size());
 }
 
+// static
+Track Track::ThreadScoped(const void* ptr, Track parent) {
+  if (parent.uuid == 0)
+    return Track::FromPointer(ptr, ThreadTrack::Current());
+  return Track::FromPointer(ptr, parent);
+}
+
 protos::gen::TrackDescriptor ProcessTrack::Serialize() const {
   auto desc = Track::Serialize();
   auto pd = desc.mutable_process();
diff --git a/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto b/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto
index b5a4aed..d3395a3 100644
--- a/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto
+++ b/test/trace_processor/diff_tests/parser/profiling/heap_profile_tracker_twoheaps.textproto
@@ -39,7 +39,7 @@
     index: 0
     continued: false
     process_dumps {
-      heap_name: "malloc"
+      heap_name: "libc.malloc"
       samples {
         callstack_id: 1
         self_allocated: 1
diff --git a/test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py b/test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py
index 3eead2a..205e9e4 100644
--- a/test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py
+++ b/test/trace_processor/diff_tests/parser/profiling/tests_heap_graph.py
@@ -361,10 +361,10 @@
         """,
         out=Csv("""
         "id","type","ts","upid","heap_name","callsite_id","count","size"
-        0,"heap_profile_allocation",0,0,"malloc",0,1,1
-        1,"heap_profile_allocation",0,0,"malloc",0,-1,-1
-        2,"heap_profile_allocation",1,0,"malloc",0,1,1
-        3,"heap_profile_allocation",1,0,"malloc",0,-1,-1
+        0,"heap_profile_allocation",0,0,"unknown",0,1,1
+        1,"heap_profile_allocation",0,0,"unknown",0,-1,-1
+        2,"heap_profile_allocation",1,0,"unknown",0,1,1
+        3,"heap_profile_allocation",1,0,"unknown",0,-1,-1
         """))
 
   def test_heap_profile_tracker_twoheaps(self):
@@ -375,8 +375,8 @@
         """,
         out=Csv("""
         "id","type","ts","upid","heap_name","callsite_id","count","size"
-        0,"heap_profile_allocation",0,0,"malloc",0,1,1
-        1,"heap_profile_allocation",0,0,"malloc",0,-1,-1
+        0,"heap_profile_allocation",0,0,"libc.malloc",0,1,1
+        1,"heap_profile_allocation",0,0,"libc.malloc",0,-1,-1
         2,"heap_profile_allocation",0,0,"custom",0,1,1
         3,"heap_profile_allocation",0,0,"custom",0,-1,-1
         """))
diff --git a/test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py b/test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py
index 483d045..ebf21c7 100644
--- a/test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py
+++ b/test/trace_processor/diff_tests/parser/profiling/tests_heap_profiling.py
@@ -59,8 +59,8 @@
         """,
         out=Csv("""
         "id","type","ts","upid","heap_name","callsite_id","count","size"
-        0,"heap_profile_allocation",-10,2,"malloc",2,0,1000
-        1,"heap_profile_allocation",-10,2,"malloc",3,0,90
+        0,"heap_profile_allocation",-10,2,"unknown",2,0,1000
+        1,"heap_profile_allocation",-10,2,"unknown",3,0,90
         """))
 
   def test_heap_profile_dump_max(self):
@@ -71,6 +71,6 @@
         """,
         out=Csv("""
         "id","type","ts","upid","heap_name","callsite_id","count","size"
-        0,"heap_profile_allocation",-10,2,"malloc",2,6,1000
-        1,"heap_profile_allocation",-10,2,"malloc",3,1,90
+        0,"heap_profile_allocation",-10,2,"unknown",2,6,1000
+        1,"heap_profile_allocation",-10,2,"unknown",3,1,90
         """))
diff --git a/tools/gen_tp_table_headers.py b/tools/gen_tp_table_headers.py
index 6d7e044..c53602e 100755
--- a/tools/gen_tp_table_headers.py
+++ b/tools/gen_tp_table_headers.py
@@ -70,7 +70,10 @@
     return module_path[module_path.rfind(os.sep + 'src') + 1:]
 
   modules = [
-      os.path.splitext(get_relin_path(i).replace('/', '.'))[0]
+      # On Windows the path can contain '/' or os.sep, depending on how this
+      # script is executed. So we need to replace both.
+      os.path.splitext(
+          get_relin_path(i).replace('/', '.').replace(os.sep, '.'))[0]
       for i in args.inputs
   ]
   headers: Dict[str, Header] = {}
diff --git a/ui/BUILD.bazel b/ui/BUILD.bazel
deleted file mode 100644
index c3c05f2..0000000
--- a/ui/BUILD.bazel
+++ /dev/null
@@ -1,6 +0,0 @@
-load("@perfetto_cfg//:perfetto_cfg.bzl", "PERFETTO_CONFIG")
-
-exports_files(
-    ["src/assets/favicon.png"],
-    visibility = PERFETTO_CONFIG.public_visibility,
-)
\ No newline at end of file
diff --git a/ui/build.js b/ui/build.js
index 698842c..56745df 100644
--- a/ui/build.js
+++ b/ui/build.js
@@ -110,11 +110,13 @@
   outDistDir: '',
   outExtDir: '',
   outBigtraceDistDir: '',
+  outOpenPerfettoTraceDistDir: '',
 };
 
 const RULES = [
   {r: /ui\/src\/assets\/index.html/, f: copyIndexHtml},
   {r: /ui\/src\/assets\/bigtrace.html/, f: copyBigtraceHtml},
+  {r: /ui\/src\/open_perfetto_trace\/index.html/, f: copyOpenPerfettoTraceHtml},
   {r: /ui\/src\/assets\/((.*)[.]png)/, f: copyAssets},
   {r: /buildtools\/typefaces\/(.+[.]woff2)/, f: copyAssets},
   {r: /buildtools\/catapult_trace_viewer\/(.+(js|html))/, f: copyAssets},
@@ -149,6 +151,7 @@
   parser.add_argument('--run-integrationtests', '-T', {action: 'store_true'});
   parser.add_argument('--debug', '-d', {action: 'store_true'});
   parser.add_argument('--bigtrace', {action: 'store_true'});
+  parser.add_argument('--open-perfetto-trace', {action: 'store_true'});
   parser.add_argument('--interactive', '-i', {action: 'store_true'});
   parser.add_argument('--rebaseline', '-r', {action: 'store_true'});
   parser.add_argument('--no-depscheck', {action: 'store_true'});
@@ -175,11 +178,16 @@
   cfg.verbose = !!args.verbose;
   cfg.debug = !!args.debug;
   cfg.bigtrace = !!args.bigtrace;
+  cfg.openPerfettoTrace = !!args.open_perfetto_trace;
   cfg.startHttpServer = args.serve;
   cfg.noOverrideGnArgs = !!args.no_override_gn_args;
   if (args.bigtrace) {
     cfg.outBigtraceDistDir = ensureDir(pjoin(cfg.outDistDir, 'bigtrace'));
   }
+  if (cfg.openPerfettoTrace) {
+    cfg.outOpenPerfettoTraceDistDir = ensureDir(pjoin(cfg.outDistRootDir,
+                                                      'open_perfetto_trace'));
+  }
   if (args.serve_host) {
     cfg.httpServerListenHost = args.serve_host;
   }
@@ -247,17 +255,25 @@
     generateImports('ui/src/plugins', 'all_plugins.ts');
     compileProtos();
     genVersion();
-    transpileTsProject('ui');
-    transpileTsProject('ui/src/service_worker');
-    if (cfg.bigtrace) {
-      transpileTsProject('ui/src/bigtrace');
+
+    const tsProjects = [
+      'ui',
+      'ui/src/service_worker'
+    ];
+    if (cfg.bigtrace) tsProjects.push('ui/src/bigtrace');
+    if (cfg.openPerfettoTrace) {
+      scanDir('ui/src/open_perfetto_trace');
+      tsProjects.push('ui/src/open_perfetto_trace');
+    }
+
+
+    for (const prj of tsProjects) {
+      transpileTsProject(prj);
     }
 
     if (cfg.watch) {
-      transpileTsProject('ui', {watch: cfg.watch});
-      transpileTsProject('ui/src/service_worker', {watch: cfg.watch});
-      if (cfg.bigtrace) {
-        transpileTsProject('ui/src/bigtrace', {watch: cfg.watch});
+      for (const prj of tsProjects) {
+        transpileTsProject(prj, {watch: cfg.watch});
       }
     }
 
@@ -350,6 +366,12 @@
   }
 }
 
+function copyOpenPerfettoTraceHtml(src) {
+  if (cfg.openPerfettoTrace) {
+    addTask(cp, [src, pjoin(cfg.outOpenPerfettoTraceDistDir, 'index.html')]);
+  }
+}
+
 function copyAssets(src, dst) {
   addTask(cp, [src, pjoin(cfg.outDistDir, 'assets', dst)]);
   if (cfg.bigtrace) {
@@ -519,6 +541,9 @@
   if (cfg.bigtrace) {
     args.push('--environment', 'ENABLE_BIGTRACE:true');
   }
+  if (cfg.openPerfettoTrace) {
+    args.push('--environment', 'ENABLE_OPEN_PERFETTO_TRACE:true');
+  }
   args.push(...(cfg.verbose ? [] : ['--silent']));
   if (cfg.watch) {
     // --waitForBundleInput is sadly quite busted so it is required ts
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
index f87a99e..144f5d9 100644
--- a/ui/config/rollup.config.js
+++ b/ui/config/rollup.config.js
@@ -97,10 +97,15 @@
     [defBundle('tsc/bigtrace', 'bigtrace', 'dist_version/bigtrace')] :
     [];
 
+const maybeOpenPerfettoTrace = process.env['ENABLE_OPEN_PERFETTO_TRACE'] ?
+    [defBundle('tsc', 'open_perfetto_trace', 'dist/open_perfetto_trace')] :
+    [];
+
+
 export default [
   defBundle('tsc', 'frontend', 'dist_version'),
   defBundle('tsc', 'engine', 'dist_version'),
   defBundle('tsc', 'traceconv', 'dist_version'),
   defBundle('tsc', 'chrome_extension', 'chrome_extension'),
   defServiceWorkerBundle(),
-].concat(maybeBigtrace);
+].concat(maybeBigtrace).concat(maybeOpenPerfettoTrace);
diff --git a/ui/release/channels.json b/ui/release/channels.json
index c961c52..c80cab9 100644
--- a/ui/release/channels.json
+++ b/ui/release/channels.json
@@ -6,7 +6,7 @@
     },
     {
       "name": "canary",
-      "rev": "9945c86178de704ff44bef7f64c020ff79aedd28"
+      "rev": "4fad83137e7ce34736f5238b7667b1dbdeeb5e2a"
     },
     {
       "name": "autopush",
diff --git a/ui/src/bigtrace/index.ts b/ui/src/bigtrace/index.ts
index d54bd87..739594e 100644
--- a/ui/src/bigtrace/index.ts
+++ b/ui/src/bigtrace/index.ts
@@ -18,7 +18,7 @@
 import m from 'mithril';
 
 import {defer} from '../base/deferred';
-import {reportError, setErrorHandler} from '../base/logging';
+import {reportError, addErrorHandler, ErrorDetails} from '../base/logging';
 import {initLiveReloadIfLocalhost} from '../core/live_reload';
 import {raf} from '../core/raf_scheduler';
 import {setScheduleFullRedraw} from '../widgets/raf';
@@ -92,7 +92,7 @@
   document.head.append(css);
 
   // Add Error handlers for JS error and for uncaught exceptions in promises.
-  setErrorHandler((err: string) => console.log(err));
+  addErrorHandler((err: ErrorDetails) => console.log(err.message, err.stack));
   window.addEventListener('error', (e) => reportError(e));
   window.addEventListener('unhandledrejection', (e) => reportError(e));
 
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..7ca7a74 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'
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/open_perfetto_trace/index.html b/ui/src/open_perfetto_trace/index.html
new file mode 100644
index 0000000..1fb52e3
--- /dev/null
+++ b/ui/src/open_perfetto_trace/index.html
@@ -0,0 +1,59 @@
+<!doctype html>
+<html lang='en-us'>
+<head>
+  <script src="open_perfetto_trace_bundle.js"></script>
+  <style type="text/css">
+  html { font-family: Roboto, sans-serif; }
+  main {display: flex; flex-direction: column; max-width: 800px;}
+  main > * { margin: 5px; }
+  </style>
+</head>
+<body>
+
+
+<main>
+  <select id='trace_source' size='5'>
+    <option>https://storage.googleapis.com/perfetto-misc/example_android_trace_15s</option>
+    <option selected>https://storage.googleapis.com/perfetto-misc/chrome_example_wikipedia.perfetto_trace.gz</option>
+  </select>
+  <label>Or select a local file: <input type="file" id="file"></label>
+  <input type='button' value='Fetch and open selected trace' id='fetch'>
+  <label><input type='checkbox' id='show_progress' checked="checked">Show progress dialog</label>
+  <label><input type='checkbox' id='new_tab'>Open in new tab</label>
+  <label><input type='checkbox' id='hide_sidebar'>Hide sidebar in Perfetto UI</label>
+
+</main>
+
+<script type='text/javascript'>
+
+function getCheckbox(id) {
+  return document.getElementById(id).checked;
+}
+
+document.getElementById('fetch').addEventListener('click', () => {
+  const opts = {};
+
+  if (location.host.startsWith('127.0.0.1') ||
+      location.host.startsWith('localhost')) {
+        opts.uiUrl = `${location.protocol}//${location.host}`;
+  }
+
+  opts.statusDialog = getCheckbox('show_progress');
+  opts.newTab = getCheckbox('new_tab');
+  opts.hideSidebar = getCheckbox('hide_sidebar');
+
+  const fileInput = document.getElementById('file');
+  let traceSource;
+  if (fileInput.files.length > 0) {
+    traceSource = fileInput.files[0]
+  } else {
+    traceSource = document.getElementById('trace_source').value;
+  }
+
+  open_perfetto_trace(traceSource, opts);
+});
+
+</script>
+</body>
+</html>
+
diff --git a/ui/src/open_perfetto_trace/index.ts b/ui/src/open_perfetto_trace/index.ts
new file mode 100644
index 0000000..5462783
--- /dev/null
+++ b/ui/src/open_perfetto_trace/index.ts
@@ -0,0 +1,191 @@
+// 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.
+
+// open-perfetto-trace is a standalone JS/TS library that can be used in other
+// projects to facilitate the deep linking into perfetto. It allows opening
+// trace files or URL with ui.perfetto.dev, handling all the handshake with it.
+
+const PERFETTO_UI_URL = 'https://ui.perfetto.dev';
+
+interface OpenTraceOptions {
+  // If true (default) shows a popup dialog with a progress bar that informs
+  // about the status of the fetch. This is only relevant when the trace source
+  // is a url.
+  statusDialog?: boolean;
+
+  // Opens the trace in a new tab.
+  newTab?: boolean;
+
+  // Override the referrer. Useful for scripts such as
+  // record_android_trace to record where the trace is coming from.
+  referrer?: string;
+
+  // For the 'mode' of the UI. For example when the mode is 'embedded'
+  // some features are disabled.
+  mode: 'embedded' | undefined;
+
+  // Hides the sidebar in the opened perfetto UI.
+  hideSidebar?: boolean;
+
+  ts?: string;
+
+  dur?: string;
+  tid?: string;
+  pid?: string;
+  query?: string;
+  visStart?: string;
+  visEnd?: string;
+
+  // Used to override ui.perfetto.dev with a custom hosted URL.
+  // Useful for testing.
+  uiUrl?: string;
+}
+
+// Opens a trace in the Perfetto UI.
+// `source` can be either:
+// - A blob (e.g. a File).
+// - A URL.
+export default function openPerfettoTrace(
+  source: Blob | string,
+  opts?: OpenTraceOptions,
+) {
+  if (source instanceof Blob) {
+    return openTraceBlob(source, opts);
+  } else if (typeof source === 'string') {
+    return fetchAndOpenTrace(source, opts);
+  }
+}
+
+function openTraceBlob(blob: Blob, opts?: OpenTraceOptions) {
+  const form = document.createElement('form');
+  form.method = 'POST';
+  form.style.visibility = 'hidden';
+  form.enctype = 'multipart/form-data';
+  const uiUrl = opts?.uiUrl ?? PERFETTO_UI_URL;
+  form.action = `${uiUrl}/_open_trace/${Date.now()}`;
+  if (opts?.newTab === true) {
+    form.target = '_blank';
+  }
+  const fileInput = document.createElement('input');
+  fileInput.name = 'trace';
+  fileInput.type = 'file';
+  const dataTransfer = new DataTransfer();
+  dataTransfer.items.add(new File([blob], 'trace.file'));
+  fileInput.files = dataTransfer.files;
+  form.appendChild(fileInput);
+  for (const [key, value] of Object.entries(opts ?? {})) {
+    const varInput = document.createElement('input');
+    varInput.type = 'hidden';
+    varInput.name = key;
+    varInput.value = value;
+    form.appendChild(varInput);
+  }
+  document.body.appendChild(form);
+  form.submit();
+}
+
+function fetchAndOpenTrace(url: string, opts?: OpenTraceOptions) {
+  updateProgressDiv({status: 'Fetching trace'}, opts);
+  const xhr = new XMLHttpRequest();
+  xhr.addEventListener('progress', (event) => {
+    if (event.lengthComputable) {
+      updateProgressDiv(
+        {
+          status: `Fetching trace (${Math.round(event.loaded / 1000)} KB)`,
+          progress: event.loaded / event.total,
+        },
+        opts,
+      );
+    }
+  });
+  xhr.addEventListener('loadend', () => {
+    if (xhr.readyState === 4 && xhr.status === 200) {
+      const blob = xhr.response as Blob;
+      updateProgressDiv({status: 'Opening trace'}, opts);
+      openTraceBlob(blob, opts);
+      updateProgressDiv({close: true}, opts);
+    }
+  });
+  xhr.addEventListener('error', () => {
+    updateProgressDiv({status: 'Failed to fetch trace'}, opts);
+  });
+  xhr.responseType = 'blob';
+  xhr.overrideMimeType('application/octet-stream');
+  xhr.open('GET', url);
+  xhr.send();
+}
+
+interface ProgressDivOpts {
+  status?: string;
+  progress?: number;
+  close?: boolean;
+}
+
+function updateProgressDiv(progress: ProgressDivOpts, opts?: OpenTraceOptions) {
+  if (opts?.statusDialog === false) return;
+
+  const kDivId = 'open_perfetto_trace';
+  let div = document.getElementById(kDivId);
+  if (!div) {
+    div = document.createElement('div');
+    div.id = kDivId;
+    div.style.all = 'initial';
+    div.style.position = 'fixed';
+    div.style.bottom = '10px';
+    div.style.left = '0';
+    div.style.right = '0';
+    div.style.width = 'fit-content';
+    div.style.height = '20px';
+    div.style.padding = '10px';
+    div.style.zIndex = '99';
+    div.style.margin = 'auto';
+    div.style.backgroundColor = '#fff';
+    div.style.color = '#333';
+    div.style.fontFamily = 'monospace';
+    div.style.fontSize = '12px';
+    div.style.border = '1px solid #eee';
+    div.style.boxShadow = '0 0 20px #aaa';
+    div.style.display = 'flex';
+    div.style.flexDirection = 'column';
+
+    const title = document.createElement('div');
+    title.className = 'perfetto-open-title';
+    title.innerText = 'Opening perfetto trace';
+    title.style.fontWeight = '12px';
+    title.style.textAlign = 'center';
+    div.appendChild(title);
+
+    const progressbar = document.createElement('progress');
+    progressbar.className = 'perfetto-open-progress';
+    progressbar.style.width = '200px';
+    progressbar.value = 0;
+    div.appendChild(progressbar);
+
+    document.body.appendChild(div);
+  }
+  const title = div.querySelector('.perfetto-open-title') as HTMLElement;
+  if (progress.status !== undefined) {
+    title.innerText = progress.status;
+  }
+  const bar = div.querySelector('.perfetto-open-progress') as HTMLInputElement;
+  if (progress.progress === undefined) {
+    bar.style.visibility = 'hidden';
+  } else {
+    bar.style.visibility = 'visible';
+    bar.value = `${progress.progress}`;
+  }
+  if (progress.close === true) {
+    div.remove();
+  }
+}
diff --git a/ui/src/open_perfetto_trace/tsconfig.json b/ui/src/open_perfetto_trace/tsconfig.json
new file mode 100644
index 0000000..66bf290
--- /dev/null
+++ b/ui/src/open_perfetto_trace/tsconfig.json
@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.base.json",
+  "include": [ "." ],
+  "exclude": [
+    "../gen/"
+  ],
+  "compilerOptions": {
+    "outDir": "../../out/tsc/open_perfetto_trace",
+    "lib": [
+      "dom",                               // Need to be explicitly mentioned now since we're overriding default included libs.
+      "es2021",                            // Need this to use Promise.allSettled, replaceAll, etc
+    ],
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+  }
+}
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/service_worker/service_worker.ts b/ui/src/service_worker/service_worker.ts
index dc467f0..699db43 100644
--- a/ui/src/service_worker/service_worker.ts
+++ b/ui/src/service_worker/service_worker.ts
@@ -45,6 +45,7 @@
 
 const LOG_TAG = `ServiceWorker: `;
 const CACHE_NAME = 'ui-perfetto-dev';
+const OPEN_TRACE_PREFIX = '/_open_trace'
 
 // If the fetch() for the / doesn't respond within 3s, return a cached version.
 // This is to avoid that a user waits too much if on a flaky network.
@@ -54,6 +55,9 @@
 // in the background.
 const INSTALL_TIMEOUT_MS = 30000;
 
+// Files passed to POST /_open_trace/NNNN.
+let postedFiles = new Map<string, File>();
+
 // The install() event is fired:
 // 1. On the first visit, when there is no SW installed.
 // 2. Every time the user opens the site and the version has been updated (they
@@ -128,6 +132,7 @@
 });
 
 self.addEventListener('fetch', (event) => {
+
   // The early return here will cause the browser to fall back on standard
   // network-based fetch.
   if (!shouldHandleHttpRequest(event.request)) {
@@ -149,6 +154,8 @@
 
   const url = new URL(req.url);
   if (url.pathname === '/live_reload') return false;
+  if (url.pathname.startsWith(OPEN_TRACE_PREFIX)) return true;
+
   return req.method === 'GET' && url.origin === self.location.origin;
 }
 
@@ -184,6 +191,8 @@
     // network fetch.
     const cachedRes = await caches.match(new Request('/'), cacheOps);
     if (cachedRes) return cachedRes;
+  } else if (url.pathname.startsWith(OPEN_TRACE_PREFIX)) {
+    return await handleOpenTraceRequest(req);
   }
 
   const cachedRes = await caches.match(req, cacheOps);
@@ -198,6 +207,60 @@
   return fetch(req);
 }
 
+// Handles GET and POST requests to /_open_trace/NNNN, where NNNN is typically a
+// random token generated by the client.
+// This works as follows:
+// - The client does a POST request to /_open_trace/NNNN passing the trace blob
+//   as multipart-data, alongside other options like hideSidebar & co that we
+//   support in the usual querystring (see router.ts)
+// - The SW takes the file and puts it in the global variable `postedFiles`.
+// - The SW responds to the POST request with a redirect to
+//   ui.perfetto.dev/#!/?url=https://ui.perfetto.dev/_open_trace/NNNN&other_args
+// - When the new ui.perfetto.dev is reloaded, it will naturally try to fetch
+//   the trace from /_open_trace/NNNN, this time via a GET request.
+// - The SW intercepts the GET request and returns the file previosly stored in
+//   `postedFiles`.
+// We use postedFiles here to handle the case of progammatically POST-ing to >1
+// instances of ui.perfetto.dev simultaneously, to avoid races.
+// Note that we should not use a global variable for `postedFiles` but we should
+// use the CacheAPI because, technically speaking, the SW could be disposed
+// and respawned in between the POST and the GET request. In practice, however,
+// SWs are disposed only after 30s seconds of idleness. The POST->GET requests
+// happen back-to-back..
+async function handleOpenTraceRequest(req: Request): Promise<Response> {
+  const url = new URL(req.url);
+  console.assert(url.pathname.startsWith(OPEN_TRACE_PREFIX));
+  const fileKey = url.pathname.substring(OPEN_TRACE_PREFIX.length);
+  if (req.method === 'POST') {
+    const formData = await req.formData();
+    const qsParams = new URLSearchParams();
+    // Iterate over the POST fields and copy them over the querystring in
+    // the hash, with the exception of the trace file. The trace file is
+    // kept in the serviceworker and passed as a url= argument.
+    formData.forEach((value, key) => {
+      if (key === 'trace') {
+        if (value instanceof File) {
+          postedFiles.set(fileKey, value);
+          qsParams.set('url', req.url);
+        }
+        return;
+      }
+      qsParams.set(key, `${value}`);
+    });  // formData.forEach()
+    return Response.redirect(`${url.protocol}//${url.host}/#!/?${qsParams}`);
+  }
+
+  // else... method == 'GET'
+  const file = postedFiles.get(fileKey);
+  if (file !== undefined) {
+    postedFiles.delete(fileKey);
+    return new Response(file);
+  }
+
+  // The file /_open_trace/NNNN does not exist.
+  return Response.error();
+}
+
 async function installAppVersionIntoCache(version: string) {
   const manifestUrl = `${version}/manifest.json`;
   try {
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;
   }