• 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 {
17  Actions,
18  DeferredAction,
19} from '../common/actions';
20import { cacheTrace } from '../common/cache_manager';
21import { TRACE_MARGIN_TIME_S } from '../common/constants';
22import { Engine } from '../common/engine';
23import { featureFlags, Flag, PERF_SAMPLE_FLAG } from '../common/feature_flags';
24import { HttpRpcEngine } from '../common/http_rpc_engine';
25import { NUM, NUM_NULL, QueryError, STR, STR_NULL } from '../common/query_result';
26import { EngineMode } from '../common/state';
27import { TimeSpan, toNs, toNsCeil, toNsFloor } from '../common/time';
28import { resetEngineWorker, WasmEngineProxy } from '../common/wasm_engine_proxy';
29import {
30  globals as frontendGlobals,
31  QuantizedLoad,
32  ThreadDesc
33} from '../frontend/globals';
34import {
35  publishHasFtrace,
36  publishMetricError,
37  publishOverviewData,
38  publishThreads
39} from '../frontend/publish';
40import { Router } from '../frontend/router';
41
42import {
43  CounterAggregationController
44} from './aggregation/counter_aggregation_controller';
45import {
46  CpuAggregationController
47} from './aggregation/cpu_aggregation_controller';
48import {
49  CpuByProcessAggregationController
50} from './aggregation/cpu_by_process_aggregation_controller';
51import {
52  FrameAggregationController
53} from './aggregation/frame_aggregation_controller';
54import {
55  SliceAggregationController
56} from './aggregation/slice_aggregation_controller';
57import {
58  ThreadAggregationController
59} from './aggregation/thread_aggregation_controller';
60import { Child, Children, Controller } from './controller';
61import {
62  CpuProfileController,
63  CpuProfileControllerArgs
64} from './cpu_profile_controller';
65import {
66  FlamegraphController,
67  FlamegraphControllerArgs
68} from './flamegraph_controller';
69import {
70  FlowEventsController,
71  FlowEventsControllerArgs
72} from './flow_events_controller';
73import { globals } from './globals';
74import { LoadingManager } from './loading_manager';
75import { LogsController } from './logs_controller';
76import { MetricsController } from './metrics_controller';
77import {
78  PivotTableController,
79  PivotTableControllerArgs
80} from './pivot_table_controller';
81import {PivotTableReduxController} from './pivot_table_redux_controller';
82import {QueryController, QueryControllerArgs} from './query_controller';
83import {SearchController} from './search_controller';
84import {
85  SelectionController,
86  SelectionControllerArgs
87} from './selection_controller';
88import {
89  TraceErrorController,
90} from './trace_error_controller';
91import {
92  TraceBufferStream,
93  TraceFileStream,
94  TraceHttpStream,
95  TraceStream
96} from './trace_stream';
97import { TrackControllerArgs, trackControllerRegistry } from './track_controller';
98import { decideTracks } from './track_decider';
99
100type States = 'init' | 'loading_trace' | 'ready';
101
102const METRICS = [
103  'android_startup',
104  'android_ion',
105  'android_lmk',
106  'android_dma_heap',
107  'android_thread_time_in_state',
108  'android_surfaceflinger',
109  'android_batt',
110  'android_sysui_cuj',
111  'android_jank',
112  'android_camera',
113  'android_other_traces',
114  'chrome_dropped_frames',
115  'chrome_long_latency',
116  'trace_metadata',
117  'android_trusty_workqueues',
118];
119const FLAGGED_METRICS: Array<[Flag, string]> = METRICS.map(m => {
120  const id = `forceMetric${m}`;
121  let name = m.split('_').join(' ') + ' metric';
122  name = name[0].toUpperCase() + name.slice(1);
123  const flag = featureFlags.register({
124    id,
125    name,
126    description: `Overrides running the '${m}' metric at import time.`,
127    defaultValue: true,
128  });
129  return [flag, m];
130});
131
132// TraceController handles handshakes with the frontend for everything that
133// concerns a single trace. It owns the WASM trace processor engine, handles
134// tracks data and SQL queries. There is one TraceController instance for each
135// trace opened in the UI (for now only one trace is supported).
136export class TraceController extends Controller<States> {
137  private readonly engineId: string;
138  private engine?: Engine;
139
140  constructor(engineId: string) {
141    super('init');
142    this.engineId = engineId;
143  }
144
145  run() {
146    const engineCfg = assertExists(globals.state.engines[this.engineId]);
147    switch (this.state) {
148      case 'init':
149        this.loadTrace()
150          .then(mode => {
151            globals.dispatch(Actions.setEngineReady({
152              engineId: this.engineId,
153              ready: true,
154              mode,
155            }));
156          })
157          .catch(err => {
158            this.updateStatus(`${err}`);
159            throw err;
160          });
161        this.updateStatus('Opening trace');
162        this.setState('loading_trace');
163        break;
164
165      case 'loading_trace':
166        // Stay in this state until loadTrace() returns and marks the engine as
167        // ready.
168        if (this.engine === undefined || !engineCfg.ready) return;
169        this.setState('ready');
170        break;
171
172      case 'ready':
173        // At this point we are ready to serve queries and handle tracks.
174        const engine = assertExists(this.engine);
175        const childControllers: Children = [];
176
177        // Create a TrackController for each track.
178        for (const trackId of Object.keys(globals.state.tracks)) {
179          const trackCfg = globals.state.tracks[trackId];
180          if (trackCfg.engineId !== this.engineId) continue;
181          if (!trackControllerRegistry.has(trackCfg.kind)) continue;
182          const trackCtlFactory = trackControllerRegistry.get(trackCfg.kind);
183          const trackArgs: TrackControllerArgs = { trackId, engine };
184          childControllers.push(Child(trackId, trackCtlFactory, trackArgs));
185        }
186
187        // Create a QueryController for each query.
188        for (const queryId of Object.keys(globals.state.queries)) {
189          const queryArgs: QueryControllerArgs = { queryId, engine };
190          childControllers.push(Child(queryId, QueryController, queryArgs));
191        }
192
193        const selectionArgs: SelectionControllerArgs = { engine };
194        childControllers.push(
195          Child('selection', SelectionController, selectionArgs));
196
197        const flowEventsArgs: FlowEventsControllerArgs = { engine };
198        childControllers.push(
199          Child('flowEvents', FlowEventsController, flowEventsArgs));
200
201        const cpuProfileArgs: CpuProfileControllerArgs = { engine };
202        childControllers.push(
203          Child('cpuProfile', CpuProfileController, cpuProfileArgs));
204
205        const flamegraphArgs: FlamegraphControllerArgs = { engine };
206        childControllers.push(
207          Child('flamegraph', FlamegraphController, flamegraphArgs));
208        childControllers.push(Child(
209          'cpu_aggregation',
210          CpuAggregationController,
211          { engine, kind: 'cpu_aggregation' }));
212        childControllers.push(Child(
213          'thread_aggregation',
214          ThreadAggregationController,
215          { engine, kind: 'thread_state_aggregation' }));
216        childControllers.push(Child(
217          'cpu_process_aggregation',
218          CpuByProcessAggregationController,
219          { engine, kind: 'cpu_by_process_aggregation' }));
220        childControllers.push(Child(
221          'slice_aggregation',
222          SliceAggregationController,
223          { engine, kind: 'slice_aggregation' }));
224        childControllers.push(Child(
225          'counter_aggregation',
226          CounterAggregationController,
227          { engine, kind: 'counter_aggregation' }));
228        childControllers.push(Child(
229          'frame_aggregation',
230          FrameAggregationController,
231          { engine, kind: 'frame_aggregation' }));
232        childControllers.push(Child('search', SearchController, {
233          engine,
234          app: globals,
235        }));
236        childControllers.push(
237            Child('pivot_table_redux', PivotTableReduxController, {engine}));
238
239        childControllers.push(Child('logs', LogsController, {
240          engine,
241          app: globals,
242        }));
243        childControllers.push(
244          Child('traceError', TraceErrorController, { engine }));
245        childControllers.push(Child('metrics', MetricsController, { engine }));
246
247        // Create a PivotTableController for each pivot table.
248        for (const pivotTableId of Object.keys(globals.state.pivotTable)) {
249          const pivotTableArgs:
250            PivotTableControllerArgs = { pivotTableId, engine };
251          childControllers.push(
252            Child(pivotTableId, PivotTableController, pivotTableArgs));
253        }
254
255        return childControllers;
256
257      default:
258        throw new Error(`unknown state ${this.state}`);
259    }
260    return;
261  }
262
263  onDestroy() {
264    frontendGlobals.engines.delete(this.engineId);
265  }
266
267  private async loadTrace(): Promise<EngineMode> {
268    this.updateStatus('Creating trace processor');
269    // Check if there is any instance of the trace_processor_shell running in
270    // HTTP RPC mode (i.e. trace_processor_shell -D).
271    let engineMode: EngineMode;
272    let useRpc = false;
273    if (globals.state.newEngineMode === 'USE_HTTP_RPC_IF_AVAILABLE') {
274      useRpc = (await HttpRpcEngine.checkConnection()).connected;
275    }
276    let engine;
277    if (useRpc) {
278      console.log('Opening trace using native accelerator over HTTP+RPC');
279      engineMode = 'HTTP_RPC';
280      engine = new HttpRpcEngine(this.engineId, LoadingManager.getInstance);
281      engine.errorHandler = (err) => {
282        globals.dispatch(
283          Actions.setEngineFailed({ mode: 'HTTP_RPC', failure: `${err}` }));
284        throw err;
285      };
286    } else {
287      console.log('Opening trace using built-in WASM engine');
288      engineMode = 'WASM';
289      const enginePort = resetEngineWorker();
290      engine = new WasmEngineProxy(
291        this.engineId, enginePort, LoadingManager.getInstance);
292    }
293    this.engine = engine;
294
295    frontendGlobals.engines.set(this.engineId, engine);
296    globals.dispatch(Actions.setEngineReady({
297      engineId: this.engineId,
298      ready: false,
299      mode: engineMode,
300    }));
301    const engineCfg = assertExists(globals.state.engines[this.engineId]);
302    let traceStream: TraceStream | undefined;
303    if (engineCfg.source.type === 'FILE') {
304      traceStream = new TraceFileStream(engineCfg.source.file);
305    } else if (engineCfg.source.type === 'ARRAY_BUFFER') {
306      traceStream = new TraceBufferStream(engineCfg.source.buffer);
307    } else if (engineCfg.source.type === 'URL') {
308      traceStream = new TraceHttpStream(engineCfg.source.url);
309    } else if (engineCfg.source.type === 'HTTP_RPC') {
310      traceStream = undefined;
311    } else {
312      throw new Error(`Unknown source: ${JSON.stringify(engineCfg.source)}`);
313    }
314
315    // |traceStream| can be undefined in the case when we are using the external
316    // HTTP+RPC endpoint and the trace processor instance has already loaded
317    // a trace (because it was passed as a cmdline argument to
318    // trace_processor_shell). In this case we don't want the UI to load any
319    // file/stream and we just want to jump to the loading phase.
320    if (traceStream !== undefined) {
321      const tStart = performance.now();
322      for (; ;) {
323        const res = await traceStream.readChunk();
324        await this.engine.parse(res.data);
325        const elapsed = (performance.now() - tStart) / 1000;
326        let status = 'Loading trace ';
327        if (res.bytesTotal > 0) {
328          const progress = Math.round(res.bytesRead / res.bytesTotal * 100);
329          status += `${progress}%`;
330        } else {
331          status += `${Math.round(res.bytesRead / 1e6)} MB`;
332        }
333        status += ` - ${Math.ceil(res.bytesRead / elapsed / 1e6)} MB/s`;
334        this.updateStatus(status);
335        if (res.eof) break;
336      }
337      await this.engine.notifyEof();
338    } else {
339      assertTrue(this.engine instanceof HttpRpcEngine);
340      await this.engine.restoreInitialTables();
341    }
342
343    // traceUuid will be '' if the trace is not cacheable (URL or RPC).
344    const traceUuid = await this.cacheCurrentTrace();
345
346    const traceTime = await this.engine.getTraceTimeBounds();
347    let startSec = traceTime.start;
348    let endSec = traceTime.end;
349    startSec -= TRACE_MARGIN_TIME_S;
350    endSec += TRACE_MARGIN_TIME_S;
351    const traceTimeState = {
352      startSec,
353      endSec,
354    };
355
356    const emptyOmniboxState = {
357      omnibox: '',
358      mode: frontendGlobals.state.frontendLocalState.omniboxState.mode ||
359        'SEARCH',
360      lastUpdate: Date.now() / 1000
361    };
362
363    const actions: DeferredAction[] = [
364      Actions.setOmnibox(emptyOmniboxState),
365      Actions.setTraceUuid({ traceUuid }),
366      Actions.setTraceTime(traceTimeState)
367    ];
368
369    let visibleStartSec = startSec;
370    let visibleEndSec = endSec;
371    const mdTime = await this.engine.getTracingMetadataTimeBounds();
372    // make sure the bounds hold
373    if (Math.max(visibleStartSec, mdTime.start - TRACE_MARGIN_TIME_S) <
374      Math.min(visibleEndSec, mdTime.end + TRACE_MARGIN_TIME_S)) {
375      visibleStartSec =
376        Math.max(visibleStartSec, mdTime.start - TRACE_MARGIN_TIME_S);
377      visibleEndSec = Math.min(visibleEndSec, mdTime.end + TRACE_MARGIN_TIME_S);
378    }
379
380    // We don't know the resolution at this point. However this will be
381    // replaced in 50ms so a guess is fine.
382    const resolution = (visibleStartSec - visibleEndSec) / 1000;
383    actions.push(Actions.setVisibleTraceTime({
384      startSec: visibleStartSec,
385      endSec: visibleEndSec,
386      lastUpdate: Date.now() / 1000,
387      resolution
388    }));
389
390    globals.dispatchMultiple(actions);
391    Router.navigate(`#!/viewer?local_cache_key=${traceUuid}`);
392
393    // Make sure the helper views are available before we start adding tracks.
394    await this.initialiseHelperViews();
395
396    {
397      // When we reload from a permalink don't create extra tracks:
398      const { pinnedTracks, tracks } = globals.state;
399      if (!pinnedTracks.length && !Object.keys(tracks).length) {
400        await this.listTracks();
401      }
402    }
403
404    await this.listThreads();
405    await this.loadTimelineOverview(traceTime);
406
407    {
408      // A quick heuristic to check if the trace has ftrace events. This is
409      // based on the assumption that most traces that have ftrace either:
410      // - Are proto traces captured via perfetto, in which case traced_probes
411      //   emits ftrace per-cpu stats that end up in the stats table.
412      // - Have a raw event with non-zero cpu or utid.
413      // Notes:
414      // - The "+1 > 1" is to avoid pushing down the constraints to the "raw"
415      //   table, which would compute a full column filter without being aware
416      //   of the limit 1, and instead delegate the filtering to the iterator.
417      const query = `select '_' as _ from raw
418          where cpu + 1 > 1 or utid + 1 > 1 limit 1`;
419      const result = await assertExists(this.engine).query(query);
420      const hasFtrace = result.numRows() > 0;
421      publishHasFtrace(hasFtrace);
422    }
423
424    globals.dispatch(Actions.removeDebugTrack({}));
425    globals.dispatch(Actions.sortThreadTracks({}));
426
427    await this.selectFirstHeapProfile();
428    if (PERF_SAMPLE_FLAG.get()) {
429      await this.selectPerfSample();
430    }
431
432    return engineMode;
433  }
434
435  private async selectPerfSample() {
436    const query = `select ts, upid
437        from perf_sample
438        join thread using (utid)
439        order by ts desc limit 1`;
440    const profile = await assertExists(this.engine).query(query);
441    if (profile.numRows() !== 1) return;
442    const row = profile.firstRow({ ts: NUM, upid: NUM });
443    const ts = row.ts;
444    const upid = row.upid;
445    globals.dispatch(
446      Actions.selectPerfSamples({ id: 0, upid, ts, type: 'perf' }));
447  }
448
449  private async selectFirstHeapProfile() {
450    const query = `select * from
451    (select distinct(ts) as ts, 'native' as type,
452        upid from heap_profile_allocation
453        union
454        select distinct(graph_sample_ts) as ts, 'graph' as type, upid from
455        heap_graph_object) order by ts limit 1`;
456    const profile = await assertExists(this.engine).query(query);
457    if (profile.numRows() !== 1) return;
458    const row = profile.firstRow({ ts: NUM, type: STR, upid: NUM });
459    const ts = row.ts;
460    const type = row.type;
461    const upid = row.upid;
462    globals.dispatch(Actions.selectHeapProfile({ id: 0, upid, ts, type }));
463  }
464
465  private async listTracks() {
466    this.updateStatus('Loading tracks');
467    const engine = assertExists<Engine>(this.engine);
468    const actions = await decideTracks(this.engineId, engine);
469    globals.dispatchMultiple(actions);
470  }
471
472  private async listThreads() {
473    this.updateStatus('Reading thread list');
474    const query = `select
475        utid,
476        tid,
477        pid,
478        ifnull(thread.name, '') as threadName,
479        ifnull(
480          case when length(process.name) > 0 then process.name else null end,
481          thread.name) as procName,
482        process.cmdline as cmdline
483        from (select * from thread order by upid) as thread
484        left join (select * from process order by upid) as process
485        using(upid)`;
486    const result = await assertExists(this.engine).query(query);
487    const threads: ThreadDesc[] = [];
488    const it = result.iter({
489      utid: NUM,
490      tid: NUM,
491      pid: NUM_NULL,
492      threadName: STR,
493      procName: STR_NULL,
494      cmdline: STR_NULL,
495    });
496    for (; it.valid(); it.next()) {
497      const utid = it.utid;
498      const tid = it.tid;
499      const pid = it.pid === null ? undefined : it.pid;
500      const threadName = it.threadName;
501      const procName = it.procName === null ? undefined : it.procName;
502      const cmdline = it.cmdline === null ? undefined : it.cmdline;
503      threads.push({ utid, tid, threadName, pid, procName, cmdline });
504    }
505    publishThreads(threads);
506  }
507
508  private async loadTimelineOverview(traceTime: TimeSpan) {
509    const engine = assertExists<Engine>(this.engine);
510    const numSteps = 100;
511    const stepSec = traceTime.duration / numSteps;
512    let hasSchedOverview = false;
513    for (let step = 0; step < numSteps; step++) {
514      this.updateStatus(
515        'Loading overview ' +
516        `${Math.round((step + 1) / numSteps * 1000) / 10}%`);
517      const startSec = traceTime.start + step * stepSec;
518      const startNs = toNsFloor(startSec);
519      const endSec = startSec + stepSec;
520      const endNs = toNsCeil(endSec);
521
522      // Sched overview.
523      const schedResult = await engine.query(
524        `select sum(dur)/${stepSec}/1e9 as load, cpu from sched ` +
525        `where ts >= ${startNs} and ts < ${endNs} and utid != 0 ` +
526        'group by cpu order by cpu');
527      const schedData: { [key: string]: QuantizedLoad } = {};
528      const it = schedResult.iter({ load: NUM, cpu: NUM });
529      for (; it.valid(); it.next()) {
530        const load = it.load;
531        const cpu = it.cpu;
532        schedData[cpu] = { startSec, endSec, load };
533        hasSchedOverview = true;
534      }
535      publishOverviewData(schedData);
536    }
537
538    if (hasSchedOverview) {
539      return;
540    }
541
542    // Slices overview.
543    const traceStartNs = toNs(traceTime.start);
544    const stepSecNs = toNs(stepSec);
545    const sliceResult = await engine.query(`select
546           bucket,
547           upid,
548           ifnull(sum(utid_sum) / cast(${stepSecNs} as float), 0) as load
549         from thread
550         inner join (
551           select
552             ifnull(cast((ts - ${traceStartNs})/${stepSecNs} as int), 0) as bucket,
553             sum(dur) as utid_sum,
554             utid
555           from slice
556           inner join thread_track on slice.track_id = thread_track.id
557           group by bucket, utid
558         ) using(utid)
559         where upid is not null
560         group by bucket, upid`);
561
562    const slicesData: { [key: string]: QuantizedLoad[] } = {};
563    const it = sliceResult.iter({ bucket: NUM, upid: NUM, load: NUM });
564    for (; it.valid(); it.next()) {
565      const bucket = it.bucket;
566      const upid = it.upid;
567      const load = it.load;
568
569      const startSec = traceTime.start + stepSec * bucket;
570      const endSec = startSec + stepSec;
571
572      const upidStr = upid.toString();
573      let loadArray = slicesData[upidStr];
574      if (loadArray === undefined) {
575        loadArray = slicesData[upidStr] = [];
576      }
577      loadArray.push({ startSec, endSec, load });
578    }
579    publishOverviewData(slicesData);
580  }
581
582  private async cacheCurrentTrace(): Promise<string> {
583    const engine = assertExists(this.engine);
584    const result = await engine.query(`select str_value as uuid from metadata
585                  where name = 'trace_uuid'`);
586    if (result.numRows() === 0) {
587      // One of the cases covered is an empty trace.
588      return '';
589    }
590    const traceUuid = result.firstRow({ uuid: STR }).uuid;
591    const engineConfig = assertExists(globals.state.engines[engine.id]);
592    if (!(await cacheTrace(engineConfig.source, traceUuid))) {
593      // If the trace is not cacheable (cacheable means it has been opened from
594      // URL or RPC) only append '?local_cache_key' to the URL, without the
595      // local_cache_key value. Doing otherwise would cause an error if the tab
596      // is discarded or the user hits the reload button because the trace is
597      // not in the cache.
598      return '';
599    }
600    return traceUuid;
601  }
602
603  async initialiseHelperViews() {
604    const engine = assertExists<Engine>(this.engine);
605
606    this.updateStatus('Creating annotation counter track table');
607    // Create the helper tables for all the annotations related data.
608    // NULL in min/max means "figure it out per track in the usual way".
609    await engine.query(`
610      CREATE TABLE annotation_counter_track(
611        id INTEGER PRIMARY KEY,
612        name STRING,
613        __metric_name STRING,
614        upid INTEGER,
615        min_value DOUBLE,
616        max_value DOUBLE
617      );
618    `);
619    this.updateStatus('Creating annotation slice track table');
620    await engine.query(`
621      CREATE TABLE annotation_slice_track(
622        id INTEGER PRIMARY KEY,
623        name STRING,
624        __metric_name STRING,
625        upid INTEGER,
626        group_name STRING
627      );
628    `);
629
630    this.updateStatus('Creating annotation counter table');
631    await engine.query(`
632      CREATE TABLE annotation_counter(
633        id BIG INT,
634        track_id INT,
635        ts BIG INT,
636        value DOUBLE,
637        PRIMARY KEY (track_id, ts)
638      ) WITHOUT ROWID;
639    `);
640    this.updateStatus('Creating annotation slice table');
641    await engine.query(`
642      CREATE TABLE annotation_slice(
643        id INTEGER PRIMARY KEY,
644        track_id INT,
645        ts BIG INT,
646        dur BIG INT,
647        depth INT,
648        cat STRING,
649        name STRING,
650        UNIQUE(track_id, ts)
651      );
652    `);
653
654
655    const availableMetrics = [];
656    const metricsResult = await engine.query('select name from trace_metrics');
657    for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
658      availableMetrics.push(it.name);
659    }
660    globals.dispatch(Actions.setAvailableMetrics({availableMetrics}));
661
662    const availableMetricsSet = new Set<string>(availableMetrics);
663    for (const [flag, metric] of FLAGGED_METRICS) {
664      if (!flag.get() || !availableMetricsSet.has(metric)) {
665        continue;
666      }
667
668      this.updateStatus(`Computing ${metric} metric`);
669      try {
670        // We don't care about the actual result of metric here as we are just
671        // interested in the annotation tracks.
672        await engine.computeMetric([metric]);
673      } catch (e) {
674        if (e instanceof QueryError) {
675          publishMetricError('MetricError: ' + e.message);
676          continue;
677        } else {
678          throw e;
679        }
680      }
681
682      this.updateStatus(`Inserting data for ${metric} metric`);
683      try {
684        const result = await engine.query(`pragma table_info(${metric}_event)`);
685        let hasSliceName = false;
686        let hasDur = false;
687        let hasUpid = false;
688        let hasValue = false;
689        let hasGroupName = false;
690        const it = result.iter({ name: STR });
691        for (; it.valid(); it.next()) {
692          const name = it.name;
693          hasSliceName = hasSliceName || name === 'slice_name';
694          hasDur = hasDur || name === 'dur';
695          hasUpid = hasUpid || name === 'upid';
696          hasValue = hasValue || name === 'value';
697          hasGroupName = hasGroupName || name === 'group_name';
698        }
699
700        const upidColumnSelect = hasUpid ? 'upid' : '0 AS upid';
701        const upidColumnWhere = hasUpid ? 'upid' : '0';
702        const groupNameColumn =
703          hasGroupName ? 'group_name' : 'NULL AS group_name';
704        if (hasSliceName && hasDur) {
705          await engine.query(`
706            INSERT INTO annotation_slice_track(
707              name, __metric_name, upid, group_name)
708            SELECT DISTINCT
709              track_name,
710              '${metric}' as metric_name,
711              ${upidColumnSelect},
712              ${groupNameColumn}
713            FROM ${metric}_event
714            WHERE track_type = 'slice'
715          `);
716          await engine.query(`
717            INSERT INTO annotation_slice(track_id, ts, dur, depth, cat, name)
718            SELECT
719              t.id AS track_id,
720              ts,
721              dur,
722              0 AS depth,
723              a.track_name as cat,
724              slice_name AS name
725            FROM ${metric}_event a
726            JOIN annotation_slice_track t
727            ON a.track_name = t.name AND t.__metric_name = '${metric}'
728            ORDER BY t.id, ts
729          `);
730        }
731
732        if (hasValue) {
733          const minMax = await engine.query(`
734            SELECT
735              IFNULL(MIN(value), 0) as minValue,
736              IFNULL(MAX(value), 0) as maxValue
737            FROM ${metric}_event
738            WHERE ${upidColumnWhere} != 0`);
739          const row = minMax.firstRow({ minValue: NUM, maxValue: NUM });
740          await engine.query(`
741            INSERT INTO annotation_counter_track(
742              name, __metric_name, min_value, max_value, upid)
743            SELECT DISTINCT
744              track_name,
745              '${metric}' as metric_name,
746              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.minValue} END,
747              CASE ${upidColumnWhere} WHEN 0 THEN NULL ELSE ${row.maxValue} END,
748              ${upidColumnSelect}
749            FROM ${metric}_event
750            WHERE track_type = 'counter'
751          `);
752          await engine.query(`
753            INSERT INTO annotation_counter(id, track_id, ts, value)
754            SELECT
755              -1 as id,
756              t.id AS track_id,
757              ts,
758              value
759            FROM ${metric}_event a
760            JOIN annotation_counter_track t
761            ON a.track_name = t.name AND t.__metric_name = '${metric}'
762            ORDER BY t.id, ts
763          `);
764        }
765      } catch (e) {
766        if (e instanceof QueryError) {
767          publishMetricError('MetricError: ' + e.message);
768        } else {
769          throw e;
770        }
771      }
772    }
773  }
774
775  private updateStatus(msg: string): void {
776    globals.dispatch(Actions.updateStatus({
777      msg,
778      timestamp: Date.now() / 1000,
779    }));
780  }
781}
782