• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2018 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15import {BigintMath} from '../base/bigint_math';
16import {assertExists, assertUnreachable} from '../base/logging';
17import {createStore, Store} from '../base/store';
18import {duration, Span, Time, time, TimeSpan} from '../base/time';
19import {Actions, DeferredAction} from '../common/actions';
20import {AggregateData} from '../common/aggregation_data';
21import {Args} from '../common/arg_types';
22import {CommandManager} from '../common/commands';
23import {
24  ConversionJobName,
25  ConversionJobStatus,
26} from '../common/conversion_jobs';
27import {createEmptyState} from '../common/empty_state';
28import {
29  HighPrecisionTime,
30  HighPrecisionTimeSpan,
31} from '../common/high_precision_time';
32import {MetricResult} from '../common/metric_data';
33import {CurrentSearchResults, SearchSummary} from '../common/search_data';
34import {
35  EngineConfig,
36  RESOLUTION_DEFAULT,
37  State,
38  getLegacySelection,
39} from '../common/state';
40import {TabManager} from '../common/tab_registry';
41import {TimestampFormat, timestampFormat} from '../core/timestamp_format';
42import {TrackManager} from '../common/track_cache';
43import {setPerfHooks} from '../core/perf';
44import {raf} from '../core/raf_scheduler';
45import {EngineBase} from '../trace_processor/engine';
46import {HttpRpcState} from '../trace_processor/http_rpc_engine';
47
48import {Analytics, initAnalytics} from './analytics';
49import {Timeline} from './frontend_local_state';
50import {Router} from './router';
51import {horizontalScrollToTs} from './scroll_helper';
52import {ServiceWorkerController} from './service_worker_controller';
53import {SliceSqlId} from './sql_types';
54import {PxSpan, TimeScale} from './time_scale';
55import {SelectionManager, LegacySelection} from '../core/selection_manager';
56import {Optional, exists} from '../base/utils';
57import {OmniboxManager} from './omnibox_manager';
58import {CallsiteInfo} from '../common/flamegraph_util';
59import {FlamegraphCache} from '../core/flamegraph_cache';
60
61const INSTANT_FOCUS_DURATION = 1n;
62const INCOMPLETE_SLICE_DURATION = 30_000n;
63
64type Dispatch = (action: DeferredAction) => void;
65type TrackDataStore = Map<string, {}>;
66type QueryResultsStore = Map<string, {} | undefined>;
67type AggregateDataStore = Map<string, AggregateData>;
68type Description = Map<string, string>;
69
70export interface SliceDetails {
71  ts?: time;
72  absTime?: string;
73  dur?: duration;
74  threadTs?: time;
75  threadDur?: duration;
76  priority?: number;
77  endState?: string | null;
78  cpu?: number;
79  id?: number;
80  threadStateId?: number;
81  utid?: number;
82  wakeupTs?: time;
83  wakerUtid?: number;
84  wakerCpu?: number;
85  category?: string;
86  name?: string;
87  tid?: number;
88  threadName?: string;
89  pid?: number;
90  processName?: string;
91  uid?: number;
92  packageName?: string;
93  versionCode?: number;
94  args?: Args;
95  description?: Description;
96}
97
98export interface FlowPoint {
99  trackId: number;
100
101  sliceName: string;
102  sliceCategory: string;
103  sliceId: SliceSqlId;
104  sliceStartTs: time;
105  sliceEndTs: time;
106  // Thread and process info. Only set in sliceSelected not in areaSelected as
107  // the latter doesn't display per-flow info and it'd be a waste to join
108  // additional tables for undisplayed info in that case. Nothing precludes
109  // adding this in a future iteration however.
110  threadName: string;
111  processName: string;
112
113  depth: number;
114
115  // TODO(altimin): Ideally we should have a generic mechanism for allowing to
116  // customise the name here, but for now we are hardcording a few
117  // Chrome-specific bits in the query here.
118  sliceChromeCustomName?: string;
119}
120
121export interface Flow {
122  id: number;
123
124  begin: FlowPoint;
125  end: FlowPoint;
126  dur: duration;
127
128  // Whether this flow connects a slice with its descendant.
129  flowToDescendant: boolean;
130
131  category?: string;
132  name?: string;
133}
134
135export interface ThreadStateDetails {
136  ts?: time;
137  dur?: duration;
138}
139
140export interface CpuProfileDetails {
141  id?: number;
142  ts?: time;
143  utid?: number;
144  stack?: CallsiteInfo[];
145}
146
147export interface QuantizedLoad {
148  start: time;
149  end: time;
150  load: number;
151}
152type OverviewStore = Map<string, QuantizedLoad[]>;
153
154export interface ThreadDesc {
155  utid: number;
156  tid: number;
157  threadName: string;
158  pid?: number;
159  procName?: string;
160  cmdline?: string;
161}
162type ThreadMap = Map<number, ThreadDesc>;
163
164function getRoot() {
165  // Works out the root directory where the content should be served from
166  // e.g. `http://origin/v1.2.3/`.
167  const script = document.currentScript as HTMLScriptElement;
168
169  // Needed for DOM tests, that do not have script element.
170  if (script === null) {
171    return '';
172  }
173
174  let root = script.src;
175  root = root.substr(0, root.lastIndexOf('/') + 1);
176  return root;
177}
178
179// Options for globals.makeSelection().
180export interface MakeSelectionOpts {
181  // Whether to switch to the current selection tab or not. Default = true.
182  switchToCurrentSelectionTab?: boolean;
183
184  // Whether to cancel the current search selection. Default = true.
185  clearSearch?: boolean;
186}
187
188// All of these control additional things we can do when doing a
189// selection.
190export interface LegacySelectionArgs {
191  clearSearch: boolean;
192  switchToCurrentSelectionTab: boolean;
193  pendingScrollId: number | undefined;
194}
195
196export interface TraceContext {
197  readonly start: time;
198  readonly end: time;
199
200  // This is the ts value at the time of the Unix epoch.
201  // Normally some large negative value, because the unix epoch is normally in
202  // the past compared to ts=0.
203  readonly realtimeOffset: time;
204
205  // This is the timestamp that we should use for our offset when in UTC mode.
206  // Usually the most recent UTC midnight compared to the trace start time.
207  readonly utcOffset: time;
208
209  // Trace TZ is like UTC but keeps into account also the timezone_off_mins
210  // recorded into the trace, to show timestamps in the device local time.
211  readonly traceTzOffset: time;
212
213  // The list of CPUs in the trace
214  readonly cpus: number[];
215
216  // The number of gpus in the trace
217  readonly gpuCount: number;
218}
219
220export const defaultTraceContext: TraceContext = {
221  start: Time.ZERO,
222  end: Time.fromSeconds(10),
223  realtimeOffset: Time.ZERO,
224  utcOffset: Time.ZERO,
225  traceTzOffset: Time.ZERO,
226  cpus: [],
227  gpuCount: 0,
228};
229
230/**
231 * Global accessors for state/dispatch in the frontend.
232 */
233class Globals {
234  readonly root = getRoot();
235
236  private _testing = false;
237  private _dispatch?: Dispatch = undefined;
238  private _store = createStore<State>(createEmptyState());
239  private _timeline?: Timeline = undefined;
240  private _serviceWorkerController?: ServiceWorkerController = undefined;
241  private _logging?: Analytics = undefined;
242  private _isInternalUser: boolean | undefined = undefined;
243
244  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
245  private _trackDataStore?: TrackDataStore = undefined;
246  private _queryResults?: QueryResultsStore = undefined;
247  private _overviewStore?: OverviewStore = undefined;
248  private _aggregateDataStore?: AggregateDataStore = undefined;
249  private _threadMap?: ThreadMap = undefined;
250  private _sliceDetails?: SliceDetails = undefined;
251  private _threadStateDetails?: ThreadStateDetails = undefined;
252  private _connectedFlows?: Flow[] = undefined;
253  private _selectedFlows?: Flow[] = undefined;
254  private _visibleFlowCategories?: Map<string, boolean> = undefined;
255  private _cpuProfileDetails?: CpuProfileDetails = undefined;
256  private _numQueriesQueued = 0;
257  private _bufferUsage?: number = undefined;
258  private _recordingLog?: string = undefined;
259  private _traceErrors?: number = undefined;
260  private _metricError?: string = undefined;
261  private _metricResult?: MetricResult = undefined;
262  private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined;
263  private _router?: Router = undefined;
264  private _embeddedMode?: boolean = undefined;
265  private _hideSidebar?: boolean = undefined;
266  private _cmdManager = new CommandManager();
267  private _tabManager = new TabManager();
268  private _trackManager = new TrackManager(this._store);
269  private _selectionManager = new SelectionManager(this._store);
270  private _hasFtrace: boolean = false;
271
272  omnibox = new OmniboxManager();
273  areaFlamegraphCache = new FlamegraphCache('area');
274
275  scrollToTrackKey?: string | number;
276  httpRpcState: HttpRpcState = {connected: false};
277  showPanningHint = false;
278  permalinkHash?: string;
279
280  traceContext = defaultTraceContext;
281
282  // TODO(hjd): Remove once we no longer need to update UUID on redraw.
283  private _publishRedraw?: () => void = undefined;
284
285  private _currentSearchResults: CurrentSearchResults = {
286    eventIds: new Float64Array(0),
287    tses: new BigInt64Array(0),
288    utids: new Float64Array(0),
289    trackKeys: [],
290    sources: [],
291    totalResults: 0,
292  };
293  searchSummary: SearchSummary = {
294    tsStarts: new BigInt64Array(0),
295    tsEnds: new BigInt64Array(0),
296    count: new Uint8Array(0),
297  };
298
299  engines = new Map<string, EngineBase>();
300
301  initialize(dispatch: Dispatch, router: Router) {
302    this._dispatch = dispatch;
303    this._router = router;
304    this._timeline = new Timeline();
305
306    setPerfHooks(
307      () => this.state.perfDebug,
308      () => this.dispatch(Actions.togglePerfDebug({})),
309    );
310
311    this._serviceWorkerController = new ServiceWorkerController();
312    this._testing =
313      /* eslint-disable @typescript-eslint/strict-boolean-expressions */
314      self.location && self.location.search.indexOf('testing=1') >= 0;
315    /* eslint-enable */
316    this._logging = initAnalytics();
317
318    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
319    this._trackDataStore = new Map<string, {}>();
320    this._queryResults = new Map<string, {}>();
321    this._overviewStore = new Map<string, QuantizedLoad[]>();
322    this._aggregateDataStore = new Map<string, AggregateData>();
323    this._threadMap = new Map<number, ThreadDesc>();
324    this._sliceDetails = {};
325    this._connectedFlows = [];
326    this._selectedFlows = [];
327    this._visibleFlowCategories = new Map<string, boolean>();
328    this._threadStateDetails = {};
329    this._cpuProfileDetails = {};
330    this.engines.clear();
331    this._selectionManager.clear();
332  }
333
334  // Only initialises the store - useful for testing.
335  initStore(initialState: State) {
336    this._store = createStore(initialState);
337  }
338
339  get router(): Router {
340    return assertExists(this._router);
341  }
342
343  get publishRedraw(): () => void {
344    return this._publishRedraw || (() => {});
345  }
346
347  set publishRedraw(f: () => void) {
348    this._publishRedraw = f;
349  }
350
351  get state(): State {
352    return assertExists(this._store).state;
353  }
354
355  get store(): Store<State> {
356    return assertExists(this._store);
357  }
358
359  get dispatch(): Dispatch {
360    return assertExists(this._dispatch);
361  }
362
363  dispatchMultiple(actions: DeferredAction[]): void {
364    const dispatch = this.dispatch;
365    for (const action of actions) {
366      dispatch(action);
367    }
368  }
369
370  get timeline() {
371    return assertExists(this._timeline);
372  }
373
374  get logging() {
375    return assertExists(this._logging);
376  }
377
378  get serviceWorkerController() {
379    return assertExists(this._serviceWorkerController);
380  }
381
382  // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
383  get overviewStore(): OverviewStore {
384    return assertExists(this._overviewStore);
385  }
386
387  get trackDataStore(): TrackDataStore {
388    return assertExists(this._trackDataStore);
389  }
390
391  get queryResults(): QueryResultsStore {
392    return assertExists(this._queryResults);
393  }
394
395  get threads() {
396    return assertExists(this._threadMap);
397  }
398
399  get sliceDetails() {
400    return assertExists(this._sliceDetails);
401  }
402
403  set sliceDetails(click: SliceDetails) {
404    this._sliceDetails = assertExists(click);
405  }
406
407  get threadStateDetails() {
408    return assertExists(this._threadStateDetails);
409  }
410
411  set threadStateDetails(click: ThreadStateDetails) {
412    this._threadStateDetails = assertExists(click);
413  }
414
415  get connectedFlows() {
416    return assertExists(this._connectedFlows);
417  }
418
419  set connectedFlows(connectedFlows: Flow[]) {
420    this._connectedFlows = assertExists(connectedFlows);
421  }
422
423  get selectedFlows() {
424    return assertExists(this._selectedFlows);
425  }
426
427  set selectedFlows(selectedFlows: Flow[]) {
428    this._selectedFlows = assertExists(selectedFlows);
429  }
430
431  get visibleFlowCategories() {
432    return assertExists(this._visibleFlowCategories);
433  }
434
435  set visibleFlowCategories(visibleFlowCategories: Map<string, boolean>) {
436    this._visibleFlowCategories = assertExists(visibleFlowCategories);
437  }
438
439  get aggregateDataStore(): AggregateDataStore {
440    return assertExists(this._aggregateDataStore);
441  }
442
443  get traceErrors() {
444    return this._traceErrors;
445  }
446
447  setTraceErrors(arg: number) {
448    this._traceErrors = arg;
449  }
450
451  get metricError() {
452    return this._metricError;
453  }
454
455  setMetricError(arg: string) {
456    this._metricError = arg;
457  }
458
459  get metricResult() {
460    return this._metricResult;
461  }
462
463  setMetricResult(result: MetricResult) {
464    this._metricResult = result;
465  }
466
467  get cpuProfileDetails() {
468    return assertExists(this._cpuProfileDetails);
469  }
470
471  set cpuProfileDetails(click: CpuProfileDetails) {
472    this._cpuProfileDetails = assertExists(click);
473  }
474
475  set numQueuedQueries(value: number) {
476    this._numQueriesQueued = value;
477  }
478
479  get numQueuedQueries() {
480    return this._numQueriesQueued;
481  }
482
483  get bufferUsage() {
484    return this._bufferUsage;
485  }
486
487  get recordingLog() {
488    return this._recordingLog;
489  }
490
491  get currentSearchResults() {
492    return this._currentSearchResults;
493  }
494
495  set currentSearchResults(results: CurrentSearchResults) {
496    this._currentSearchResults = results;
497  }
498
499  set hasFtrace(value: boolean) {
500    this._hasFtrace = value;
501  }
502
503  get hasFtrace(): boolean {
504    return this._hasFtrace;
505  }
506
507  getConversionJobStatus(name: ConversionJobName): ConversionJobStatus {
508    return this.getJobStatusMap().get(name) || ConversionJobStatus.NotRunning;
509  }
510
511  setConversionJobStatus(name: ConversionJobName, status: ConversionJobStatus) {
512    const map = this.getJobStatusMap();
513    if (status === ConversionJobStatus.NotRunning) {
514      map.delete(name);
515    } else {
516      map.set(name, status);
517    }
518  }
519
520  private getJobStatusMap(): Map<ConversionJobName, ConversionJobStatus> {
521    if (!this._jobStatus) {
522      this._jobStatus = new Map();
523    }
524    return this._jobStatus;
525  }
526
527  get embeddedMode(): boolean {
528    return !!this._embeddedMode;
529  }
530
531  set embeddedMode(value: boolean) {
532    this._embeddedMode = value;
533  }
534
535  get hideSidebar(): boolean {
536    return !!this._hideSidebar;
537  }
538
539  set hideSidebar(value: boolean) {
540    this._hideSidebar = value;
541  }
542
543  setBufferUsage(bufferUsage: number) {
544    this._bufferUsage = bufferUsage;
545  }
546
547  setTrackData(id: string, data: {}) {
548    this.trackDataStore.set(id, data);
549  }
550
551  setRecordingLog(recordingLog: string) {
552    this._recordingLog = recordingLog;
553  }
554
555  setAggregateData(kind: string, data: AggregateData) {
556    this.aggregateDataStore.set(kind, data);
557  }
558
559  getCurResolution(): duration {
560    // Truncate the resolution to the closest power of 2 (in nanosecond space).
561    // We choose to work in ns space because resolution is consumed be track
562    // controllers for quantization and they rely on resolution to be a power
563    // of 2 in nanosecond form. This is property does not hold if we work in
564    // second space.
565    //
566    // This effectively means the resolution changes approximately every 6 zoom
567    // levels. Logic: each zoom level represents a delta of 0.1 * (visible
568    // window span). Therefore, zooming out by six levels is 1.1^6 ~= 2.
569    // Similarily, zooming in six levels is 0.9^6 ~= 0.5.
570    const timeScale = this.timeline.visibleTimeScale;
571    // TODO(b/186265930): Remove once fixed:
572    if (timeScale.pxSpan.delta === 0) {
573      console.error(`b/186265930: Bad pxToSec suppressed`);
574      return RESOLUTION_DEFAULT;
575    }
576
577    const timePerPx = timeScale.pxDeltaToDuration(this.quantPx);
578
579    return BigintMath.bitFloor(timePerPx.toTime('floor'));
580  }
581
582  getCurrentEngine(): EngineConfig | undefined {
583    return this.state.engine;
584  }
585
586  makeSelection(action: DeferredAction<{}>, opts: MakeSelectionOpts = {}) {
587    const {switchToCurrentSelectionTab = true, clearSearch = true} = opts;
588    const currentSelectionTabUri = 'current_selection';
589
590    // A new selection should cancel the current search selection.
591    clearSearch && globals.dispatch(Actions.setSearchIndex({index: -1}));
592
593    if (switchToCurrentSelectionTab) {
594      globals.dispatch(Actions.showTab({uri: currentSelectionTabUri}));
595    }
596    globals.dispatch(action);
597  }
598
599  setLegacySelection(
600    legacySelection: LegacySelection,
601    args: Partial<LegacySelectionArgs> = {},
602  ): void {
603    this._selectionManager.setLegacy(legacySelection);
604    this.handleSelectionArgs(args);
605  }
606
607  selectSingleEvent(
608    trackKey: string,
609    eventId: number,
610    args: Partial<LegacySelectionArgs> = {},
611  ): void {
612    this._selectionManager.setEvent(trackKey, eventId);
613    this.handleSelectionArgs(args);
614  }
615
616  private handleSelectionArgs(args: Partial<LegacySelectionArgs> = {}): void {
617    const {
618      clearSearch = true,
619      switchToCurrentSelectionTab = true,
620      pendingScrollId = undefined,
621    } = args;
622    if (clearSearch) {
623      globals.dispatch(Actions.setSearchIndex({index: -1}));
624    }
625    if (pendingScrollId !== undefined) {
626      globals.dispatch(
627        Actions.setPendingScrollId({
628          pendingScrollId,
629        }),
630      );
631    }
632    if (switchToCurrentSelectionTab) {
633      globals.dispatch(Actions.showTab({uri: 'current_selection'}));
634    }
635  }
636
637  clearSelection(): void {
638    globals.dispatch(Actions.setSearchIndex({index: -1}));
639    this._selectionManager.clear();
640  }
641
642  resetForTesting() {
643    this._dispatch = undefined;
644    this._timeline = undefined;
645    this._serviceWorkerController = undefined;
646
647    // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads.
648    this._trackDataStore = undefined;
649    this._queryResults = undefined;
650    this._overviewStore = undefined;
651    this._threadMap = undefined;
652    this._sliceDetails = undefined;
653    this._threadStateDetails = undefined;
654    this._aggregateDataStore = undefined;
655    this._numQueriesQueued = 0;
656    this._metricResult = undefined;
657    this._currentSearchResults = {
658      eventIds: new Float64Array(0),
659      tses: new BigInt64Array(0),
660      utids: new Float64Array(0),
661      trackKeys: [],
662      sources: [],
663      totalResults: 0,
664    };
665  }
666
667  // This variable is set by the is_internal_user.js script if the user is a
668  // googler. This is used to avoid exposing features that are not ready yet
669  // for public consumption. The gated features themselves are not secret.
670  // If a user has been detected as a Googler once, make that sticky in
671  // localStorage, so that we keep treating them as such when they connect over
672  // public networks.
673  get isInternalUser() {
674    if (this._isInternalUser === undefined) {
675      this._isInternalUser = localStorage.getItem('isInternalUser') === '1';
676    }
677    return this._isInternalUser;
678  }
679
680  set isInternalUser(value: boolean) {
681    localStorage.setItem('isInternalUser', value ? '1' : '0');
682    this._isInternalUser = value;
683    raf.scheduleFullRedraw();
684  }
685
686  get testing() {
687    return this._testing;
688  }
689
690  // Used when switching to the legacy TraceViewer UI.
691  // Most resources are cleaned up by replacing the current |window| object,
692  // however pending RAFs and workers seem to outlive the |window| and need to
693  // be cleaned up explicitly.
694  shutdown() {
695    raf.shutdown();
696  }
697
698  // Get a timescale that covers the entire trace
699  getTraceTimeScale(pxSpan: PxSpan): TimeScale {
700    const {start, end} = this.traceContext;
701    const traceTime = HighPrecisionTimeSpan.fromTime(start, end);
702    return TimeScale.fromHPTimeSpan(traceTime, pxSpan);
703  }
704
705  // Get the trace time bounds
706  stateTraceTime(): Span<HighPrecisionTime> {
707    const {start, end} = this.traceContext;
708    return HighPrecisionTimeSpan.fromTime(start, end);
709  }
710
711  stateTraceTimeTP(): Span<time, duration> {
712    const {start, end} = this.traceContext;
713    return new TimeSpan(start, end);
714  }
715
716  // Get the state version of the visible time bounds
717  stateVisibleTime(): Span<time, duration> {
718    const {start, end} = this.state.frontendLocalState.visibleState;
719    return new TimeSpan(start, end);
720  }
721
722  // How many pixels to use for one quanta of horizontal resolution
723  get quantPx(): number {
724    const quantPx = (self as {} as {quantPx: number | undefined}).quantPx;
725    return quantPx ?? 1;
726  }
727
728  get commandManager(): CommandManager {
729    return assertExists(this._cmdManager);
730  }
731
732  get tabManager() {
733    return this._tabManager;
734  }
735
736  get trackManager() {
737    return this._trackManager;
738  }
739
740  // Offset between t=0 and the configured time domain.
741  timestampOffset(): time {
742    const fmt = timestampFormat();
743    switch (fmt) {
744      case TimestampFormat.Timecode:
745      case TimestampFormat.Seconds:
746        return this.traceContext.start;
747      case TimestampFormat.Raw:
748      case TimestampFormat.RawLocale:
749        return Time.ZERO;
750      case TimestampFormat.UTC:
751        return this.traceContext.utcOffset;
752      case TimestampFormat.TraceTz:
753        return this.traceContext.traceTzOffset;
754      default:
755        const x: never = fmt;
756        throw new Error(`Unsupported format ${x}`);
757    }
758  }
759
760  // Convert absolute time to domain time.
761  toDomainTime(ts: time): time {
762    return Time.sub(ts, this.timestampOffset());
763  }
764
765  async findTimeRangeOfSelection(): Promise<
766    Optional<{start: time; end: time}>
767  > {
768    const sel = globals.state.selection;
769    if (sel.kind === 'area') {
770      return sel;
771    } else if (sel.kind === 'note') {
772      const selectedNote = this.state.notes[sel.id];
773      // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
774      if (selectedNote) {
775        const kind = selectedNote.noteType;
776        switch (kind) {
777          case 'SPAN':
778            return {
779              start: selectedNote.start,
780              end: selectedNote.end,
781            };
782          case 'DEFAULT':
783            return {
784              start: selectedNote.timestamp,
785              end: Time.add(selectedNote.timestamp, INSTANT_FOCUS_DURATION),
786            };
787          default:
788            assertUnreachable(kind);
789        }
790      }
791    } else if (sel.kind === 'single') {
792      const uri = globals.state.tracks[sel.trackKey]?.uri;
793      if (uri) {
794        const bounds = await globals.trackManager
795          .resolveTrackInfo(uri)
796          ?.getEventBounds?.(sel.eventId);
797        if (bounds) {
798          return {
799            start: bounds.ts,
800            end: Time.add(bounds.ts, bounds.dur),
801          };
802        }
803      }
804      return undefined;
805    }
806
807    const selection = getLegacySelection(this.state);
808    if (selection === null) {
809      return undefined;
810    }
811
812    if (selection.kind === 'SCHED_SLICE' || selection.kind === 'SLICE') {
813      const slice = this.sliceDetails;
814      return findTimeRangeOfSlice(slice);
815    } else if (selection.kind === 'THREAD_STATE') {
816      const threadState = this.threadStateDetails;
817      return findTimeRangeOfSlice(threadState);
818    } else if (selection.kind === 'LOG') {
819      // TODO(hjd): Make focus selection work for logs.
820    } else if (selection.kind === 'GENERIC_SLICE') {
821      return findTimeRangeOfSlice({
822        ts: selection.start,
823        dur: selection.duration,
824      });
825    }
826
827    return undefined;
828  }
829
830  panToTimestamp(ts: time): void {
831    horizontalScrollToTs(ts);
832  }
833}
834
835interface SliceLike {
836  ts: time;
837  dur: duration;
838}
839
840// Returns the start and end points of a slice-like object If slice is instant
841// or incomplete, dummy time will be returned which instead.
842function findTimeRangeOfSlice(slice: Partial<SliceLike>): {
843  start: time;
844  end: time;
845} {
846  if (exists(slice.ts) && exists(slice.dur)) {
847    if (slice.dur === -1n) {
848      return {
849        start: slice.ts,
850        end: Time.add(slice.ts, INCOMPLETE_SLICE_DURATION),
851      };
852    } else if (slice.dur === 0n) {
853      return {
854        start: slice.ts,
855        end: Time.add(slice.ts, INSTANT_FOCUS_DURATION),
856      };
857    } else {
858      return {start: slice.ts, end: Time.add(slice.ts, slice.dur)};
859    }
860  } else {
861    return {start: Time.INVALID, end: Time.INVALID};
862  }
863}
864
865// Returns the time span of the current selection, or the visible window if
866// there is no current selection.
867export async function getTimeSpanOfSelectionOrVisibleWindow(): Promise<
868  Span<time, duration>
869> {
870  const range = await globals.findTimeRangeOfSelection();
871  if (exists(range)) {
872    return new TimeSpan(range.start, range.end);
873  } else {
874    return globals.stateVisibleTime();
875  }
876}
877
878export const globals = new Globals();
879