• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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