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} from '../base/logging'; 17import {Actions, DeferredAction} from '../common/actions'; 18import {AggregateData} from '../common/aggregation_data'; 19import {Args, ArgsTree} from '../common/arg_types'; 20import { 21 ConversionJobName, 22 ConversionJobStatus, 23} from '../common/conversion_jobs'; 24import {createEmptyState} from '../common/empty_state'; 25import {Engine} from '../common/engine'; 26import { 27 HighPrecisionTime, 28 HighPrecisionTimeSpan, 29} from '../common/high_precision_time'; 30import {MetricResult} from '../common/metric_data'; 31import {CurrentSearchResults, SearchSummary} from '../common/search_data'; 32import {CallsiteInfo, EngineConfig, ProfileType, State} from '../common/state'; 33import {Span, tpTimeFromSeconds} from '../common/time'; 34import { 35 TPDuration, 36 TPTime, 37 TPTimeSpan, 38} from '../common/time'; 39 40import {Analytics, initAnalytics} from './analytics'; 41import {BottomTabList} from './bottom_tab'; 42import {FrontendLocalState} from './frontend_local_state'; 43import {RafScheduler} from './raf_scheduler'; 44import {Router} from './router'; 45import {ServiceWorkerController} from './service_worker_controller'; 46import {PxSpan, TimeScale} from './time_scale'; 47 48type Dispatch = (action: DeferredAction) => void; 49type TrackDataStore = Map<string, {}>; 50type QueryResultsStore = Map<string, {}|undefined>; 51type AggregateDataStore = Map<string, AggregateData>; 52type Description = Map<string, string>; 53 54export interface SliceDetails { 55 ts?: TPTime; 56 absTime?: string; 57 dur?: TPDuration; 58 threadTs?: TPTime; 59 threadDur?: TPDuration; 60 priority?: number; 61 endState?: string|null; 62 cpu?: number; 63 id?: number; 64 threadStateId?: number; 65 utid?: number; 66 wakeupTs?: TPTime; 67 wakerUtid?: number; 68 wakerCpu?: number; 69 category?: string; 70 name?: string; 71 tid?: number; 72 threadName?: string; 73 pid?: number; 74 processName?: string; 75 uid?: number; 76 packageName?: string; 77 versionCode?: number; 78 args?: Args; 79 argsTree?: ArgsTree; 80 description?: Description; 81} 82 83export interface FlowPoint { 84 trackId: number; 85 86 sliceName: string; 87 sliceCategory: string; 88 sliceId: number; 89 sliceStartTs: number; 90 sliceEndTs: number; 91 // Thread and process info. Only set in sliceSelected not in areaSelected as 92 // the latter doesn't display per-flow info and it'd be a waste to join 93 // additional tables for undisplayed info in that case. Nothing precludes 94 // adding this in a future iteration however. 95 threadName: string; 96 processName: string; 97 98 depth: number; 99 100 // TODO(altimin): Ideally we should have a generic mechanism for allowing to 101 // customise the name here, but for now we are hardcording a few 102 // Chrome-specific bits in the query here. 103 sliceChromeCustomName?: string; 104} 105 106export interface Flow { 107 id: number; 108 109 begin: FlowPoint; 110 end: FlowPoint; 111 dur: number; 112 113 category?: string; 114 name?: string; 115} 116 117export interface CounterDetails { 118 startTime?: TPTime; 119 value?: number; 120 delta?: number; 121 duration?: TPDuration; 122 name?: string; 123} 124 125export interface ThreadStateDetails { 126 ts?: TPTime; 127 dur?: TPDuration; 128} 129 130export interface FlamegraphDetails { 131 type?: ProfileType; 132 id?: number; 133 start?: TPTime; 134 dur?: TPDuration; 135 pids?: number[]; 136 upids?: number[]; 137 flamegraph?: CallsiteInfo[]; 138 expandedCallsite?: CallsiteInfo; 139 viewingOption?: string; 140 expandedId?: number; 141 // isInAreaSelection is true if a flamegraph is part of the current area 142 // selection. 143 isInAreaSelection?: boolean; 144 // When heap_graph_non_finalized_graph has a count >0, we mark the graph 145 // as incomplete. 146 graphIncomplete?: boolean; 147} 148 149export interface CpuProfileDetails { 150 id?: number; 151 ts?: number; 152 utid?: number; 153 stack?: CallsiteInfo[]; 154} 155 156export interface QuantizedLoad { 157 start: TPTime; 158 end: TPTime; 159 load: number; 160} 161type OverviewStore = Map<string, QuantizedLoad[]>; 162 163export interface ThreadDesc { 164 utid: number; 165 tid: number; 166 threadName: string; 167 pid?: number; 168 procName?: string; 169 cmdline?: string; 170} 171type ThreadMap = Map<number, ThreadDesc>; 172 173export interface FtraceEvent { 174 id: number; 175 ts: TPTime; 176 name: string; 177 cpu: number; 178 thread: string|null; 179 process: string|null; 180 args: string; 181} 182 183export interface FtracePanelData { 184 events: FtraceEvent[]; 185 offset: number; 186 numEvents: number; // Number of events in the visible window 187} 188 189export interface FtraceStat { 190 name: string; 191 count: number; 192} 193 194function getRoot() { 195 // Works out the root directory where the content should be served from 196 // e.g. `http://origin/v1.2.3/`. 197 const script = document.currentScript as HTMLScriptElement; 198 199 // Needed for DOM tests, that do not have script element. 200 if (script === null) { 201 return ''; 202 } 203 204 let root = script.src; 205 root = root.substr(0, root.lastIndexOf('/') + 1); 206 return root; 207} 208 209/** 210 * Global accessors for state/dispatch in the frontend. 211 */ 212class Globals { 213 readonly root = getRoot(); 214 215 bottomTabList?: BottomTabList = undefined; 216 217 private _testing = false; 218 private _dispatch?: Dispatch = undefined; 219 private _state?: State = undefined; 220 private _frontendLocalState?: FrontendLocalState = undefined; 221 private _rafScheduler?: RafScheduler = undefined; 222 private _serviceWorkerController?: ServiceWorkerController = undefined; 223 private _logging?: Analytics = undefined; 224 private _isInternalUser: boolean|undefined = undefined; 225 226 // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads. 227 private _trackDataStore?: TrackDataStore = undefined; 228 private _queryResults?: QueryResultsStore = undefined; 229 private _overviewStore?: OverviewStore = undefined; 230 private _aggregateDataStore?: AggregateDataStore = undefined; 231 private _threadMap?: ThreadMap = undefined; 232 private _sliceDetails?: SliceDetails = undefined; 233 private _threadStateDetails?: ThreadStateDetails = undefined; 234 private _connectedFlows?: Flow[] = undefined; 235 private _selectedFlows?: Flow[] = undefined; 236 private _visibleFlowCategories?: Map<string, boolean> = undefined; 237 private _counterDetails?: CounterDetails = undefined; 238 private _flamegraphDetails?: FlamegraphDetails = undefined; 239 private _cpuProfileDetails?: CpuProfileDetails = undefined; 240 private _numQueriesQueued = 0; 241 private _bufferUsage?: number = undefined; 242 private _recordingLog?: string = undefined; 243 private _traceErrors?: number = undefined; 244 private _metricError?: string = undefined; 245 private _metricResult?: MetricResult = undefined; 246 private _jobStatus?: Map<ConversionJobName, ConversionJobStatus> = undefined; 247 private _router?: Router = undefined; 248 private _embeddedMode?: boolean = undefined; 249 private _hideSidebar?: boolean = undefined; 250 private _ftraceCounters?: FtraceStat[] = undefined; 251 private _ftracePanelData?: FtracePanelData = undefined; 252 253 // TODO(hjd): Remove once we no longer need to update UUID on redraw. 254 private _publishRedraw?: () => void = undefined; 255 256 private _currentSearchResults: CurrentSearchResults = { 257 sliceIds: new Float64Array(0), 258 tsStarts: new Float64Array(0), 259 utids: new Float64Array(0), 260 trackIds: [], 261 sources: [], 262 totalResults: 0, 263 }; 264 searchSummary: SearchSummary = { 265 tsStarts: new Float64Array(0), 266 tsEnds: new Float64Array(0), 267 count: new Uint8Array(0), 268 }; 269 270 engines = new Map<string, Engine>(); 271 272 initialize(dispatch: Dispatch, router: Router) { 273 this._dispatch = dispatch; 274 this._router = router; 275 this._state = createEmptyState(); 276 this._frontendLocalState = new FrontendLocalState(); 277 this._rafScheduler = new RafScheduler(); 278 this._serviceWorkerController = new ServiceWorkerController(); 279 this._testing = 280 self.location && self.location.search.indexOf('testing=1') >= 0; 281 this._logging = initAnalytics(); 282 283 // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads. 284 this._trackDataStore = new Map<string, {}>(); 285 this._queryResults = new Map<string, {}>(); 286 this._overviewStore = new Map<string, QuantizedLoad[]>(); 287 this._aggregateDataStore = new Map<string, AggregateData>(); 288 this._threadMap = new Map<number, ThreadDesc>(); 289 this._sliceDetails = {}; 290 this._connectedFlows = []; 291 this._selectedFlows = []; 292 this._visibleFlowCategories = new Map<string, boolean>(); 293 this._counterDetails = {}; 294 this._threadStateDetails = {}; 295 this._flamegraphDetails = {}; 296 this._cpuProfileDetails = {}; 297 this.engines.clear(); 298 } 299 300 get router(): Router { 301 return assertExists(this._router); 302 } 303 304 get publishRedraw(): () => void { 305 return this._publishRedraw || (() => {}); 306 } 307 308 set publishRedraw(f: () => void) { 309 this._publishRedraw = f; 310 } 311 312 get state(): State { 313 return assertExists(this._state); 314 } 315 316 set state(state: State) { 317 this._state = assertExists(state); 318 } 319 320 get dispatch(): Dispatch { 321 return assertExists(this._dispatch); 322 } 323 324 dispatchMultiple(actions: DeferredAction[]): void { 325 const dispatch = this.dispatch; 326 for (const action of actions) { 327 dispatch(action); 328 } 329 } 330 331 get frontendLocalState() { 332 return assertExists(this._frontendLocalState); 333 } 334 335 get rafScheduler() { 336 return assertExists(this._rafScheduler); 337 } 338 339 get logging() { 340 return assertExists(this._logging); 341 } 342 343 get serviceWorkerController() { 344 return assertExists(this._serviceWorkerController); 345 } 346 347 // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads. 348 get overviewStore(): OverviewStore { 349 return assertExists(this._overviewStore); 350 } 351 352 get trackDataStore(): TrackDataStore { 353 return assertExists(this._trackDataStore); 354 } 355 356 get queryResults(): QueryResultsStore { 357 return assertExists(this._queryResults); 358 } 359 360 get threads() { 361 return assertExists(this._threadMap); 362 } 363 364 get sliceDetails() { 365 return assertExists(this._sliceDetails); 366 } 367 368 set sliceDetails(click: SliceDetails) { 369 this._sliceDetails = assertExists(click); 370 } 371 372 get threadStateDetails() { 373 return assertExists(this._threadStateDetails); 374 } 375 376 set threadStateDetails(click: ThreadStateDetails) { 377 this._threadStateDetails = assertExists(click); 378 } 379 380 get connectedFlows() { 381 return assertExists(this._connectedFlows); 382 } 383 384 set connectedFlows(connectedFlows: Flow[]) { 385 this._connectedFlows = assertExists(connectedFlows); 386 } 387 388 get selectedFlows() { 389 return assertExists(this._selectedFlows); 390 } 391 392 set selectedFlows(selectedFlows: Flow[]) { 393 this._selectedFlows = assertExists(selectedFlows); 394 } 395 396 get visibleFlowCategories() { 397 return assertExists(this._visibleFlowCategories); 398 } 399 400 set visibleFlowCategories(visibleFlowCategories: Map<string, boolean>) { 401 this._visibleFlowCategories = assertExists(visibleFlowCategories); 402 } 403 404 get counterDetails() { 405 return assertExists(this._counterDetails); 406 } 407 408 set counterDetails(click: CounterDetails) { 409 this._counterDetails = assertExists(click); 410 } 411 412 get aggregateDataStore(): AggregateDataStore { 413 return assertExists(this._aggregateDataStore); 414 } 415 416 get flamegraphDetails() { 417 return assertExists(this._flamegraphDetails); 418 } 419 420 set flamegraphDetails(click: FlamegraphDetails) { 421 this._flamegraphDetails = assertExists(click); 422 } 423 424 get traceErrors() { 425 return this._traceErrors; 426 } 427 428 setTraceErrors(arg: number) { 429 this._traceErrors = arg; 430 } 431 432 get metricError() { 433 return this._metricError; 434 } 435 436 setMetricError(arg: string) { 437 this._metricError = arg; 438 } 439 440 get metricResult() { 441 return this._metricResult; 442 } 443 444 setMetricResult(result: MetricResult) { 445 this._metricResult = result; 446 } 447 448 get cpuProfileDetails() { 449 return assertExists(this._cpuProfileDetails); 450 } 451 452 set cpuProfileDetails(click: CpuProfileDetails) { 453 this._cpuProfileDetails = assertExists(click); 454 } 455 456 set numQueuedQueries(value: number) { 457 this._numQueriesQueued = value; 458 } 459 460 get numQueuedQueries() { 461 return this._numQueriesQueued; 462 } 463 464 get bufferUsage() { 465 return this._bufferUsage; 466 } 467 468 get recordingLog() { 469 return this._recordingLog; 470 } 471 472 get currentSearchResults() { 473 return this._currentSearchResults; 474 } 475 476 set currentSearchResults(results: CurrentSearchResults) { 477 this._currentSearchResults = results; 478 } 479 480 get hasFtrace(): boolean { 481 return Boolean(this._ftraceCounters && this._ftraceCounters.length > 0); 482 } 483 484 get ftraceCounters(): FtraceStat[]|undefined { 485 return this._ftraceCounters; 486 } 487 488 set ftraceCounters(value: FtraceStat[]|undefined) { 489 this._ftraceCounters = value; 490 } 491 492 getConversionJobStatus(name: ConversionJobName): ConversionJobStatus { 493 return this.getJobStatusMap().get(name) || ConversionJobStatus.NotRunning; 494 } 495 496 setConversionJobStatus(name: ConversionJobName, status: ConversionJobStatus) { 497 const map = this.getJobStatusMap(); 498 if (status === ConversionJobStatus.NotRunning) { 499 map.delete(name); 500 } else { 501 map.set(name, status); 502 } 503 } 504 505 private getJobStatusMap(): Map<ConversionJobName, ConversionJobStatus> { 506 if (!this._jobStatus) { 507 this._jobStatus = new Map(); 508 } 509 return this._jobStatus; 510 } 511 512 get embeddedMode(): boolean { 513 return !!this._embeddedMode; 514 } 515 516 set embeddedMode(value: boolean) { 517 this._embeddedMode = value; 518 } 519 520 get hideSidebar(): boolean { 521 return !!this._hideSidebar; 522 } 523 524 set hideSidebar(value: boolean) { 525 this._hideSidebar = value; 526 } 527 528 setBufferUsage(bufferUsage: number) { 529 this._bufferUsage = bufferUsage; 530 } 531 532 setTrackData(id: string, data: {}) { 533 this.trackDataStore.set(id, data); 534 } 535 536 setRecordingLog(recordingLog: string) { 537 this._recordingLog = recordingLog; 538 } 539 540 setAggregateData(kind: string, data: AggregateData) { 541 this.aggregateDataStore.set(kind, data); 542 } 543 544 getCurResolution(): TPDuration { 545 // Truncate the resolution to the closest power of 2 (in nanosecond space). 546 // We choose to work in ns space because resolution is consumed be track 547 // controllers for quantization and they rely on resolution to be a power 548 // of 2 in nanosecond form. This is property does not hold if we work in 549 // second space. 550 // 551 // This effectively means the resolution changes approximately every 6 zoom 552 // levels. Logic: each zoom level represents a delta of 0.1 * (visible 553 // window span). Therefore, zooming out by six levels is 1.1^6 ~= 2. 554 // Similarily, zooming in six levels is 0.9^6 ~= 0.5. 555 const timeScale = this.frontendLocalState.visibleTimeScale; 556 // TODO(b/186265930): Remove once fixed: 557 if (timeScale.pxSpan.delta === 0) { 558 console.error(`b/186265930: Bad pxToSec suppressed`); 559 return BigintMath.bitFloor(tpTimeFromSeconds(1000)); 560 } 561 562 const timePerPx = HighPrecisionTime.max( 563 timeScale.pxDeltaToDuration(1), new HighPrecisionTime(1n)); 564 565 const resolutionBig = BigintMath.bitFloor(timePerPx.toTPTime()); 566 return resolutionBig; 567 } 568 569 getCurrentEngine(): EngineConfig|undefined { 570 return this.state.engine; 571 } 572 573 get ftracePanelData(): FtracePanelData|undefined { 574 return this._ftracePanelData; 575 } 576 577 set ftracePanelData(data: FtracePanelData|undefined) { 578 this._ftracePanelData = data; 579 } 580 581 makeSelection(action: DeferredAction<{}>, tabToOpen = 'current_selection') { 582 // A new selection should cancel the current search selection. 583 globals.dispatch(Actions.setSearchIndex({index: -1})); 584 const tab = action.type === 'deselect' ? undefined : tabToOpen; 585 globals.dispatch(Actions.setCurrentTab({tab})); 586 globals.dispatch(action); 587 } 588 589 resetForTesting() { 590 this._dispatch = undefined; 591 this._state = undefined; 592 this._frontendLocalState = undefined; 593 this._rafScheduler = undefined; 594 this._serviceWorkerController = undefined; 595 596 // TODO(hjd): Unify trackDataStore, queryResults, overviewStore, threads. 597 this._trackDataStore = undefined; 598 this._queryResults = undefined; 599 this._overviewStore = undefined; 600 this._threadMap = undefined; 601 this._sliceDetails = undefined; 602 this._threadStateDetails = undefined; 603 this._aggregateDataStore = undefined; 604 this._numQueriesQueued = 0; 605 this._metricResult = undefined; 606 this._currentSearchResults = { 607 sliceIds: new Float64Array(0), 608 tsStarts: new Float64Array(0), 609 utids: new Float64Array(0), 610 trackIds: [], 611 sources: [], 612 totalResults: 0, 613 }; 614 } 615 616 // This variable is set by the is_internal_user.js script if the user is a 617 // googler. This is used to avoid exposing features that are not ready yet 618 // for public consumption. The gated features themselves are not secret. 619 // If a user has been detected as a Googler once, make that sticky in 620 // localStorage, so that we keep treating them as such when they connect over 621 // public networks. 622 get isInternalUser() { 623 if (this._isInternalUser === undefined) { 624 this._isInternalUser = localStorage.getItem('isInternalUser') === '1'; 625 } 626 return this._isInternalUser; 627 } 628 629 set isInternalUser(value: boolean) { 630 localStorage.setItem('isInternalUser', value ? '1' : '0'); 631 this._isInternalUser = value; 632 } 633 634 get testing() { 635 return this._testing; 636 } 637 638 // Used when switching to the legacy TraceViewer UI. 639 // Most resources are cleaned up by replacing the current |window| object, 640 // however pending RAFs and workers seem to outlive the |window| and need to 641 // be cleaned up explicitly. 642 shutdown() { 643 this._rafScheduler!.shutdown(); 644 } 645 646 // Get a timescale that covers the entire trace 647 getTraceTimeScale(pxSpan: PxSpan): TimeScale { 648 const {start, end} = this.state.traceTime; 649 const traceTime = HighPrecisionTimeSpan.fromTpTime(start, end); 650 return new TimeScale(traceTime.start, traceTime.duration.nanos, pxSpan); 651 } 652 653 // Get the trace time bounds 654 stateTraceTime(): Span<HighPrecisionTime> { 655 const {start, end} = this.state.traceTime; 656 return HighPrecisionTimeSpan.fromTpTime(start, end); 657 } 658 659 stateTraceTimeTP(): Span<TPTime> { 660 const {start, end} = this.state.traceTime; 661 return new TPTimeSpan(start, end); 662 } 663 664 // Get the state version of the visible time bounds 665 stateVisibleTime(): Span<TPTime> { 666 const {start, end} = this.state.frontendLocalState.visibleState; 667 return new TPTimeSpan(start, end); 668 } 669} 670 671export const globals = new Globals(); 672