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