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