Merge "ui: Move traceTime out of state" into main
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/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/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 a2fd592..7c9766c 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -26,21 +26,22 @@
   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';
@@ -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,
@@ -1217,3 +1144,77 @@
   }
   return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd);
 }
+
+async function getTraceTimeDetails(engine: Engine): 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/frontend/app.ts b/ui/src/frontend/app.ts
index 18adc7f..09b1036 100644
--- a/ui/src/frontend/app.ts
+++ b/ui/src/frontend/app.ts
@@ -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/globals.ts b/ui/src/frontend/globals.ts
index bdb46ac..04cdc17 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -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;
 
@@ -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/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/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/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) {