• 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 {
18  Actions,
19  DeferredAction,
20} from '../common/actions';
21import {cacheTrace} from '../common/cache_manager';
22import {Engine} from '../common/engine';
23import {featureFlags, Flag, PERF_SAMPLE_FLAG} from '../common/feature_flags';
24import {
25  HighPrecisionTime,
26  HighPrecisionTimeSpan,
27} from '../common/high_precision_time';
28import {HttpRpcEngine} from '../common/http_rpc_engine';
29import {
30  getEnabledMetatracingCategories,
31  isMetatracingEnabled,
32} from '../common/metatracing';
33import {
34  LONG,
35  NUM,
36  NUM_NULL,
37  QueryError,
38  STR,
39  STR_NULL,
40} from '../common/query_result';
41import {onSelectionChanged} from '../common/selection_observer';
42import {defaultTraceTime, EngineMode, ProfileType} from '../common/state';
43import {Span} from '../common/time';
44import {
45  TPTime,
46  TPTimeSpan,
47} from '../common/time';
48import {resetEngineWorker, WasmEngineProxy} from '../common/wasm_engine_proxy';
49import {BottomTabList} from '../frontend/bottom_tab';
50import {
51  FtraceStat,
52  globals,
53  QuantizedLoad,
54  ThreadDesc,
55} from '../frontend/globals';
56import {showModal} from '../frontend/modal';
57import {
58  clearOverviewData,
59  publishFtraceCounters,
60  publishMetricError,
61  publishOverviewData,
62  publishThreads,
63} from '../frontend/publish';
64import {Router} from '../frontend/router';
65
66import {
67  CounterAggregationController,
68} from './aggregation/counter_aggregation_controller';
69import {
70  CpuAggregationController,
71} from './aggregation/cpu_aggregation_controller';
72import {
73  CpuByProcessAggregationController,
74} from './aggregation/cpu_by_process_aggregation_controller';
75import {
76  FrameAggregationController,
77} from './aggregation/frame_aggregation_controller';
78import {
79  SliceAggregationController,
80} from './aggregation/slice_aggregation_controller';
81import {
82  ThreadAggregationController,
83} from './aggregation/thread_aggregation_controller';
84import {Child, Children, Controller} from './controller';
85import {
86  CpuProfileController,
87  CpuProfileControllerArgs,
88} from './cpu_profile_controller';
89import {
90  FlamegraphController,
91  FlamegraphControllerArgs,
92  profileType,
93} from './flamegraph_controller';
94import {
95  FlowEventsController,
96  FlowEventsControllerArgs,
97} from './flow_events_controller';
98import {FtraceController} from './ftrace_controller';
99import {LoadingManager} from './loading_manager';
100import {LogsController} from './logs_controller';
101import {MetricsController} from './metrics_controller';
102import {
103  PIVOT_TABLE_REDUX_FLAG,
104  PivotTableController,
105} from './pivot_table_controller';
106import {SearchController} from './search_controller';
107import {
108  SelectionController,
109  SelectionControllerArgs,
110} from './selection_controller';
111import {
112  TraceErrorController,
113} from './trace_error_controller';
114import {
115  TraceBufferStream,
116  TraceFileStream,
117  TraceHttpStream,
118  TraceStream,
119} from './trace_stream';
120import {TrackControllerArgs, trackControllerRegistry} from './track_controller';
121import {decideTracks} from './track_decider';
122import {VisualisedArgController} from './visualised_args_controller';
123
124type States = 'init' | 'loading_trace' | 'ready';
125
126const METRICS = [
127  'android_startup',
128  'android_ion',
129  'android_lmk',
130  'android_dma_heap',
131  'android_surfaceflinger',
132  'android_batt',
133  'android_other_traces',
134  'chrome_dropped_frames',
135  'chrome_long_latency',
136  'trace_metadata',
137  'android_trusty_workqueues',
138];
139const FLAGGED_METRICS: Array<[Flag, string]> = METRICS.map((m) => {
140  const id = `forceMetric${m}`;
141  let name = m.split('_').join(' ');
142  name = name[0].toUpperCase() + name.slice(1);
143  name = 'Metric: ' + name;
144  const flag = featureFlags.register({
145    id,
146    name,
147    description: `Overrides running the '${m}' metric at import time.`,
148    defaultValue: true,
149  });
150  return [flag, m];
151});
152
153const ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG = featureFlags.register({
154  id: 'enableChromeReliableRangeZoom',
155  name: 'Enable Chrome reliable range zoom',
156  description: 'Automatically zoom into the reliable range for Chrome traces',
157  defaultValue: false,
158});
159
160const ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG = featureFlags.register({
161  id: 'enableChromeReliableRangeAnnotation',
162  name: 'Enable Chrome reliable range annotation',
163  description: 'Automatically adds an annotation for the reliable range start',
164  defaultValue: false,
165});
166
167// The following flags control TraceProcessor Config.
168const CROP_TRACK_EVENTS_FLAG = featureFlags.register({
169  id: 'cropTrackEvents',
170  name: 'Crop track events',
171  description: 'Ignores track events outside of the range of interest',
172  defaultValue: false,
173});
174const INGEST_FTRACE_IN_RAW_TABLE_FLAG = featureFlags.register({
175  id: 'ingestFtraceInRawTable',
176  name: 'Ingest ftrace in raw table',
177  description: 'Enables ingestion of typed ftrace events into the raw table',
178  defaultValue: true,
179});
180const ANALYZE_TRACE_PROTO_CONTENT_FLAG = featureFlags.register({
181  id: 'analyzeTraceProtoContent',
182  name: 'Analyze trace proto content',
183  description: 'Enables trace proto content analysis',
184  defaultValue: false,
185});
186
187// A local storage key where the indication that JSON warning has been shown is
188// stored.
189const SHOWN_JSON_WARNING_KEY = 'shownJsonWarning';
190
191function showJsonWarning() {
192  showModal({
193    title: 'Warning',
194    content:
195      m('div',
196        m('span',
197          'Perfetto UI features are limited for JSON traces. ',
198          'We recommend recording ',
199          m('a',
200            {href: 'https://perfetto.dev/docs/quickstart/chrome-tracing'},
201            'proto-format traces'),
202          ' from Chrome.'),
203        m('br')),
204    buttons: [],
205  });
206}
207
208// TraceController handles handshakes with the frontend for everything that
209// concerns a single trace. It owns the WASM trace processor engine, handles
210// tracks data and SQL queries. There is one TraceController instance for each
211// trace opened in the UI (for now only one trace is supported).
212export class TraceController extends Controller<States> {
213  private readonly engineId: string;
214  private engine?: Engine;
215
216  constructor(engineId: string) {
217    super('init');
218    this.engineId = engineId;
219  }
220
221  run() {
222    const engineCfg = assertExists(globals.state.engine);
223    switch (this.state) {
224      case 'init':
225        this.loadTrace()
226            .then((mode) => {
227              globals.dispatch(Actions.setEngineReady({
228                engineId: this.engineId,
229                ready: true,
230                mode,
231              }));
232            })
233            .catch((err) => {
234              this.updateStatus(`${err}`);
235              throw err;
236            });
237        this.updateStatus('Opening trace');
238        this.setState('loading_trace');
239        break;
240
241      case 'loading_trace':
242        // Stay in this state until loadTrace() returns and marks the engine as
243        // ready.
244        if (this.engine === undefined || !engineCfg.ready) return;
245        this.setState('ready');
246        break;
247
248      case 'ready':
249        // At this point we are ready to serve queries and handle tracks.
250        const engine = assertExists(this.engine);
251        const childControllers: Children = [];
252
253        // Create a TrackController for each track.
254        for (const trackId of Object.keys(globals.state.tracks)) {
255          const trackCfg = globals.state.tracks[trackId];
256          if (trackCfg.engineId !== this.engineId) continue;
257          if (!trackControllerRegistry.has(trackCfg.kind)) continue;
258          const trackCtlFactory = trackControllerRegistry.get(trackCfg.kind);
259          const trackArgs: TrackControllerArgs = {trackId, engine};
260          childControllers.push(Child(trackId, trackCtlFactory, trackArgs));
261        }
262
263        for (const argName of globals.state.visualisedArgs) {
264          childControllers.push(
265            Child(argName, VisualisedArgController, {argName, engine}));
266        }
267
268        const selectionArgs: SelectionControllerArgs = {engine};
269        childControllers.push(
270          Child('selection', SelectionController, selectionArgs));
271
272        const flowEventsArgs: FlowEventsControllerArgs = {engine};
273        childControllers.push(
274          Child('flowEvents', FlowEventsController, flowEventsArgs));
275
276        const cpuProfileArgs: CpuProfileControllerArgs = {engine};
277        childControllers.push(
278          Child('cpuProfile', CpuProfileController, cpuProfileArgs));
279
280        const flamegraphArgs: FlamegraphControllerArgs = {engine};
281        childControllers.push(
282          Child('flamegraph', FlamegraphController, flamegraphArgs));
283        childControllers.push(Child(
284          'cpu_aggregation',
285          CpuAggregationController,
286          {engine, kind: 'cpu_aggregation'}));
287        childControllers.push(Child(
288          'thread_aggregation',
289          ThreadAggregationController,
290          {engine, kind: 'thread_state_aggregation'}));
291        childControllers.push(Child(
292          'cpu_process_aggregation',
293          CpuByProcessAggregationController,
294          {engine, kind: 'cpu_by_process_aggregation'}));
295        if (!PIVOT_TABLE_REDUX_FLAG.get()) {
296          // Pivot table is supposed to handle the use cases the slice
297          // aggregation panel is used right now. When a flag to use pivot
298          // tables is enabled, do not add slice aggregation controller.
299          childControllers.push(Child(
300            'slice_aggregation',
301            SliceAggregationController,
302            {engine, kind: 'slice_aggregation'}));
303        }
304        childControllers.push(Child(
305          'counter_aggregation',
306          CounterAggregationController,
307          {engine, kind: 'counter_aggregation'}));
308        childControllers.push(Child(
309          'frame_aggregation',
310          FrameAggregationController,
311          {engine, kind: 'frame_aggregation'}));
312        childControllers.push(Child('search', SearchController, {
313          engine,
314          app: globals,
315        }));
316        childControllers.push(
317            Child('pivot_table', PivotTableController, {engine}));
318
319        childControllers.push(Child('logs', LogsController, {
320          engine,
321          app: globals,
322        }));
323
324        childControllers.push(
325            Child('ftrace', FtraceController, {engine, app: globals}));
326
327        childControllers.push(
328          Child('traceError', TraceErrorController, {engine}));
329        childControllers.push(Child('metrics', MetricsController, {engine}));
330
331        return childControllers;
332
333      default:
334        throw new Error(`unknown state ${this.state}`);
335    }
336    return;
337  }
338
339  onDestroy() {
340    globals.engines.delete(this.engineId);
341  }
342
343  private async loadTrace(): Promise<EngineMode> {
344    this.updateStatus('Creating trace processor');
345    // Check if there is any instance of the trace_processor_shell running in
346    // HTTP RPC mode (i.e. trace_processor_shell -D).
347    let engineMode: EngineMode;
348    let useRpc = false;
349    if (globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
350      useRpc = (await HttpRpcEngine.checkConnection()).connected;
351    }
352    let engine;
353    if (useRpc) {
354      console.log('Opening trace using native accelerator over HTTP+RPC');
355      engineMode = 'HTTP_RPC';
356      engine = new HttpRpcEngine(this.engineId, LoadingManager.getInstance);
357      engine.errorHandler = (err) => {
358        globals.dispatch(
359            Actions.setEngineFailed({mode: 'HTTP_RPC', failure: `${err}`}));
360        throw err;
361      };
362    } else {
363      console.log('Opening trace using built-in WASM engine');
364      engineMode = 'WASM';
365      const enginePort = resetEngineWorker();
366      engine = new WasmEngineProxy(
367        this.engineId, enginePort, LoadingManager.getInstance);
368      engine.resetTraceProcessor({
369        cropTrackEvents: CROP_TRACK_EVENTS_FLAG.get(),
370        ingestFtraceInRawTable: INGEST_FTRACE_IN_RAW_TABLE_FLAG.get(),
371        analyzeTraceProtoContent: ANALYZE_TRACE_PROTO_CONTENT_FLAG.get(),
372      });
373    }
374    this.engine = engine;
375
376    if (isMetatracingEnabled()) {
377      this.engine.enableMetatrace(
378        assertExists(getEnabledMetatracingCategories()));
379    }
380    globals.bottomTabList = new BottomTabList(engine.getProxy('BottomTabList'));
381
382    globals.engines.set(this.engineId, engine);
383    globals.dispatch(Actions.setEngineReady({
384      engineId: this.engineId,
385      ready: false,
386      mode: engineMode,
387    }));
388    const engineCfg = assertExists(globals.state.engine);
389    assertTrue(engineCfg.id === this.engineId);
390    let traceStream: TraceStream | undefined;
391    if (engineCfg.source.type === 'FILE') {
392      traceStream = new TraceFileStream(engineCfg.source.file);
393    } else if (engineCfg.source.type === 'ARRAY_BUFFER') {
394      traceStream = new TraceBufferStream(engineCfg.source.buffer);
395    } else if (engineCfg.source.type === 'URL') {
396      traceStream = new TraceHttpStream(engineCfg.source.url);
397    } else if (engineCfg.source.type === 'HTTP_RPC') {
398      traceStream = undefined;
399    } else {
400      throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`);
401    }
402
403    // |traceStream| can be undefined in the case when we are using the external
404    // HTTP+RPC endpoint and the trace processor instance has already loaded
405    // a trace (because it was passed as a cmdline argument to
406    // trace_processor_shell). In this case we don't want the UI to load any
407    // file/stream and we just want to jump to the loading phase.
408    if (traceStream !== undefined) {
409      const tStart = performance.now();
410      for (; ;) {
411        const res = await traceStream.readChunk();
412        await this.engine.parse(res.data);
413        const elapsed = (performance.now() - tStart) / 1000;
414        let status = 'Loading trace ';
415        if (res.bytesTotal > 0) {
416          const progress = Math.round(res.bytesRead / res.bytesTotal * 100);
417          status += `${progress}%`;
418        } else {
419          status += `${Math.round(res.bytesRead / 1e6)} MB`;
420        }
421        status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`;
422        this.updateStatus(status);
423        if (res.eof) break;
424      }
425      await this.engine.notifyEof();
426    } else {
427      assertTrue(this.engine instanceof HttpRpcEngine);
428      await this.engine.restoreInitialTables();
429    }
430
431    // traceUuid will be '' if the trace is not cacheable (URL or RPC).
432    const traceUuid = await this.cacheCurrentTrace();
433
434    const traceTime = await this.engine.getTraceTimeBounds();
435    const start = traceTime.start;
436    const end = traceTime.end;
437    const traceTimeState = {
438      start,
439      end,
440    };
441
442    const shownJsonWarning =
443      window.localStorage.getItem(SHOWN_JSON_WARNING_KEY) !== null;
444
445    // Show warning if the trace is in JSON format.
446    const query = `select str_value from metadata where name = 'trace_type'`;
447    const result = await assertExists(this.engine).query(query);
448    const traceType = result.firstRow({str_value: STR}).str_value;
449    const isJsonTrace = traceType == 'json';
450    if (!shownJsonWarning) {
451      // When in embedded mode, the host app will control which trace format
452      // it passes to Perfetto, so we don't need to show this warning.
453      if (isJsonTrace && !globals.embeddedMode) {
454        showJsonWarning();
455        // Save that the warning has been shown. Value is irrelevant since only
456        // the presence of key is going to be checked.
457        window.localStorage.setItem(SHOWN_JSON_WARNING_KEY, 'true');
458      }
459    }
460
461    const emptyOmniboxState = {
462      omnibox: '',
463      mode: globals.state.omniboxState.mode || 'SEARCH',
464    };
465
466    const actions: DeferredAction[] = [
467      Actions.setOmnibox(emptyOmniboxState),
468      Actions.setTraceUuid({traceUuid}),
469      Actions.setTraceTime(traceTimeState),
470    ];
471
472    const visibleTimeSpan = await computeVisibleTime(
473        traceTime.start, traceTime.end, isJsonTrace, this.engine);
474    // We don't know the resolution at this point. However this will be
475    // replaced in 50ms so a guess is fine.
476    const resolution = visibleTimeSpan.duration.divide(1000).toTPTime();
477    actions.push(Actions.setVisibleTraceTime({
478      start: visibleTimeSpan.start.toTPTime(),
479      end: visibleTimeSpan.end.toTPTime(),
480      lastUpdate: Date.now() / 1000,
481      resolution: BigintMath.max(resolution, 1n),
482    }));
483
484    globals.dispatchMultiple(actions);
485    Router.navigate(`#!/viewer?local_cache_key=${traceUuid}`);
486
487    // Make sure the helper views are available before we start adding tracks.
488    await this.initialiseHelperViews();
489
490    {
491      // When we reload from a permalink don't create extra tracks:
492      const {pinnedTracks, tracks} = globals.state;
493      if (!pinnedTracks.length && !Object.keys(tracks).length) {
494        await this.listTracks();
495      }
496    }
497
498    await this.listThreads();
499    await this.loadTimelineOverview(traceTime);
500
501    {
502      // Pull out the counts ftrace events by name
503      const query = `select
504            name,
505            count(name) as cnt
506          from ftrace_event
507          group by name
508          order by cnt desc`;
509      const result = await assertExists(this.engine).query(query);
510      const counters: FtraceStat[] = [];
511      const it = result.iter({name: STR, cnt: NUM});
512      for (let row = 0; it.valid(); it.next(), row++) {
513        counters.push({name: it.name, count: it.cnt});
514      }
515      publishFtraceCounters(counters);
516    }
517
518    globals.dispatch(Actions.sortThreadTracks({}));
519    globals.dispatch(Actions.maybeExpandOnlyTrackGroup({}));
520
521    await this.selectFirstHeapProfile();
522    if (PERF_SAMPLE_FLAG.get()) {
523      await this.selectPerfSample();
524    }
525
526    // If the trace was shared via a permalink, it might already have a
527    // selection. Emit onSelectionChanged to ensure that the components (like
528    // current selection details) react to it.
529    if (globals.state.currentSelection !== null) {
530      onSelectionChanged(globals.state.currentSelection, undefined);
531    }
532
533    // Trace Processor doesn't support the reliable range feature for JSON
534    // traces.
535    if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ANNOTATION_FLAG.get()) {
536      const reliableRangeStart = await computeTraceReliableRangeStart(engine);
537      if (reliableRangeStart > 0) {
538        globals.dispatch(Actions.addAutomaticNote({
539          timestamp: reliableRangeStart,
540          color: '#ff0000',
541          text: 'Reliable Range Start',
542        }));
543      }
544    }
545
546    return engineMode;
547  }
548
549  private async selectPerfSample() {
550    const query = `select upid
551        from perf_sample
552        join thread using (utid)
553        where callsite_id is not null
554        order by ts desc limit 1`;
555    const profile = await assertExists(this.engine).query(query);
556    if (profile.numRows() !== 1) return;
557    const row = profile.firstRow({upid: NUM});
558    const upid = row.upid;
559    const leftTs = globals.state.traceTime.start;
560    const rightTs = globals.state.traceTime.end;
561    globals.dispatch(Actions.selectPerfSamples(
562        {id: 0, upid, leftTs, rightTs, type: ProfileType.PERF_SAMPLE}));
563  }
564
565  private async selectFirstHeapProfile() {
566    const query = `select * from (
567      select
568        min(ts) AS ts,
569        'heap_profile:' || group_concat(distinct heap_name) AS type,
570        upid
571      from heap_profile_allocation
572      group by upid
573      union
574      select distinct graph_sample_ts as ts, 'graph' as type, upid
575      from heap_graph_object)
576      order by ts limit 1`;
577    const profile = await assertExists(this.engine).query(query);
578    if (profile.numRows() !== 1) return;
579    const row = profile.firstRow({ts: LONG, type: STR, upid: NUM});
580    const ts = row.ts;
581    const type = profileType(row.type);
582    const upid = row.upid;
583    globals.dispatch(Actions.selectHeapProfile({id: 0, upid, ts, type}));
584  }
585
586  private async listTracks() {
587    this.updateStatus('Loading tracks');
588    const engine = assertExists<Engine>(this.engine);
589    const actions = await decideTracks(this.engineId, engine);
590    globals.dispatchMultiple(actions);
591  }
592
593  private async listThreads() {
594    this.updateStatus('Reading thread list');
595    const query = `select
596        utid,
597        tid,
598        pid,
599        ifnull(thread.name, '') as threadName,
600        ifnull(
601          case when length(process.name) > 0 then process.name else null end,
602          thread.name) as procName,
603        process.cmdline as cmdline
604        from (select * from thread order by upid) as thread
605        left join (select * from process order by upid) as process
606        using(upid)`;
607    const result = await assertExists(this.engine).query(query);
608    const threads: ThreadDesc[] = [];
609    const it = result.iter({
610      utid: NUM,
611      tid: NUM,
612      pid: NUM_NULL,
613      threadName: STR,
614      procName: STR_NULL,
615      cmdline: STR_NULL,
616    });
617    for (; it.valid(); it.next()) {
618      const utid = it.utid;
619      const tid = it.tid;
620      const pid = it.pid === null ? undefined : it.pid;
621      const threadName = it.threadName;
622      const procName = it.procName === null ? undefined : it.procName;
623      const cmdline = it.cmdline === null ? undefined : it.cmdline;
624      threads.push({utid, tid, threadName, pid, procName, cmdline});
625    }
626    publishThreads(threads);
627  }
628
629  private async loadTimelineOverview(trace: Span<TPTime>) {
630    clearOverviewData();
631
632    const engine = assertExists<Engine>(this.engine);
633    const stepSize = BigintMath.max(1n, trace.duration / 100n);
634    let hasSchedOverview = false;
635    for (let start = trace.start; start < trace.end; start += stepSize) {
636      const progress = start - trace.start;
637      const ratio = Number(progress) / Number(trace.duration);
638      this.updateStatus(
639          'Loading overview ' +
640          `${Math.round(ratio * 100)}%`);
641      const end = start + stepSize;
642
643      // Sched overview.
644      const schedResult = await engine.query(
645          `select cast(sum(dur) as float)/${
646              stepSize} as load, cpu from sched ` +
647          `where ts >= ${start} and ts < ${end} and utid != 0 ` +
648          'group by cpu order by cpu');
649      const schedData: {[key: string]: QuantizedLoad} = {};
650      const it = schedResult.iter({load: NUM, cpu: NUM});
651      for (; it.valid(); it.next()) {
652        const load = it.load;
653        const cpu = it.cpu;
654        schedData[cpu] = {start, end, load};
655        hasSchedOverview = true;
656      }
657      publishOverviewData(schedData);
658    }
659
660    if (hasSchedOverview) {
661      return;
662    }
663
664    // Slices overview.
665    const sliceResult = await engine.query(`select
666           bucket,
667           upid,
668           ifnull(sum(utid_sum) / cast(${stepSize} as float), 0) as load
669         from thread
670         inner join (
671           select
672             ifnull(cast((ts - ${trace.start})/${
673        stepSize} as int), 0) as bucket,
674             sum(dur) as utid_sum,
675             utid
676           from slice
677           inner join thread_track on slice.track_id = thread_track.id
678           group by bucket, utid
679         ) using(utid)
680         where upid is not null
681         group by bucket, upid`);
682
683    const slicesData: {[key: string]: QuantizedLoad[]} = {};
684    const it = sliceResult.iter({bucket: LONG, upid: NUM, load: NUM});
685    for (; it.valid(); it.next()) {
686      const bucket = it.bucket;
687      const upid = it.upid;
688      const load = it.load;
689
690      const start = trace.start + stepSize * bucket;
691      const end = start + stepSize;
692
693      const upidStr = upid.toString();
694      let loadArray = slicesData[upidStr];
695      if (loadArray === undefined) {
696        loadArray = slicesData[upidStr] = [];
697      }
698      loadArray.push({start, end, load});
699    }
700    publishOverviewData(slicesData);
701  }
702
703  private async cacheCurrentTrace(): Promise<string> {
704    const engine = assertExists(this.engine);
705    const result = await engine.query(`select str_value as uuid from metadata
706                  where name = 'trace_uuid'`);
707    if (result.numRows() === 0) {
708      // One of the cases covered is an empty trace.
709      return '';
710    }
711    const traceUuid = result.firstRow({uuid: STR}).uuid;
712    const engineConfig = assertExists(globals.state.engine);
713    assertTrue(engineConfig.id === this.engineId);
714    if (!(await cacheTrace(engineConfig.source, traceUuid))) {
715      // If the trace is not cacheable (cacheable means it has been opened from
716      // URL or RPC) only append '?local_cache_key' to the URL, without the
717      // local_cache_key value. Doing otherwise would cause an error if the tab
718      // is discarded or the user hits the reload button because the trace is
719      // not in the cache.
720      return '';
721    }
722    return traceUuid;
723  }
724
725  async initialiseHelperViews() {
726    const engine = assertExists<Engine>(this.engine);
727
728    this.updateStatus('Creating annotation counter track table');
729    // Create the helper tables for all the annotations related data.
730    // NULL in min/max means "figure it out per track in the usual way".
731    await engine.query(`
732      CREATE TABLE annotation_counter_track(
733        id INTEGER PRIMARY KEY,
734        name STRING,
735        __metric_name STRING,
736        upid INTEGER,
737        min_value DOUBLE,
738        max_value DOUBLE
739      );
740    `);
741    this.updateStatus('Creating annotation slice track table');
742    await engine.query(`
743      CREATE TABLE annotation_slice_track(
744        id INTEGER PRIMARY KEY,
745        name STRING,
746        __metric_name STRING,
747        upid INTEGER,
748        group_name STRING
749      );
750    `);
751
752    this.updateStatus('Creating annotation counter table');
753    await engine.query(`
754      CREATE TABLE annotation_counter(
755        id BIGINT,
756        track_id INT,
757        ts BIGINT,
758        value DOUBLE,
759        PRIMARY KEY (track_id, ts)
760      ) WITHOUT ROWID;
761    `);
762    this.updateStatus('Creating annotation slice table');
763    await engine.query(`
764      CREATE TABLE annotation_slice(
765        id INTEGER PRIMARY KEY,
766        track_id INT,
767        ts BIGINT,
768        dur BIGINT,
769        thread_dur BIGINT,
770        depth INT,
771        cat STRING,
772        name STRING,
773        UNIQUE(track_id, ts)
774      );
775    `);
776
777
778    const availableMetrics = [];
779    const metricsResult = await engine.query('select name from trace_metrics');
780    for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
781      availableMetrics.push(it.name);
782    }
783    globals.dispatch(Actions.setAvailableMetrics({availableMetrics}));
784
785    const availableMetricsSet = new Set<string>(availableMetrics);
786    for (const [flag, metric] of FLAGGED_METRICS) {
787      if (!flag.get() || !availableMetricsSet.has(metric)) {
788        continue;
789      }
790
791      this.updateStatus(`Computing ${metric} metric`);
792      try {
793        // We don't care about the actual result of metric here as we are just
794        // interested in the annotation tracks.
795        await engine.computeMetric([metric]);
796      } catch (e) {
797        if (e instanceof QueryError) {
798          publishMetricError('MetricError: ' + e.message);
799          continue;
800        } else {
801          throw e;
802        }
803      }
804
805      this.updateStatus(`Inserting data for ${metric} metric`);
806      try {
807        const result = await engine.query(`pragma table_info(${metric}_event)`);
808        let hasSliceName = false;
809        let hasDur = false;
810        let hasUpid = false;
811        let hasValue = false;
812        let hasGroupName = false;
813        const it = result.iter({name: STR});
814        for (; it.valid(); it.next()) {
815          const name = it.name;
816          hasSliceName = hasSliceName || name === 'slice_name';
817          hasDur = hasDur || name === 'dur';
818          hasUpid = hasUpid || name === 'upid';
819          hasValue = hasValue || name === 'value';
820          hasGroupName = hasGroupName || name === 'group_name';
821        }
822
823        const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
824        const upidColumnWhere = hasUpid ? 'upid' : '0';
825        const groupNameColumn =
826          hasGroupName ? 'group_name' : 'NULL AS group_name';
827        if (hasSliceName && hasDur) {
828          await engine.query(`
829            INSERT INTO annotation_slice_track(
830              name, __metric_name, upid, group_name)
831            SELECT DISTINCT
832              track_name,
833              '${metric}' as metric_name,
834              ${upidColumnSelect},
835              ${groupNameColumn}
836            FROM ${metric}_event
837            WHERE track_type = 'slice'
838          `);
839          await engine.query(`
840            INSERT INTO annotation_slice(
841              track_id, ts, dur, thread_dur, depth, cat, name
842            )
843            SELECT
844              t.id AS track_id,
845              ts,
846              dur,
847              NULL as thread_dur,
848              0 AS depth,
849              a.track_name as cat,
850              slice_name AS name
851            FROM ${metric}_event a
852            JOIN annotation_slice_track t
853            ON a.track_name = t.name AND t.__metric_name = '${metric}'
854            ORDER BY t.id, ts
855          `);
856        }
857
858        if (hasValue) {
859          const minMax = await engine.query(`
860            SELECT
861              IFNULL(MIN(value), 0) as minValue,
862              IFNULL(MAX(value), 0) as maxValue
863            FROM ${metric}_event
864            WHERE ${upidColumnWhere} != 0`);
865          const row = minMax.firstRow({minValue: NUM, maxValue: NUM});
866          await engine.query(`
867            INSERT INTO annotation_counter_track(
868              name, __metric_name, min_value, max_value, upid)
869            SELECT DISTINCT
870              track_name,
871              '${metric}' as metric_name,
872              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.minValue} END,
873              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.maxValue} END,
874              ${upidColumnSelect}
875            FROM ${metric}_event
876            WHERE track_type = 'counter'
877          `);
878          await engine.query(`
879            INSERT INTO annotation_counter(id, track_id, ts, value)
880            SELECT
881              -1 as id,
882              t.id AS track_id,
883              ts,
884              value
885            FROM ${metric}_event a
886            JOIN annotation_counter_track t
887            ON a.track_name = t.name AND t.__metric_name = '${metric}'
888            ORDER BY t.id, ts
889          `);
890        }
891      } catch (e) {
892        if (e instanceof QueryError) {
893          publishMetricError('MetricError: ' + e.message);
894        } else {
895          throw e;
896        }
897      }
898    }
899  }
900
901  private updateStatus(msg: string): void {
902    globals.dispatch(Actions.updateStatus({
903      msg,
904      timestamp: Date.now() / 1000,
905    }));
906  }
907}
908
909async function computeTraceReliableRangeStart(engine: Engine): Promise<TPTime> {
910  const result =
911    await engine.query(`SELECT RUN_METRIC('chrome/chrome_reliable_range.sql');
912       SELECT start FROM chrome_reliable_range`);
913  const bounds = result.firstRow({start: LONG});
914  return bounds.start;
915}
916
917async function computeVisibleTime(
918    traceStart: TPTime, traceEnd: TPTime, isJsonTrace: boolean, engine: Engine):
919    Promise<Span<HighPrecisionTime>> {
920  // if we have non-default visible state, update the visible time to it
921  const previousVisibleState = globals.stateVisibleTime();
922  const defaultTraceSpan =
923      new TPTimeSpan(defaultTraceTime.start, defaultTraceTime.end);
924  if (!(previousVisibleState.start === defaultTraceSpan.start &&
925        previousVisibleState.end === defaultTraceSpan.end) &&
926      (previousVisibleState.start >= traceStart &&
927       previousVisibleState.end <= traceEnd)) {
928    return HighPrecisionTimeSpan.fromTpTime(
929        previousVisibleState.start, previousVisibleState.end);
930  }
931
932  // initialise visible time to the trace time bounds
933  let visibleStartSec = traceStart;
934  let visibleEndSec = traceEnd;
935
936  // compare start and end with metadata computed by the trace processor
937  const mdTime = await engine.getTracingMetadataTimeBounds();
938  // make sure the bounds hold
939  if (BigintMath.max(visibleStartSec, mdTime.start) <
940      BigintMath.min(visibleEndSec, mdTime.end)) {
941    visibleStartSec = BigintMath.max(visibleStartSec, mdTime.start);
942    visibleEndSec = BigintMath.min(visibleEndSec, mdTime.end);
943  }
944
945  // Trace Processor doesn't support the reliable range feature for JSON
946  // traces.
947  if (!isJsonTrace && ENABLE_CHROME_RELIABLE_RANGE_ZOOM_FLAG.get()) {
948    const reliableRangeStart = await computeTraceReliableRangeStart(engine);
949    visibleStartSec = BigintMath.max(visibleStartSec, reliableRangeStart);
950  }
951
952  return HighPrecisionTimeSpan.fromTpTime(visibleStartSec, visibleEndSec);
953}
954