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, assertTrue} from '../base/logging'; 16import {time, Time, TimeSpan} from '../base/time'; 17import {cacheTrace} from './cache_manager'; 18import {Cpu} from '../base/multi_machine_trace'; 19import { 20 getEnabledMetatracingCategories, 21 isMetatracingEnabled, 22} from './metatracing'; 23import {featureFlags} from './feature_flags'; 24import {Engine, EngineBase} from '../trace_processor/engine'; 25import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; 26import { 27 LONG, 28 LONG_NULL, 29 NUM, 30 NUM_NULL, 31 STR, 32} from '../trace_processor/query_result'; 33import {WasmEngineProxy} from '../trace_processor/wasm_engine_proxy'; 34import { 35 TraceBufferStream, 36 TraceFileStream, 37 TraceHttpStream, 38 TraceStream, 39} from '../core/trace_stream'; 40import { 41 deserializeAppStatePhase1, 42 deserializeAppStatePhase2, 43} from './state_serialization'; 44import {AppImpl} from './app_impl'; 45import {raf} from './raf_scheduler'; 46import {TraceImpl} from './trace_impl'; 47import {SerializedAppState} from './state_serialization_schema'; 48import {TraceSource} from './trace_source'; 49import {Router} from '../core/router'; 50import {TraceInfoImpl} from './trace_info_impl'; 51 52const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({ 53 id: 'enableChromeReliableRangeZoom', 54 name: 'Enable Chrome reliable range zoom', 55 description: 'Automatically zoom into the reliable range for Chrome traces', 56 defaultValue: false, 57}); 58 59const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({ 60 id: 'enableChromeReliableRangeAnnotation', 61 name: 'Enable Chrome reliable range annotation', 62 description: 'Automatically adds an annotation for the reliable range start', 63 defaultValue: false, 64}); 65 66// The following flags control TraceProcessor Config. 67const CROP_TRACK_EVENTS_FLAG = featureFlags.register({ 68 id: 'cropTrackEvents', 69 name: 'Crop track events', 70 description: 'Ignores track events outside of the range of interest', 71 defaultValue: false, 72}); 73const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({ 74 id: 'ingestFtraceInRawTable', 75 name: 'Ingest ftrace in raw table', 76 description: 'Enables ingestion of typed ftrace events into the raw table', 77 defaultValue: true, 78}); 79const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({ 80 id: 'analyzeTraceProtoContent', 81 name: 'Analyze trace proto content', 82 description: 83 'Enables trace proto content analysis (experimental_proto_content table)', 84 defaultValue: false, 85}); 86const FTRACE_DROP_UNTIL_FLAG = featureFlags.register({ 87 id: 'ftraceDropUntilAllCpusValid', 88 name: 'Crop ftrace events', 89 description: 90 'Drop ftrace events until all per-cpu data streams are known to be valid', 91 defaultValue: true, 92}); 93 94// TODO(stevegolton): Move this into some global "SQL extensions" file and 95// ensure it's only run once. 96async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise<void> { 97 await engine.query(` 98 create perfetto function __max_layout_depth(track_count INT, track_ids STRING) 99 returns INT AS 100 select iif( 101 $track_count = 1, 102 ( 103 select max_depth 104 from _slice_track_summary 105 where id = cast($track_ids AS int) 106 ), 107 ( 108 select max(layout_depth) 109 from experimental_slice_layout($track_ids) 110 ) 111 ); 112 `); 113} 114 115let lastEngineId = 0; 116 117export async function loadTrace( 118 app: AppImpl, 119 traceSource: TraceSource, 120): Promise<TraceImpl> { 121 updateStatus(app, 'Opening trace'); 122 const engineId = `${++lastEngineId}`; 123 const engine = await createEngine(app, engineId); 124 return await loadTraceIntoEngine(app, traceSource, engine); 125} 126 127async function createEngine( 128 app: AppImpl, 129 engineId: string, 130): Promise<EngineBase> { 131 // Check if there is any instance of the trace_processor_shell running in 132 // HTTP RPC mode (i.e. trace_processor_shell -D). 133 let useRpc = false; 134 if (app.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') { 135 useRpc = (await HttpRpcEngine.checkConnection()).connected; 136 } 137 let engine; 138 if (useRpc) { 139 console.log('Opening trace using native accelerator over HTTP+RPC'); 140 engine = new HttpRpcEngine(engineId); 141 } else { 142 console.log('Opening trace using built-in WASM engine'); 143 engine = new WasmEngineProxy(engineId); 144 engine.resetTraceProcessor({ 145 cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(), 146 ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(), 147 analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(), 148 ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(), 149 }); 150 } 151 engine.onResponseReceived = () => raf.scheduleFullRedraw(); 152 153 if (isMetatracingEnabled()) { 154 engine.enableMetatrace(assertExists(getEnabledMetatracingCategories())); 155 } 156 return engine; 157} 158 159async function loadTraceIntoEngine( 160 app: AppImpl, 161 traceSource: TraceSource, 162 engine: EngineBase, 163): Promise<TraceImpl> { 164 let traceStream: TraceStream | undefined; 165 let serializedAppState: SerializedAppState | undefined; 166 if (traceSource.type === 'FILE') { 167 traceStream = new TraceFileStream(traceSource.file); 168 } else if (traceSource.type === 'ARRAY_BUFFER') { 169 traceStream = new TraceBufferStream(traceSource.buffer); 170 } else if (traceSource.type === 'URL') { 171 traceStream = new TraceHttpStream(traceSource.url); 172 serializedAppState = traceSource.serializedAppState; 173 } else if (traceSource.type === 'HTTP_RPC') { 174 traceStream = undefined; 175 } else { 176 throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`); 177 } 178 179 // |traceStream| can be undefined in the case when we are using the external 180 // HTTP+RPC endpoint and the trace processor instance has already loaded 181 // a trace (because it was passed as a cmdline argument to 182 // trace_processor_shell). In this case we don't want the UI to load any 183 // file/stream and we just want to jump to the loading phase. 184 if (traceStream !== undefined) { 185 const tStart = performance.now(); 186 for (;;) { 187 const res = await traceStream.readChunk(); 188 await engine.parse(res.data); 189 const elapsed = (performance.now() - tStart) / 1000; 190 let status = 'Loading trace '; 191 if (res.bytesTotal > 0) { 192 const progress = Math.round((res.bytesRead / res.bytesTotal) * 100); 193 status += `${progress}%`; 194 } else { 195 status += `${Math.round(res.bytesRead / 1e6)} MB`; 196 } 197 status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`; 198 updateStatus(app, status); 199 if (res.eof) break; 200 } 201 await engine.notifyEof(); 202 } else { 203 assertTrue(engine instanceof HttpRpcEngine); 204 await engine.restoreInitialTables(); 205 } 206 for (const p of app.extraSqlPackages) { 207 await engine.registerSqlPackages(p); 208 } 209 210 const traceDetails = await getTraceInfo(engine, traceSource); 211 const trace = TraceImpl.createInstanceForCore(app, engine, traceDetails); 212 app.setActiveTrace(trace); 213 214 const visibleTimeSpan = await computeVisibleTime( 215 traceDetails.start, 216 traceDetails.end, 217 trace.traceInfo.traceType === 'json', 218 engine, 219 ); 220 221 trace.timeline.updateVisibleTime(visibleTimeSpan); 222 223 const cacheUuid = traceDetails.cached ? traceDetails.uuid : ''; 224 Router.navigate(`#!/viewer?local_cache_key=${cacheUuid}`); 225 226 // Make sure the helper views are available before we start adding tracks. 227 await includeSummaryTables(trace); 228 229 await defineMaxLayoutDepthSqlFunction(engine); 230 231 if (serializedAppState !== undefined) { 232 deserializeAppStatePhase1(serializedAppState, trace); 233 } 234 235 await app.plugins.onTraceLoad(trace, (id) => { 236 updateStatus(app, `Running plugin: ${id}`); 237 }); 238 239 decideTabs(trace); 240 241 // Trace Processor doesn't support the reliable range feature for JSON 242 // traces. 243 if ( 244 trace.traceInfo.traceType !== 'json' && 245 ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get() 246 ) { 247 const reliableRangeStart = await computeTraceReliableRangeStart(engine); 248 if (reliableRangeStart > 0) { 249 trace.notes.addNote({ 250 timestamp: reliableRangeStart, 251 color: '#ff0000', 252 text: 'Reliable Range Start', 253 }); 254 } 255 } 256 257 // notify() will await that all listeners' promises have resolved. 258 await trace.onTraceReady.notify(); 259 260 if (serializedAppState !== undefined) { 261 // Wait that plugins have completed their actions and then proceed with 262 // the final phase of app state restore. 263 // TODO(primiano): this can probably be removed once we refactor tracks 264 // to be URI based and can deal with non-existing URIs. 265 deserializeAppStatePhase2(serializedAppState, trace); 266 } 267 268 return trace; 269} 270 271function decideTabs(trace: TraceImpl) { 272 // Show the list of default tabs, but don't make them active! 273 for (const tabUri of trace.tabs.defaultTabs) { 274 trace.tabs.showTab(tabUri); 275 } 276} 277 278async function includeSummaryTables(trace: TraceImpl) { 279 const engine = trace.engine; 280 updateStatus(trace, 'Creating slice summaries'); 281 await engine.query(`include perfetto module viz.summary.slices;`); 282 283 updateStatus(trace, 'Creating counter summaries'); 284 await engine.query(`include perfetto module viz.summary.counters;`); 285 286 updateStatus(trace, 'Creating thread summaries'); 287 await engine.query(`include perfetto module viz.summary.threads;`); 288 289 updateStatus(trace, 'Creating processes summaries'); 290 await engine.query(`include perfetto module viz.summary.processes;`); 291} 292 293function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void { 294 const showUntilDismissed = 0; 295 traceOrApp.omnibox.showStatusMessage(msg, showUntilDismissed); 296} 297 298async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> { 299 const result = await engine.query(` 300 SELECT min(ts) as start, max(ts) as end FROM ftrace_event; 301 `); 302 const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL}); 303 if (start !== null && end !== null) { 304 return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end)); 305 } 306 return null; 307} 308 309async function computeTraceReliableRangeStart(engine: Engine): Promise<time> { 310 const result = 311 await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql'); 312 SELECT start FROM chrome_reliable_range`); 313 const bounds = result.firstRow({start: LONG}); 314 return Time.fromRaw(bounds.start); 315} 316 317async function computeVisibleTime( 318 traceStart: time, 319 traceEnd: time, 320 isJsonTrace: boolean, 321 engine: Engine, 322): Promise<TimeSpan> { 323 // initialise visible time to the trace time bounds 324 let visibleStart = traceStart; 325 let visibleEnd = traceEnd; 326 327 // compare start and end with metadata computed by the trace processor 328 const mdTime = await getTracingMetadataTimeBounds(engine); 329 // make sure the bounds hold 330 if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) { 331 visibleStart = Time.max(visibleStart, mdTime.start); 332 visibleEnd = Time.min(visibleEnd, mdTime.end); 333 } 334 335 // Trace Processor doesn't support the reliable range feature for JSON 336 // traces. 337 if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) { 338 const reliableRangeStart = await computeTraceReliableRangeStart(engine); 339 visibleStart = Time.max(visibleStart, reliableRangeStart); 340 } 341 342 // Move start of visible window to the first ftrace event 343 const ftraceBounds = await computeFtraceBounds(engine); 344 if (ftraceBounds !== null) { 345 // Avoid moving start of visible window past its end! 346 visibleStart = Time.min(ftraceBounds.start, visibleEnd); 347 } 348 return new TimeSpan(visibleStart, visibleEnd); 349} 350 351async function getTraceInfo( 352 engine: Engine, 353 traceSource: TraceSource, 354): Promise<TraceInfoImpl> { 355 const traceTime = await getTraceTimeBounds(engine); 356 357 // Find the first REALTIME or REALTIME_COARSE clock snapshot. 358 // Prioritize REALTIME over REALTIME_COARSE. 359 const query = `select 360 ts, 361 clock_value as clockValue, 362 clock_name as clockName 363 from clock_snapshot 364 where 365 snapshot_id = 0 AND 366 clock_name in ('REALTIME', 'REALTIME_COARSE') 367 `; 368 const result = await engine.query(query); 369 const it = result.iter({ 370 ts: LONG, 371 clockValue: LONG, 372 clockName: STR, 373 }); 374 375 let snapshot = { 376 clockName: '', 377 ts: Time.ZERO, 378 clockValue: Time.ZERO, 379 }; 380 381 // Find the most suitable snapshot 382 for (let row = 0; it.valid(); it.next(), row++) { 383 if (it.clockName === 'REALTIME') { 384 snapshot = { 385 clockName: it.clockName, 386 ts: Time.fromRaw(it.ts), 387 clockValue: Time.fromRaw(it.clockValue), 388 }; 389 break; 390 } else if (it.clockName === 'REALTIME_COARSE') { 391 if (snapshot.clockName !== 'REALTIME') { 392 snapshot = { 393 clockName: it.clockName, 394 ts: Time.fromRaw(it.ts), 395 clockValue: Time.fromRaw(it.clockValue), 396 }; 397 } 398 } 399 } 400 401 // The max() is so the query returns NULL if the tz info doesn't exist. 402 const queryTz = `select max(int_value) as tzOffMin from metadata 403 where name = 'timezone_off_mins'`; 404 const resTz = await assertExists(engine).query(queryTz); 405 const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0; 406 407 // This is the offset between the unix epoch and ts in the ts domain. 408 // I.e. the value of ts at the time of the unix epoch - usually some large 409 // negative value. 410 const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue); 411 412 // Find the previous closest midnight from the trace start time. 413 const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset); 414 415 const traceTzOffset = Time.getLatestMidnight( 416 traceTime.start, 417 Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)), 418 ); 419 420 let traceTitle = ''; 421 let traceUrl = ''; 422 switch (traceSource.type) { 423 case 'FILE': 424 // Split on both \ and / (because C:\Windows\paths\are\like\this). 425 traceTitle = traceSource.file.name.split(/[/\\]/).pop()!; 426 const fileSizeMB = Math.ceil(traceSource.file.size / 1e6); 427 traceTitle += ` (${fileSizeMB} MB)`; 428 break; 429 case 'URL': 430 traceUrl = traceSource.url; 431 traceTitle = traceUrl.split('/').pop()!; 432 break; 433 case 'ARRAY_BUFFER': 434 traceTitle = traceSource.title; 435 traceUrl = traceSource.url ?? ''; 436 const arrayBufferSizeMB = Math.ceil(traceSource.buffer.byteLength / 1e6); 437 traceTitle += ` (${arrayBufferSizeMB} MB)`; 438 break; 439 case 'HTTP_RPC': 440 traceTitle = `RPC @ ${HttpRpcEngine.hostAndPort}`; 441 break; 442 default: 443 break; 444 } 445 446 const traceType = await getTraceType(engine); 447 448 const hasFtrace = 449 (await engine.query(`select * from ftrace_event limit 1`)).numRows() > 0; 450 451 const uuidRes = await engine.query(`select str_value as uuid from metadata 452 where name = 'trace_uuid'`); 453 // trace_uuid can be missing from the TP tables if the trace is empty or in 454 // other similar edge cases. 455 const uuid = uuidRes.numRows() > 0 ? uuidRes.firstRow({uuid: STR}).uuid : ''; 456 const cached = await cacheTrace(traceSource, uuid); 457 458 const downloadable = 459 (traceSource.type === 'ARRAY_BUFFER' && !traceSource.localOnly) || 460 traceSource.type === 'FILE' || 461 traceSource.type === 'URL'; 462 463 return { 464 ...traceTime, 465 traceTitle, 466 traceUrl, 467 realtimeOffset, 468 utcOffset, 469 traceTzOffset, 470 cpus: await getCpus(engine), 471 importErrors: await getTraceErrors(engine), 472 source: traceSource, 473 traceType, 474 hasFtrace, 475 uuid, 476 cached, 477 downloadable, 478 }; 479} 480 481async function getTraceType(engine: Engine) { 482 const result = await engine.query( 483 `select str_value from metadata where name = 'trace_type'`, 484 ); 485 486 if (result.numRows() === 0) return undefined; 487 return result.firstRow({str_value: STR}).str_value; 488} 489 490async function getTraceTimeBounds(engine: Engine): Promise<TimeSpan> { 491 const result = await engine.query( 492 `select start_ts as startTs, end_ts as endTs from trace_bounds`, 493 ); 494 const bounds = result.firstRow({ 495 startTs: LONG, 496 endTs: LONG, 497 }); 498 return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs)); 499} 500 501// TODO(hjd): When streaming must invalidate this somehow. 502async function getCpus(engine: Engine): Promise<Cpu[]> { 503 const cpus: Cpu[] = []; 504 const queryRes = await engine.query( 505 `select ucpu, cpu, ifnull(machine_id, 0) as machine from cpu`, 506 ); 507 for ( 508 const it = queryRes.iter({ucpu: NUM, cpu: NUM, machine: NUM}); 509 it.valid(); 510 it.next() 511 ) { 512 cpus.push(new Cpu(it.ucpu, it.cpu, it.machine)); 513 } 514 return cpus; 515} 516 517async function getTraceErrors(engine: Engine): Promise<number> { 518 const sql = `SELECT sum(value) as errs FROM stats WHERE severity != 'info'`; 519 const result = await engine.query(sql); 520 return result.firstRow({errs: NUM}).errs; 521} 522 523async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> { 524 const queryRes = await engine.query(`select 525 name, 526 int_value as intValue 527 from metadata 528 where name = 'tracing_started_ns' or name = 'tracing_disabled_ns' 529 or name = 'all_data_source_started_ns'`); 530 let startBound = Time.MIN; 531 let endBound = Time.MAX; 532 const it = queryRes.iter({name: STR, intValue: LONG_NULL}); 533 for (; it.valid(); it.next()) { 534 const columnName = it.name; 535 const timestamp = it.intValue; 536 if (timestamp === null) continue; 537 if (columnName === 'tracing_disabled_ns') { 538 endBound = Time.min(endBound, Time.fromRaw(timestamp)); 539 } else { 540 startBound = Time.max(startBound, Time.fromRaw(timestamp)); 541 } 542 } 543 544 return new TimeSpan(startBound, endBound); 545} 546