blob: 3776f957305de1dc859ea1b5abb205ededdff8ed [file] [log] [blame]
// 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 {
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<void> {
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<TraceImpl> {
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<EngineBase> {
// 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.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<TraceImpl> {
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',
});
}
}
for (const callback of trace.getEventListeners('traceready')) {
await callback();
}
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;`);
updateStatus(trace, 'Creating track summaries');
await engine.query(`include perfetto module viz.summary.tracks;`);
}
function updateStatus(traceOrApp: TraceImpl | AppImpl, msg: string): void {
const showUntilDismissed = 0;
traceOrApp.omnibox.showStatusMessage(msg, showUntilDismissed);
}
async function computeFtraceBounds(engine: Engine): Promise<TimeSpan | null> {
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<time> {
const result =
await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql');
SELECT start FROM chrome_reliable_range`);
const bounds = result.firstRow({start: LONG});
return Time.fromRaw(bounds.start);
}
async function computeVisibleTime(
traceStart: time,
traceEnd: time,
isJsonTrace: boolean,
engine: Engine,
): Promise<TimeSpan> {
// initialise visible time to the trace time bounds
let visibleStart = traceStart;
let visibleEnd = traceEnd;
// compare start and end with metadata computed by the trace processor
const mdTime = await getTracingMetadataTimeBounds(engine);
// make sure the bounds hold
if (Time.max(visibleStart, mdTime.start) < Time.min(visibleEnd, mdTime.end)) {
visibleStart = Time.max(visibleStart, mdTime.start);
visibleEnd = Time.min(visibleEnd, mdTime.end);
}
// Trace Processor doesn't support the reliable range feature for JSON
// traces.
if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) {
const reliableRangeStart = await computeTraceReliableRangeStart(engine);
visibleStart = Time.max(visibleStart, reliableRangeStart);
}
// Move start of visible window to the first ftrace event
const ftraceBounds = await computeFtraceBounds(engine);
if (ftraceBounds !== null) {
// Avoid moving start of visible window past its end!
visibleStart = Time.min(ftraceBounds.start, visibleEnd);
}
return new TimeSpan(visibleStart, visibleEnd);
}
async function getTraceInfo(
engine: Engine,
traceSource: TraceSource,
): Promise<TraceInfoImpl> {
const traceTime = await getTraceTimeBounds(engine);
// Find the first REALTIME or REALTIME_COARSE clock snapshot.
// Prioritize REALTIME over REALTIME_COARSE.
const query = `select
ts,
clock_value as clockValue,
clock_name as clockName
from clock_snapshot
where
snapshot_id = 0 AND
clock_name in ('REALTIME', 'REALTIME_COARSE')
`;
const result = await engine.query(query);
const it = result.iter({
ts: LONG,
clockValue: LONG,
clockName: STR,
});
let snapshot = {
clockName: '',
ts: Time.ZERO,
clockValue: Time.ZERO,
};
// Find the most suitable snapshot
for (let row = 0; it.valid(); it.next(), row++) {
if (it.clockName === 'REALTIME') {
snapshot = {
clockName: it.clockName,
ts: Time.fromRaw(it.ts),
clockValue: Time.fromRaw(it.clockValue),
};
break;
} else if (it.clockName === 'REALTIME_COARSE') {
if (snapshot.clockName !== 'REALTIME') {
snapshot = {
clockName: it.clockName,
ts: Time.fromRaw(it.ts),
clockValue: Time.fromRaw(it.clockValue),
};
}
}
}
// The max() is so the query returns NULL if the tz info doesn't exist.
const queryTz = `select max(int_value) as tzOffMin from metadata
where name = 'timezone_off_mins'`;
const resTz = await assertExists(engine).query(queryTz);
const tzOffMin = resTz.firstRow({tzOffMin: NUM_NULL}).tzOffMin ?? 0;
// This is the offset between the unix epoch and ts in the ts domain.
// I.e. the value of ts at the time of the unix epoch - usually some large
// negative value.
const realtimeOffset = Time.sub(snapshot.ts, snapshot.clockValue);
// Find the previous closest midnight from the trace start time.
const utcOffset = Time.getLatestMidnight(traceTime.start, realtimeOffset);
const traceTzOffset = Time.getLatestMidnight(
traceTime.start,
Time.sub(realtimeOffset, Time.fromSeconds(tzOffMin * 60)),
);
let traceTitle = '';
let traceUrl = '';
switch (traceSource.type) {
case 'FILE':
// Split on both \ and / (because C:\Windows\paths\are\like\this).
traceTitle = traceSource.file.name.split(/[/\\]/).pop()!;
const fileSizeMB = Math.ceil(traceSource.file.size / 1e6);
traceTitle += ` (${fileSizeMB} MB)`;
break;
case 'URL':
traceUrl = traceSource.url;
traceTitle = traceUrl.split('/').pop()!;
break;
case 'ARRAY_BUFFER':
traceTitle = traceSource.title;
traceUrl = traceSource.url ?? '';
const arrayBufferSizeMB = Math.ceil(traceSource.buffer.byteLength / 1e6);
traceTitle += ` (${arrayBufferSizeMB} MB)`;
break;
case 'HTTP_RPC':
traceTitle = `RPC @ ${HttpRpcEngine.hostAndPort}`;
break;
default:
break;
}
const traceType = await getTraceType(engine);
const hasFtrace =
(await engine.query(`select * from ftrace_event limit 1`)).numRows() > 0;
const uuidRes = await engine.query(`select str_value as uuid from metadata
where name = 'trace_uuid'`);
// trace_uuid can be missing from the TP tables if the trace is empty or in
// other similar edge cases.
const uuid = uuidRes.numRows() > 0 ? uuidRes.firstRow({uuid: STR}).uuid : '';
const cached = await cacheTrace(traceSource, uuid);
const downloadable =
(traceSource.type === 'ARRAY_BUFFER' && !traceSource.localOnly) ||
traceSource.type === 'FILE' ||
traceSource.type === 'URL';
return {
...traceTime,
traceTitle,
traceUrl,
realtimeOffset,
utcOffset,
traceTzOffset,
cpus: await getCpus(engine),
importErrors: await getTraceErrors(engine),
source: traceSource,
traceType,
hasFtrace,
uuid,
cached,
downloadable,
};
}
async function getTraceType(engine: Engine) {
const result = await engine.query(
`select str_value from metadata where name = 'trace_type'`,
);
if (result.numRows() === 0) return undefined;
return result.firstRow({str_value: STR}).str_value;
}
async function getTraceTimeBounds(engine: Engine): Promise<TimeSpan> {
const result = await engine.query(
`select start_ts as startTs, end_ts as endTs from trace_bounds`,
);
const bounds = result.firstRow({
startTs: LONG,
endTs: LONG,
});
return new TimeSpan(Time.fromRaw(bounds.startTs), Time.fromRaw(bounds.endTs));
}
// TODO(hjd): When streaming must invalidate this somehow.
async function getCpus(engine: Engine): Promise<number[]> {
const cpus = [];
const queryRes = await engine.query(
'select distinct(cpu) as cpu from sched order by cpu;',
);
for (const it = queryRes.iter({cpu: NUM}); it.valid(); it.next()) {
cpus.push(it.cpu);
}
return cpus;
}
async function getTraceErrors(engine: Engine): Promise<number> {
const sql = `SELECT sum(value) as errs FROM stats WHERE severity != 'info'`;
const result = await engine.query(sql);
return result.firstRow({errs: NUM}).errs;
}
async function getTracingMetadataTimeBounds(engine: Engine): Promise<TimeSpan> {
const queryRes = await engine.query(`select
name,
int_value as intValue
from metadata
where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
or name = 'all_data_source_started_ns'`);
let startBound = Time.MIN;
let endBound = Time.MAX;
const it = queryRes.iter({name: STR, intValue: LONG_NULL});
for (; it.valid(); it.next()) {
const columnName = it.name;
const timestamp = it.intValue;
if (timestamp === null) continue;
if (columnName === 'tracing_disabled_ns') {
endBound = Time.min(endBound, Time.fromRaw(timestamp));
} else {
startBound = Time.max(startBound, Time.fromRaw(timestamp));
}
}
return new TimeSpan(startBound, endBound);
}