// Copyright (C) 2018 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {assertExists, assertTrue} from '../base/logging'; import {time, Time, TimeSpan} from '../base/time'; import {cacheTrace} from './cache_manager'; import {Cpu} from '../base/multi_machine_trace'; import { getEnabledMetatracingCategories, isMetatracingEnabled, } from './metatracing'; import {featureFlags} from './feature_flags'; import {Engine, EngineBase} from '../trace_processor/engine'; import {HttpRpcEngine} from '../trace_processor/http_rpc_engine'; import { LONG, LONG_NULL, NUM, NUM_NULL, STR, } from '../trace_processor/query_result'; import {WasmEngineProxy} from '../trace_processor/wasm_engine_proxy'; import { TraceBufferStream, TraceFileStream, TraceHttpStream, TraceStream, } from '../core/trace_stream'; import { deserializeAppStatePhase1, deserializeAppStatePhase2, } from './state_serialization'; import {AppImpl} from './app_impl'; import {raf} from './raf_scheduler'; import {TraceImpl} from './trace_impl'; import {SerializedAppState} from './state_serialization_schema'; import {TraceSource} from './trace_source'; import {Router} from '../core/router'; import {TraceInfoImpl} from './trace_info_impl'; const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({ id: 'enableChromeReliableRangeZoom', name: 'Enable Chrome reliable range zoom', description: 'Automatically zoom into the reliable range for Chrome traces', defaultValue: false, }); const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({ id: 'enableChromeReliableRangeAnnotation', name: 'Enable Chrome reliable range annotation', description: 'Automatically adds an annotation for the reliable range start', defaultValue: false, }); // The following flags control TraceProcessor Config. const CROP_TRACK_EVENTS_FLAG = featureFlags.register({ id: 'cropTrackEvents', name: 'Crop track events', description: 'Ignores track events outside of the range of interest', defaultValue: false, }); const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({ id: 'ingestFtraceInRawTable', name: 'Ingest ftrace in raw table', description: 'Enables ingestion of typed ftrace events into the raw table', defaultValue: true, }); const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({ id: 'analyzeTraceProtoContent', name: 'Analyze trace proto content', description: 'Enables trace proto content analysis (experimental_proto_content table)', defaultValue: false, }); const FTRACE_DROP_UNTIL_FLAG = featureFlags.register({ id: 'ftraceDropUntilAllCpusValid', name: 'Crop ftrace events', description: 'Drop ftrace events until all per-cpu data streams are known to be valid', defaultValue: true, }); // TODO(stevegolton): Move this into some global "SQL extensions" file and // ensure it's only run once. async function defineMaxLayoutDepthSqlFunction(engine: Engine): Promise { await engine.query(` create perfetto function __max_layout_depth(track_count INT, track_ids STRING) returns INT AS select iif( $track_count = 1, ( select max_depth from _slice_track_summary where id = cast($track_ids AS int) ), ( select max(layout_depth) from experimental_slice_layout($track_ids) ) ); `); } let lastEngineId = 0; export async function loadTrace( app: AppImpl, traceSource: TraceSource, ): Promise { updateStatus(app, 'Opening trace'); const engineId = `${++lastEngineId}`; const engine = await createEngine(app, engineId); return await loadTraceIntoEngine(app, traceSource, engine); } async function createEngine( app: AppImpl, engineId: string, ): Promise { // Check if there is any instance of the trace_processor_shell running in // HTTP RPC mode (i.e. trace_processor_shell -D). let useRpc = false; if (app.httpRpc.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') { useRpc = (await HttpRpcEngine.checkConnection()).connected; } let engine; if (useRpc) { console.log('Opening trace using native accelerator over HTTP+RPC'); engine = new HttpRpcEngine(engineId); } else { console.log('Opening trace using built-in WASM engine'); engine = new WasmEngineProxy(engineId); engine.resetTraceProcessor({ cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(), ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(), analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(), ftraceDropUntilAllCpusValid: FTRACE_DROP_UNTIL_FLAG.get(), }); } engine.onResponseReceived = () => raf.scheduleFullRedraw(); if (isMetatracingEnabled()) { engine.enableMetatrace(assertExists(getEnabledMetatracingCategories())); } return engine; } async function loadTraceIntoEngine( app: AppImpl, traceSource: TraceSource, engine: EngineBase, ): Promise { let traceStream: TraceStream | undefined; let serializedAppState: SerializedAppState | undefined; if (traceSource.type === 'FILE') { traceStream = new TraceFileStream(traceSource.file); } else if (traceSource.type === 'ARRAY_BUFFER') { traceStream = new TraceBufferStream(traceSource.buffer); } else if (traceSource.type === 'URL') { traceStream = new TraceHttpStream(traceSource.url); serializedAppState = traceSource.serializedAppState; } else if (traceSource.type === 'HTTP_RPC') { traceStream = undefined; } else { throw new Error(`Unknown source: ${JSON.stringify(traceSource)}`); } // |traceStream| can be undefined in the case when we are using the external // HTTP+RPC endpoint and the trace processor instance has already loaded // a trace (because it was passed as a cmdline argument to // trace_processor_shell). In this case we don't want the UI to load any // file/stream and we just want to jump to the loading phase. if (traceStream !== undefined) { const tStart = performance.now(); for (;;) { const res = await traceStream.readChunk(); await engine.parse(res.data); const elapsed = (performance.now() - tStart) / 1000; let status = 'Loading trace '; if (res.bytesTotal > 0) { const progress = Math.round((res.bytesRead / res.bytesTotal) * 100); status += `${progress}%`; } else { status += `${Math.round(res.bytesRead / 1e6)} MB`; } status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`; updateStatus(app, status); if (res.eof) break; } await engine.notifyEof(); } else { assertTrue(engine instanceof HttpRpcEngine); await engine.restoreInitialTables(); } for (const p of app.extraSqlPackages) { await engine.registerSqlPackages(p); } const traceDetails = await getTraceInfo(engine, traceSource); const trace = TraceImpl.createInstanceForCore(app, engine, traceDetails); app.setActiveTrace(trace); const visibleTimeSpan = await computeVisibleTime( traceDetails.start, traceDetails.end, trace.traceInfo.traceType === 'json', engine, ); trace.timeline.updateVisibleTime(visibleTimeSpan); const cacheUuid = traceDetails.cached ? traceDetails.uuid : ''; Router.navigate(`#!/viewer?local_cache_key=${cacheUuid}`); // Make sure the helper views are available before we start adding tracks. await includeSummaryTables(trace); await defineMaxLayoutDepthSqlFunction(engine); if (serializedAppState !== undefined) { deserializeAppStatePhase1(serializedAppState, trace); } await app.plugins.onTraceLoad(trace, (id) => { updateStatus(app, `Running plugin: ${id}`); }); decideTabs(trace); // Trace Processor doesn't support the reliable range feature for JSON // traces. if ( trace.traceInfo.traceType !== 'json' && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get() ) { const reliableRangeStart = await computeTraceReliableRangeStart(engine); if (reliableRangeStart > 0) { trace.notes.addNote({ timestamp: reliableRangeStart, color: '#ff0000', text: 'Reliable Range Start', }); } } // notify() will await that all listeners' promises have resolved. await trace.onTraceReady.notify(); if (serializedAppState !== undefined) { // Wait that plugins have completed their actions and then proceed with // the final phase of app state restore. // TODO(primiano): this can probably be removed once we refactor tracks // to be URI based and can deal with non-existing URIs. deserializeAppStatePhase2(serializedAppState, trace); } return trace; } function decideTabs(trace: TraceImpl) { // Show the list of default tabs, but don't make them active! for (const tabUri of trace.tabs.defaultTabs) { trace.tabs.showTab(tabUri); } } async function includeSummaryTables(trace: TraceImpl) { const engine = trace.engine; updateStatus(trace, 'Creating slice summaries'); await engine.query(`include perfetto module viz.summary.slices;`); updateStatus(trace, 'Creating counter summaries'); await engine.query(`include perfetto module viz.summary.counters;`); updateStatus(trace, 'Creating thread summaries'); await engine.query(`include perfetto module viz.summary.threads;`); updateStatus(trace, 'Creating processes summaries'); await engine.query(`include perfetto module viz.summary.processes;`); } function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void { const showUntilDismissed = 0; traceOrApp.omnibox.showStatusMessage(msg, showUntilDismissed); } async function computeFtraceBounds(engine: Engine): Promise { const result = await engine.query(` SELECT min(ts) as start, max(ts) as end FROM ftrace_event; `); const {start, end} = result.firstRow({start: LONG_NULL, end: LONG_NULL}); if (start !== null && end !== null) { return new TimeSpan(Time.fromRaw(start), Time.fromRaw(end)); } return null; } async function computeTraceReliableRangeStart(engine: Engine): Promise