• 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 {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