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, assertTrue} from '../base/logging'; 17import {Duration, duration, Span, time, Time, TimeSpan} from '../base/time'; 18import {Actions, DeferredAction} from '../common/actions'; 19import {cacheTrace} from '../common/cache_manager'; 20import { 21 HighPrecisionTime, 22 HighPrecisionTimeSpan, 23} from '../common/high_precision_time'; 24import { 25 getEnabledMetatracingCategories, 26 isMetatracingEnabled, 27} from '../common/metatracing'; 28import {pluginManager} from '../common/plugins'; 29import {EngineMode, PendingDeeplinkState, ProfileType} from '../common/state'; 30import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../core/feature_flags'; 31import { 32 defaultTraceContext, 33 globals, 34 QuantizedLoad, 35 ThreadDesc, 36 TraceContext, 37} from '../frontend/globals'; 38import { 39 clearOverviewData, 40 publishHasFtrace, 41 publishMetricError, 42 publishOverviewData, 43 publishThreads, 44 publishTraceContext, 45} from '../frontend/publish'; 46import {addQueryResultsTab} from '../frontend/query_result_tab'; 47import {Router} from '../frontend/router'; 48import {Engine, EngineBase} from '../trace_processor/engine'; 49import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; 50import { 51 LONG, 52 LONG_NULL, 53 NUM, 54 NUM_NULL, 55 QueryError, 56 STR, 57 STR_NULL, 58} from '../trace_processor/query_result'; 59import { 60 resetEngineWorker, 61 WasmEngineProxy, 62} from '../trace_processor/wasm_engine_proxy'; 63import {showModal} from '../widgets/modal'; 64 65import {CounterAggregationController} from './aggregation/counter_aggregation_controller'; 66import {CpuAggregationController} from './aggregation/cpu_aggregation_controller'; 67import {CpuByProcessAggregationController} from './aggregation/cpu_by_process_aggregation_controller'; 68import {FrameAggregationController} from './aggregation/frame_aggregation_controller'; 69import {SliceAggregationController} from './aggregation/slice_aggregation_controller'; 70import {ThreadAggregationController} from './aggregation/thread_aggregation_controller'; 71import {Child, Children, Controller} from './controller'; 72import { 73 CpuProfileController, 74 CpuProfileControllerArgs, 75} from './cpu_profile_controller'; 76import { 77 FlowEventsController, 78 FlowEventsControllerArgs, 79} from './flow_events_controller'; 80import {LoadingManager} from './loading_manager'; 81import { 82 PIVOT_TABLE_REDUX_FLAG, 83 PivotTableController, 84} from './pivot_table_controller'; 85import {SearchController} from './search_controller'; 86import { 87 SelectionController, 88 SelectionControllerArgs, 89} from './selection_controller'; 90import {TraceErrorController} from './trace_error_controller'; 91import { 92 TraceBufferStream, 93 TraceFileStream, 94 TraceHttpStream, 95 TraceStream, 96} from '../core/trace_stream'; 97import {decideTracks} from './track_decider'; 98import {profileType} from '../frontend/flamegraph_panel'; 99import {FlamegraphCache} from '../core/flamegraph_cache'; 100 101type States = 'init' | 'loading_trace' | 'ready'; 102 103const METRICS = [ 104 'android_ion', 105 'android_lmk', 106 'android_dma_heap', 107 'android_surfaceflinger', 108 'android_batt', 109 'android_other_traces', 110 'chrome_dropped_frames', 111 // TODO(289365196): Reenable: 112 // 'chrome_long_latency', 113 'trace_metadata', 114 'android_trusty_workqueues', 115]; 116const FLAGGED_METRICS: Array<[Flag, string]> = METRICS.map((m) => { 117 const id = `forceMetric${m}`; 118 let name = m.split('_').join(' '); 119 name = name[0].toUpperCase() + name.slice(1); 120 name = 'Metric: ' + name; 121 const flag = featureFlags.register({ 122 id, 123 name, 124 description: `Overrides running the '${m}' metric at import time.`, 125 defaultValue: true, 126 }); 127 return [flag, m]; 128}); 129 130const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({ 131 id: 'enableChromeReliableRangeZoom', 132 name: 'Enable Chrome reliable range zoom', 133 description: 'Automatically zoom into the reliable range for Chrome traces', 134 defaultValue: false, 135}); 136 137const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({ 138 id: 'enableChromeReliableRangeAnnotation', 139 name: 'Enable Chrome reliable range annotation', 140 description: 'Automatically adds an annotation for the reliable range start', 141 defaultValue: false, 142}); 143 144// The following flags control TraceProcessor Config. 145const CROP_TRACK_EVENTS_FLAG = featureFlags.register({ 146 id: 'cropTrackEvents', 147 name: 'Crop track events', 148 description: 'Ignores track events outside of the range of interest', 149 defaultValue: false, 150}); 151const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({ 152 id: 'ingestFtraceInRawTable', 153 name: 'Ingest ftrace in raw table', 154 description: 'Enables ingestion of typed ftrace events into the raw table', 155 defaultValue: true, 156}); 157const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({ 158 id: 'analyzeTraceProtoContent', 159 name: 'Analyze trace proto content', 160 description: 161 'Enables trace proto content analysis (experimental_proto_content table)', 162 defaultValue: false, 163}); 164const FTRACE_DROP_UNTIL_FLAG = featureFlags.register({ 165 id: 'ftraceDropUntilAllCpusValid', 166 name: 'Crop ftrace events', 167 description: 168 'Drop ftrace events until all per-cpu data streams are known to be valid', 169 defaultValue: true, 170}); 171 172// A local storage key where the indication that JSON warning has been shown is 173// stored. 174const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning'; 175 176function showJsonWarning() { 177 showModal({ 178 title: 'Warning', 179 content: m( 180 'div', 181 m( 182 'span', 183 'Perfetto UI features are limited for JSON traces. ', 184 'We recommend recording ', 185 m( 186 'a', 187 {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'}, 188 'proto-format traces', 189 ), 190 ' from Chrome.', 191 ), 192 m('br'), 193 ), 194 buttons: [], 195 }); 196} 197 198// TODO(stevegolton): Move this into some global "SQL extensions" file and 199// ensure it's only run once. 200async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> { 201 await engine.query(` 202 create perfetto function __max_layout_depth(track_count INT, track_ids STRING) 203 returns INT AS 204 select iif( 205 $track_count = 1, 206 ( 207 select max_depth 208 from _slice_track_summary 209 where id = cast($track_ids AS int) 210 ), 211 ( 212 select max(layout_depth) 213 from experimental_slice_layout($track_ids) 214 ) 215 ); 216 `); 217} 218 219// TraceController handles handshakes with the frontend for everything that 220// concerns a single trace. It owns the WASM trace processor engine, handles 221// tracks data and SQL queries. There is one TraceController instance for each 222// trace opened in the UI (for now only one trace is supported). 223export class TraceController extends Controller<States> { 224 private readonly engineId: string; 225 private engine?: EngineBase; 226 227 constructor(engineId: string) { 228 super('init'); 229 this.engineId = engineId; 230 } 231 232 run() { 233 const engineCfg = assertExists(globals.state.engine); 234 switch (this.state) { 235 case 'init': 236 this.loadTrace() 237 .then((mode) => { 238 globals.dispatch( 239 Actions.setEngineReady({ 240 engineId: this.engineId, 241 ready: true, 242 mode, 243 }), 244 ); 245 }) 246 .catch((err) => { 247 this.updateStatus(`${err}`); 248 throw err; 249 }); 250 this.updateStatus('Opening trace'); 251 this.setState('loading_trace'); 252 break; 253 254 case 'loading_trace': 255 // Stay in this state until loadTrace() returns and marks the engine as 256 // ready. 257 if (this.engine === undefined || !engineCfg.ready) return; 258 this.setState('ready'); 259 break; 260 261 case 'ready': 262 // At this point we are ready to serve queries and handle tracks. 263 const engine = assertExists(this.engine); 264 const childControllers: Children = []; 265 266 const selectionArgs: SelectionControllerArgs = {engine}; 267 childControllers.push( 268 Child('selection', SelectionController, selectionArgs), 269 ); 270 271 const flowEventsArgs: FlowEventsControllerArgs = {engine}; 272 childControllers.push( 273 Child('flowEvents', FlowEventsController, flowEventsArgs), 274 ); 275 276 const cpuProfileArgs: CpuProfileControllerArgs = {engine}; 277 childControllers.push( 278 Child('cpuProfile', CpuProfileController, cpuProfileArgs), 279 ); 280 281 childControllers.push( 282 Child('cpu_aggregation', CpuAggregationController, { 283 engine, 284 kind: 'cpu_aggregation', 285 }), 286 ); 287 childControllers.push( 288 Child('thread_aggregation', ThreadAggregationController, { 289 engine, 290 kind: 'thread_state_aggregation', 291 }), 292 ); 293 childControllers.push( 294 Child('cpu_process_aggregation', CpuByProcessAggregationController, { 295 engine, 296 kind: 'cpu_by_process_aggregation', 297 }), 298 ); 299 if (!PIVOT_TABLE_REDUX_FLAG.get()) { 300 // Pivot table is supposed to handle the use cases the slice 301 // aggregation panel is used right now. When a flag to use pivot 302 // tables is enabled, do not add slice aggregation controller. 303 childControllers.push( 304 Child('slice_aggregation', SliceAggregationController, { 305 engine, 306 kind: 'slice_aggregation', 307 }), 308 ); 309 } 310 childControllers.push( 311 Child('counter_aggregation', CounterAggregationController, { 312 engine, 313 kind: 'counter_aggregation', 314 }), 315 ); 316 childControllers.push( 317 Child('frame_aggregation', FrameAggregationController, { 318 engine, 319 kind: 'frame_aggregation', 320 }), 321 ); 322 childControllers.push( 323 Child('search', SearchController, { 324 engine, 325 app: globals, 326 }), 327 ); 328 childControllers.push( 329 Child('pivot_table', PivotTableController, {engine}), 330 ); 331 332 childControllers.push( 333 Child('traceError', TraceErrorController, {engine}), 334 ); 335 336 return childControllers; 337 338 default: 339 throw new Error(`unknown state ${this.state}`); 340 } 341 return; 342 } 343 344 onDestroy() { 345 pluginManager.onTraceClose(); 346 globals.engines.delete(this.engineId); 347 348 // Invalidate the flamegraph cache. 349 // TODO(stevegolton): migrate this to the new system when it's ready. 350 globals.areaFlamegraphCache = new FlamegraphCache('area'); 351 } 352 353 private async loadTrace(): Promise<EngineMode> { 354 this.updateStatus('Creating trace processor'); 355 // Check if there is any instance of the trace_processor_shell running in 356 // HTTP RPC mode (i.e. trace_processor_shell -D). 357 let engineMode: EngineMode; 358 let useRpc = false; 359 if (globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') { 360 useRpc = (await HttpRpcEngine.checkConnection()).connected; 361 } 362 let engine; 363 if (useRpc) { 364 console.log('Opening trace using native accelerator over HTTP+RPC'); 365 engineMode = 'HTTP_RPC'; 366 engine = new HttpRpcEngine(this.engineId, LoadingManager.getInstance); 367 engine.errorHandler = (err) => { 368 globals.dispatch( 369 Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}), 370 ); 371 throw err; 372 }; 373 } else { 374 console.log('Opening trace using built-in WASM engine'); 375 engineMode = 'WASM'; 376 const enginePort = resetEngineWorker(); 377 engine = new WasmEngineProxy( 378 this.engineId, 379 enginePort, 380 LoadingManager.getInstance, 381 ); 382 engine.resetTraceProcessor({ 383 cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(), 384 ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(), 385 analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(), 386 ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(), 387 }); 388 } 389 this.engine = engine; 390 391 if (isMetatracingEnabled()) { 392 this.engine.enableMetatrace( 393 assertExists(getEnabledMetatracingCategories()), 394 ); 395 } 396 397 globals.engines.set(this.engineId, engine); 398 globals.dispatch( 399 Actions.setEngineReady({ 400 engineId: this.engineId, 401 ready: false, 402 mode: engineMode, 403 }), 404 ); 405 const engineCfg = assertExists(globals.state.engine); 406 assertTrue(engineCfg.id === this.engineId); 407 let traceStream: TraceStream | undefined; 408 if (engineCfg.source.type === 'FILE') { 409 traceStream = new TraceFileStream(engineCfg.source.file); 410 } else if (engineCfg.source.type === 'ARRAY_BUFFER') { 411 traceStream = new TraceBufferStream(engineCfg.source.buffer); 412 } else if (engineCfg.source.type === 'URL') { 413 traceStream = new TraceHttpStream(engineCfg.source.url); 414 } else if (engineCfg.source.type === 'HTTP_RPC') { 415 traceStream = undefined; 416 } else { 417 throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`); 418 } 419 420 // |traceStream| can be undefined in the case when we are using the external 421 // HTTP+RPC endpoint and the trace processor instance has already loaded 422 // a trace (because it was passed as a cmdline argument to 423 // trace_processor_shell). In this case we don't want the UI to load any 424 // file/stream and we just want to jump to the loading phase. 425 if (traceStream !== undefined) { 426 const tStart = performance.now(); 427 for (;;) { 428 const res = await traceStream.readChunk(); 429 await this.engine.parse(res.data); 430 const elapsed = (performance.now() - tStart) / 1000; 431 let status = 'Loading trace '; 432 if (res.bytesTotal > 0) { 433 const progress = Math.round((res.bytesRead / res.bytesTotal) * 100); 434 status += `${progress}%`; 435 } else { 436 status += `${Math.round(res.bytesRead / 1e6)} MB`; 437 } 438 status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`; 439 this.updateStatus(status); 440 if (res.eof) break; 441 } 442 await this.engine.notifyEof(); 443 } else { 444 assertTrue(this.engine instanceof HttpRpcEngine); 445 await this.engine.restoreInitialTables(); 446 } 447 448 // traceUuid will be '' if the trace is not cacheable (URL or RPC). 449 const traceUuid = await this.cacheCurrentTrace(); 450 451 const traceDetails = await getTraceTimeDetails(this.engine); 452 publishTraceContext(traceDetails); 453 454 const shownJsonWarning = 455 window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null; 456 457 // Show warning if the trace is in JSON format. 458 const query = `select str_value from metadata where name = 'trace_type'`; 459 const result = await assertExists(this.engine).query(query); 460 const traceType = result.firstRow({str_value: STR}).str_value; 461 const isJsonTrace = traceType == 'json'; 462 if (!shownJsonWarning) { 463 // When in embedded mode, the host app will control which trace format 464 // it passes to Perfetto, so we don't need to show this warning. 465 if (isJsonTrace && !globals.embeddedMode) { 466 showJsonWarning(); 467 // Save that the warning has been shown. Value is irrelevant since only 468 // the presence of key is going to be checked. 469 window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true'); 470 } 471 } 472 473 const emptyOmniboxState = { 474 omnibox: '', 475 mode: globals.state.omniboxState.mode || 'SEARCH', 476 }; 477 478 const actions: DeferredAction[] = [ 479 Actions.setOmnibox(emptyOmniboxState), 480 Actions.setTraceUuid({traceUuid}), 481 ]; 482 483 const visibleTimeSpan = await computeVisibleTime( 484 traceDetails.start, 485 traceDetails.end, 486 isJsonTrace, 487 this.engine, 488 ); 489 // We don't know the resolution at this point. However this will be 490 // replaced in 50ms so a guess is fine. 491 const resolution = visibleTimeSpan.duration.divide(1000).toTime(); 492 actions.push( 493 Actions.setVisibleTraceTime({ 494 start: visibleTimeSpan.start.toTime(), 495 end: visibleTimeSpan.end.toTime(), 496 lastUpdate: Date.now() / 1000, 497 resolution: BigintMath.max(resolution, 1n), 498 }), 499 ); 500 501 globals.dispatchMultiple(actions); 502 Router.navigate(`#!/viewer?local_cache_key=${traceUuid}`); 503 504 // Make sure the helper views are available before we start adding tracks. 505 await this.initialiseHelperViews(); 506 await this.includeSummaryTables(); 507 508 await defineMaxLayoutDepthSqlFunction(engine); 509 510 await pluginManager.onTraceLoad(engine, (id) => { 511 this.updateStatus(`Running plugin: ${id}`); 512 }); 513 514 { 515 // When we reload from a permalink don't create extra tracks: 516 const {pinnedTracks, tracks} = globals.state; 517 if (!pinnedTracks.length && !Object.keys(tracks).length) { 518 await this.listTracks(); 519 } 520 } 521 522 this.decideTabs(); 523 524 await this.listThreads(); 525 await this.loadTimelineOverview( 526 new TimeSpan(traceDetails.start, traceDetails.end), 527 ); 528 529 { 530 // Check if we have any ftrace events at all 531 const query = ` 532 select 533 * 534 from ftrace_event 535 limit 1`; 536 537 const res = await engine.query(query); 538 publishHasFtrace(res.numRows() > 0); 539 } 540 541 globals.dispatch(Actions.sortThreadTracks({})); 542 globals.dispatch(Actions.maybeExpandOnlyTrackGroup({})); 543 544 await this.selectFirstHeapProfile(); 545 if (PERF_SAMPLE_FLAG.get()) { 546 await this.selectPerfSample(traceDetails); 547 } 548 549 const pendingDeeplink = globals.state.pendingDeeplink; 550 if (pendingDeeplink !== undefined) { 551 globals.dispatch(Actions.clearPendingDeeplink({})); 552 await this.selectPendingDeeplink(pendingDeeplink); 553 if ( 554 pendingDeeplink.visStart !== undefined && 555 pendingDeeplink.visEnd !== undefined 556 ) { 557 this.zoomPendingDeeplink( 558 pendingDeeplink.visStart, 559 pendingDeeplink.visEnd, 560 ); 561 } 562 if (pendingDeeplink.query !== undefined) { 563 addQueryResultsTab({ 564 query: pendingDeeplink.query, 565 title: 'Deeplink Query', 566 }); 567 } 568 } 569 570 globals.dispatch(Actions.maybeExpandOnlyTrackGroup({})); 571 572 // Trace Processor doesn't support the reliable range feature for JSON 573 // traces. 574 if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) { 575 const reliableRangeStart = await computeTraceReliableRangeStart(engine); 576 if (reliableRangeStart > 0) { 577 globals.dispatch( 578 Actions.addNote({ 579 timestamp: reliableRangeStart, 580 color: '#ff0000', 581 text: 'Reliable Range Start', 582 }), 583 ); 584 } 585 } 586 587 return engineMode; 588 } 589 590 private async selectPerfSample(traceTime: {start: time; end: time}) { 591 const query = `select upid 592 from perf_sample 593 join thread using (utid) 594 where callsite_id is not null 595 order by ts desc limit 1`; 596 const profile = await assertExists(this.engine).query(query); 597 if (profile.numRows() !== 1) return; 598 const row = profile.firstRow({upid: NUM}); 599 const upid = row.upid; 600 const leftTs = traceTime.start; 601 const rightTs = traceTime.end; 602 globals.dispatch( 603 Actions.selectPerfSamples({ 604 id: 0, 605 upid, 606 leftTs, 607 rightTs, 608 type: ProfileType.PERF_SAMPLE, 609 }), 610 ); 611 } 612 613 private async selectFirstHeapProfile() { 614 const query = `select * from ( 615 select 616 min(ts) AS ts, 617 'heap_profile:' || group_concat(distinct heap_name) AS type, 618 upid 619 from heap_profile_allocation 620 group by upid 621 union 622 select distinct graph_sample_ts as ts, 'graph' as type, upid 623 from heap_graph_object) 624 order by ts limit 1`; 625 const profile = await assertExists(this.engine).query(query); 626 if (profile.numRows() !== 1) return; 627 const row = profile.firstRow({ts: LONG, type: STR, upid: NUM}); 628 const ts = Time.fromRaw(row.ts); 629 let profType = row.type; 630 if (profType == 'heap_profile:libc.malloc,com.android.art') { 631 profType = 'heap_profile:com.android.art,libc.malloc'; 632 } 633 const type = profileType(profType); 634 const upid = row.upid; 635 globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type})); 636 } 637 638 private async selectPendingDeeplink(link: PendingDeeplinkState) { 639 const conditions = []; 640 const {ts, dur} = link; 641 642 if (ts !== undefined) { 643 conditions.push(`ts = ${ts}`); 644 } 645 if (dur !== undefined) { 646 conditions.push(`dur = ${dur}`); 647 } 648 649 if (conditions.length === 0) { 650 return; 651 } 652 653 const query = ` 654 select 655 id, 656 track_id as traceProcessorTrackId, 657 type 658 from slice 659 where ${conditions.join(' and ')} 660 ;`; 661 662 const result = await assertExists(this.engine).query(query); 663 if (result.numRows() > 0) { 664 const row = result.firstRow({ 665 id: NUM, 666 traceProcessorTrackId: NUM, 667 type: STR, 668 }); 669 670 const id = row.traceProcessorTrackId; 671 const trackKey = globals.trackManager.trackKeyByTrackId.get(id); 672 if (trackKey === undefined) { 673 return; 674 } 675 globals.setLegacySelection( 676 { 677 kind: 'SLICE', 678 id: row.id, 679 trackKey, 680 table: 'slice', 681 }, 682 { 683 clearSearch: true, 684 pendingScrollId: row.id, 685 switchToCurrentSelectionTab: false, 686 }, 687 ); 688 } 689 } 690 691 private async listTracks() { 692 this.updateStatus('Loading tracks'); 693 const engine = assertExists(this.engine); 694 const actions = await decideTracks(engine); 695 globals.dispatchMultiple(actions); 696 } 697 698 // Show the list of default tabs, but don't make them active! 699 private decideTabs() { 700 for (const tabUri of globals.tabManager.defaultTabs) { 701 globals.dispatch(Actions.showTab({uri: tabUri})); 702 } 703 } 704 705 private async listThreads() { 706 this.updateStatus('Reading thread list'); 707 const query = `select 708 utid, 709 tid, 710 pid, 711 ifnull(thread.name, '') as threadName, 712 ifnull( 713 case when length(process.name) > 0 then process.name else null end, 714 thread.name) as procName, 715 process.cmdline as cmdline 716 from (select * from thread order by upid) as thread 717 left join (select * from process order by upid) as process 718 using(upid)`; 719 const result = await assertExists(this.engine).query(query); 720 const threads: ThreadDesc[] = []; 721 const it = result.iter({ 722 utid: NUM, 723 tid: NUM, 724 pid: NUM_NULL, 725 threadName: STR, 726 procName: STR_NULL, 727 cmdline: STR_NULL, 728 }); 729 for (; it.valid(); it.next()) { 730 const utid = it.utid; 731 const tid = it.tid; 732 const pid = it.pid === null ? undefined : it.pid; 733 const threadName = it.threadName; 734 const procName = it.procName === null ? undefined : it.procName; 735 const cmdline = it.cmdline === null ? undefined : it.cmdline; 736 threads.push({utid, tid, threadName, pid, procName, cmdline}); 737 } 738 publishThreads(threads); 739 } 740 741 private async loadTimelineOverview(trace: Span<time, duration>) { 742 clearOverviewData(); 743 const engine = assertExists<Engine>(this.engine); 744 const stepSize = Duration.max(1n, trace.duration / 100n); 745 const hasSchedSql = 'select ts from sched limit 1'; 746 const hasSchedOverview = (await engine.query(hasSchedSql)).numRows() > 0; 747 if (hasSchedOverview) { 748 const stepPromises = []; 749 for ( 750 let start = trace.start; 751 start < trace.end; 752 start = Time.add(start, stepSize) 753 ) { 754 const progress = start - trace.start; 755 const ratio = Number(progress) / Number(trace.duration); 756 this.updateStatus('Loading overview ' + `${Math.round(ratio * 100)}%`); 757 const end = Time.add(start, stepSize); 758 // The (async() => {})() queues all the 100 async promises in one batch. 759 // Without that, we would wait for each step to be rendered before 760 // kicking off the next one. That would interleave an animation frame 761 // between each step, slowing down significantly the overall process. 762 stepPromises.push( 763 (async () => { 764 const schedResult = await engine.query( 765 `select cast(sum(dur) as float)/${stepSize} as load, cpu from sched ` + 766 `where ts >= ${start} and ts < ${end} and utid != 0 ` + 767 'group by cpu order by cpu', 768 ); 769 const schedData: {[key: string]: QuantizedLoad} = {}; 770 const it = schedResult.iter({load: NUM, cpu: NUM}); 771 for (; it.valid(); it.next()) { 772 const load = it.load; 773 const cpu = it.cpu; 774 schedData[cpu] = {start, end, load}; 775 } 776 publishOverviewData(schedData); 777 })(), 778 ); 779 } // for(start = ...) 780 await Promise.all(stepPromises); 781 return; 782 } // if (hasSchedOverview) 783 784 // Slices overview. 785 const sliceResult = await engine.query(`select 786 bucket, 787 upid, 788 ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load 789 from thread 790 inner join ( 791 select 792 ifnull(cast((ts - ${trace.start})/${stepSize} as int), 0) as bucket, 793 sum(dur) as utid_sum, 794 utid 795 from slice 796 inner join thread_track on slice.track_id = thread_track.id 797 group by bucket, utid 798 ) using(utid) 799 where upid is not null 800 group by bucket, upid`); 801 802 const slicesData: {[key: string]: QuantizedLoad[]} = {}; 803 const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM}); 804 for (; it.valid(); it.next()) { 805 const bucket = it.bucket; 806 const upid = it.upid; 807 const load = it.load; 808 809 const start = Time.add(trace.start, stepSize * bucket); 810 const end = Time.add(start, stepSize); 811 812 const upidStr = upid.toString(); 813 let loadArray = slicesData[upidStr]; 814 if (loadArray === undefined) { 815 loadArray = slicesData[upidStr] = []; 816 } 817 loadArray.push({start, end, load}); 818 } 819 publishOverviewData(slicesData); 820 } 821 822 private async cacheCurrentTrace(): Promise<string> { 823 const engine = assertExists(this.engine); 824 const result = await engine.query(`select str_value as uuid from metadata 825 where name = 'trace_uuid'`); 826 if (result.numRows() === 0) { 827 // One of the cases covered is an empty trace. 828 return ''; 829 } 830 const traceUuid = result.firstRow({uuid: STR}).uuid; 831 const engineConfig = assertExists(globals.state.engine); 832 assertTrue(engineConfig.id === this.engineId); 833 if (!(await cacheTrace(engineConfig.source, traceUuid))) { 834 // If the trace is not cacheable (cacheable means it has been opened from 835 // URL or RPC) only append '?local_cache_key' to the URL, without the 836 // local_cache_key value. Doing otherwise would cause an error if the tab 837 // is discarded or the user hits the reload button because the trace is 838 // not in the cache. 839 return ''; 840 } 841 return traceUuid; 842 } 843 844 async initialiseHelperViews() { 845 const engine = assertExists(this.engine); 846 847 this.updateStatus('Creating annotation counter track table'); 848 // Create the helper tables for all the annotations related data. 849 // NULL in min/max means "figure it out per track in the usual way". 850 await engine.query(` 851 CREATE TABLE annotation_counter_track( 852 id INTEGER PRIMARY KEY, 853 name STRING, 854 __metric_name STRING, 855 upid INTEGER, 856 min_value DOUBLE, 857 max_value DOUBLE 858 ); 859 `); 860 this.updateStatus('Creating annotation slice track table'); 861 await engine.query(` 862 CREATE TABLE annotation_slice_track( 863 id INTEGER PRIMARY KEY, 864 name STRING, 865 __metric_name STRING, 866 upid INTEGER, 867 group_name STRING 868 ); 869 `); 870 871 this.updateStatus('Creating annotation counter table'); 872 await engine.query(` 873 CREATE TABLE annotation_counter( 874 id BIGINT, 875 track_id INT, 876 ts BIGINT, 877 value DOUBLE, 878 PRIMARY KEY (track_id, ts) 879 ) WITHOUT ROWID; 880 `); 881 this.updateStatus('Creating annotation slice table'); 882 await engine.query(` 883 CREATE TABLE annotation_slice( 884 id INTEGER PRIMARY KEY, 885 track_id INT, 886 ts BIGINT, 887 dur BIGINT, 888 thread_dur BIGINT, 889 depth INT, 890 cat STRING, 891 name STRING, 892 UNIQUE(track_id, ts) 893 ); 894 `); 895 896 const availableMetrics = []; 897 const metricsResult = await engine.query('select name from trace_metrics'); 898 for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) { 899 availableMetrics.push(it.name); 900 } 901 902 const availableMetricsSet = new Set<string>(availableMetrics); 903 for (const [flag, metric] of FLAGGED_METRICS) { 904 if (!flag.get() || !availableMetricsSet.has(metric)) { 905 continue; 906 } 907 908 this.updateStatus(`Computing ${metric} metric`); 909 try { 910 // We don't care about the actual result of metric here as we are just 911 // interested in the annotation tracks. 912 await engine.computeMetric([metric], 'proto'); 913 } catch (e) { 914 if (e instanceof QueryError) { 915 publishMetricError('MetricError: ' + e.message); 916 continue; 917 } else { 918 throw e; 919 } 920 } 921 922 this.updateStatus(`Inserting data for ${metric} metric`); 923 try { 924 const result = await engine.query(`pragma table_info(${metric}_event)`); 925 let hasSliceName = false; 926 let hasDur = false; 927 let hasUpid = false; 928 let hasValue = false; 929 let hasGroupName = false; 930 const it = result.iter({name: STR}); 931 for (; it.valid(); it.next()) { 932 const name = it.name; 933 hasSliceName = hasSliceName || name === 'slice_name'; 934 hasDur = hasDur || name === 'dur'; 935 hasUpid = hasUpid || name === 'upid'; 936 hasValue = hasValue || name === 'value'; 937 hasGroupName = hasGroupName || name === 'group_name'; 938 } 939 940 const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid'; 941 const upidColumnWhere = hasUpid ? 'upid' : '0'; 942 const groupNameColumn = hasGroupName 943 ? 'group_name' 944 : 'NULL AS group_name'; 945 if (hasSliceName && hasDur) { 946 await engine.query(` 947 INSERT INTO annotation_slice_track( 948 name, __metric_name, upid, group_name) 949 SELECT DISTINCT 950 track_name, 951 '${metric}' as metric_name, 952 ${upidColumnSelect}, 953 ${groupNameColumn} 954 FROM ${metric}_event 955 WHERE track_type = 'slice' 956 `); 957 await engine.query(` 958 INSERT INTO annotation_slice( 959 track_id, ts, dur, thread_dur, depth, cat, name 960 ) 961 SELECT 962 t.id AS track_id, 963 ts, 964 dur, 965 NULL as thread_dur, 966 0 AS depth, 967 a.track_name as cat, 968 slice_name AS name 969 FROM ${metric}_event a 970 JOIN annotation_slice_track t 971 ON a.track_name = t.name AND t.__metric_name = '${metric}' 972 ORDER BY t.id, ts 973 `); 974 } 975 976 if (hasValue) { 977 const minMax = await engine.query(` 978 SELECT 979 IFNULL(MIN(value), 0) as minValue, 980 IFNULL(MAX(value), 0) as maxValue 981 FROM ${metric}_event 982 WHERE ${upidColumnWhere} != 0`); 983 const row = minMax.firstRow({minValue: NUM, maxValue: NUM}); 984 await engine.query(` 985 INSERT INTO annotation_counter_track( 986 name, __metric_name, min_value, max_value, upid) 987 SELECT DISTINCT 988 track_name, 989 '${metric}' as metric_name, 990 CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.minValue} END, 991 CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.maxValue} END, 992 ${upidColumnSelect} 993 FROM ${metric}_event 994 WHERE track_type = 'counter' 995 `); 996 await engine.query(` 997 INSERT INTO annotation_counter(id, track_id, ts, value) 998 SELECT 999 -1 as id, 1000 t.id AS track_id, 1001 ts, 1002 value 1003 FROM ${metric}_event a 1004 JOIN annotation_counter_track t 1005 ON a.track_name = t.name AND t.__metric_name = '${metric}' 1006 ORDER BY t.id, ts 1007 `); 1008 } 1009 } catch (e) { 1010 if (e instanceof QueryError) { 1011 publishMetricError('MetricError: ' + e.message); 1012 } else { 1013 throw e; 1014 } 1015 } 1016 } 1017 } 1018 1019 async includeSummaryTables() { 1020 const engine = assertExists<Engine>(this.engine); 1021 1022 this.updateStatus('Creating slice summaries'); 1023 await engine.query(`include perfetto module viz.summary.slices;`); 1024 1025 this.updateStatus('Creating counter summaries'); 1026 await engine.query(`include perfetto module viz.summary.counters;`); 1027 1028 this.updateStatus('Creating thread summaries'); 1029 await engine.query(`include perfetto module viz.summary.threads;`); 1030 1031 this.updateStatus('Creating processes summaries'); 1032 await engine.query(`include perfetto module viz.summary.processes;`); 1033 1034 this.updateStatus('Creating track summaries'); 1035 await engine.query(`include perfetto module viz.summary.tracks;`); 1036 } 1037 1038 private updateStatus(msg: string): void { 1039 globals.dispatch( 1040 Actions.updateStatus({ 1041 msg, 1042 timestamp: Date.now() / 1000, 1043 }), 1044 ); 1045 } 1046 1047 private zoomPendingDeeplink(visStart: string, visEnd: string) { 1048 const visualStart = Time.fromRaw(BigInt(visStart)); 1049 const visualEnd = Time.fromRaw(BigInt(visEnd)); 1050 const traceTime = globals.stateTraceTimeTP(); 1051 1052 if ( 1053 !( 1054 visualStart < visualEnd && 1055 traceTime.start <= visualStart && 1056 visualEnd <= traceTime.end 1057 ) 1058 ) { 1059 return; 1060 } 1061 1062 const res = (visualEnd - visualStart) / 1000n; 1063 1064 globals.dispatch( 1065 Actions.setVisibleTraceTime({ 1066 start: visualStart, 1067 end: visualEnd, 1068 resolution: BigintMath.max(res, 1n), 1069 lastUpdate: Date.now() / 1000, 1070 }), 1071 ); 1072 } 1073} 1074 1075async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> { 1076 const result = await engine.query(` 1077 SELECT min(ts) as start, max(ts) as end FROM ftrace_event; 1078 `); 1079 const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL}); 1080 if (start !== null && end !== null) { 1081 return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end)); 1082 } 1083 return null; 1084} 1085 1086async function computeTraceReliableRangeStart(engine: Engine): Promise<time> { 1087 const result = 1088 await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql'); 1089 SELECT start FROM chrome_reliable_range`); 1090 const bounds = result.firstRow({start: LONG}); 1091 return Time.fromRaw(bounds.start); 1092} 1093 1094async function computeVisibleTime( 1095 traceStart: time, 1096 traceEnd: time, 1097 isJsonTrace: boolean, 1098 engine: Engine, 1099): Promise<Span<HighPrecisionTime>> { 1100 // if we have non-default visible state, update the visible time to it 1101 const previousVisibleState = globals.stateVisibleTime(); 1102 const defaultTraceSpan = new TimeSpan( 1103 defaultTraceContext.start, 1104 defaultTraceContext.end, 1105 ); 1106 if ( 1107 !( 1108 previousVisibleState.start === defaultTraceSpan.start && 1109 previousVisibleState.end === defaultTraceSpan.end 1110 ) && 1111 previousVisibleState.start >= traceStart && 1112 previousVisibleState.end <= traceEnd 1113 ) { 1114 return HighPrecisionTimeSpan.fromTime( 1115 previousVisibleState.start, 1116 previousVisibleState.end, 1117 ); 1118 } 1119 1120 // initialise visible time to the trace time bounds 1121 let visibleStart = traceStart; 1122 let visibleEnd = traceEnd; 1123 1124 // compare start and end with metadata computed by the trace processor 1125 const mdTime = await getTracingMetadataTimeBounds(engine); 1126 // make sure the bounds hold 1127 if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) { 1128 visibleStart = Time.max(visibleStart, mdTime.start); 1129 visibleEnd = Time.min(visibleEnd, mdTime.end); 1130 } 1131 1132 // Trace Processor doesn't support the reliable range feature for JSON 1133 // traces. 1134 if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) { 1135 const reliableRangeStart = await computeTraceReliableRangeStart(engine); 1136 visibleStart = Time.max(visibleStart, reliableRangeStart); 1137 } 1138 1139 // Move start of visible window to the first ftrace event 1140 const ftraceBounds = await computeFtraceBounds(engine); 1141 if (ftraceBounds !== null) { 1142 // Avoid moving start of visible window past its end! 1143 visibleStart = Time.min(ftraceBounds.start, visibleEnd); 1144 } 1145 return HighPrecisionTimeSpan.fromTime(visibleStart, visibleEnd); 1146} 1147 1148async function getTraceTimeDetails(engine: Engine): Promise<TraceContext> { 1149 const traceTime = await getTraceTimeBounds(engine); 1150 1151 // Find the first REALTIME or REALTIME_COARSE clock snapshot. 1152 // Prioritize REALTIME over REALTIME_COARSE. 1153 const query = `select 1154 ts, 1155 clock_value as clockValue, 1156 clock_name as clockName 1157 from clock_snapshot 1158 where 1159 snapshot_id = 0 AND 1160 clock_name in ('REALTIME', 'REALTIME_COARSE') 1161 `; 1162 const result = await engine.query(query); 1163 const it = result.iter({ 1164 ts: LONG, 1165 clockValue: LONG, 1166 clockName: STR, 1167 }); 1168 1169 let snapshot = { 1170 clockName: '', 1171 ts: Time.ZERO, 1172 clockValue: Time.ZERO, 1173 }; 1174 1175 // Find the most suitable snapshot 1176 for (let row = 0; it.valid(); it.next(), row++) { 1177 if (it.clockName === 'REALTIME') { 1178 snapshot = { 1179 clockName: it.clockName, 1180 ts: Time.fromRaw(it.ts), 1181 clockValue: Time.fromRaw(it.clockValue), 1182 }; 1183 break; 1184 } else if (it.clockName === 'REALTIME_COARSE') { 1185 if (snapshot.clockName !== 'REALTIME') { 1186 snapshot = { 1187 clockName: it.clockName, 1188 ts: Time.fromRaw(it.ts), 1189 clockValue: Time.fromRaw(it.clockValue), 1190 }; 1191 } 1192 } 1193 } 1194 1195 // The max() is so the query returns NULL if the tz info doesn't exist. 1196 const queryTz = `select max(int_value) as tzOffMin from metadata 1197 where name = 'timezone_off_mins'`; 1198 const resTz = await assertExists(engine).query(queryTz); 1199 const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0; 1200 1201 // This is the offset between the unix epoch and ts in the ts domain. 1202 // I.e. the value of ts at the time of the unix epoch - usually some large 1203 // negative value. 1204 const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue); 1205 1206 // Find the previous closest midnight from the trace start time. 1207 const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset); 1208 1209 const traceTzOffset = Time.getLatestMidnight( 1210 traceTime.start, 1211 Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)), 1212 ); 1213 1214 return { 1215 ...traceTime, 1216 realtimeOffset, 1217 utcOffset, 1218 traceTzOffset, 1219 cpus: await getCpus(engine), 1220 gpuCount: await getNumberOfGpus(engine), 1221 }; 1222} 1223 1224async function getTraceTimeBounds( 1225 engine: Engine, 1226): Promise<Span<time, duration>> { 1227 const result = await engine.query( 1228 `select start_ts as startTs, end_ts as endTs from trace_bounds`, 1229 ); 1230 const bounds = result.firstRow({ 1231 startTs: LONG, 1232 endTs: LONG, 1233 }); 1234 return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs)); 1235} 1236 1237// TODO(hjd): When streaming must invalidate this somehow. 1238async function getCpus(engine: Engine): Promise<number[]> { 1239 const cpus = []; 1240 const queryRes = await engine.query( 1241 'select distinct(cpu) as cpu from sched order by cpu;', 1242 ); 1243 for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) { 1244 cpus.push(it.cpu); 1245 } 1246 return cpus; 1247} 1248 1249async function getNumberOfGpus(engine: Engine): Promise<number> { 1250 const result = await engine.query(` 1251 select count(distinct(gpu_id)) as gpuCount 1252 from gpu_counter_track 1253 where name = 'gpufreq'; 1254 `); 1255 return result.firstRow({gpuCount: NUM}).gpuCount; 1256} 1257 1258async function getTracingMetadataTimeBounds( 1259 engine: Engine, 1260): Promise<Span<time, duration>> { 1261 const queryRes = await engine.query(`select 1262 name, 1263 int_value as intValue 1264 from metadata 1265 where name = 'tracing_started_ns' or name = 'tracing_disabled_ns' 1266 or name = 'all_data_source_started_ns'`); 1267 let startBound = Time.MIN; 1268 let endBound = Time.MAX; 1269 const it = queryRes.iter({name: STR, intValue: LONG_NULL}); 1270 for (; it.valid(); it.next()) { 1271 const columnName = it.name; 1272 const timestamp = it.intValue; 1273 if (timestamp === null) continue; 1274 if (columnName === 'tracing_disabled_ns') { 1275 endBound = Time.min(endBound, Time.fromRaw(timestamp)); 1276 } else { 1277 startBound = Time.max(startBound, Time.fromRaw(timestamp)); 1278 } 1279 } 1280 1281 return new TimeSpan(startBound, endBound); 1282} 1283