• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2020 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 m from 'mithril';
16
17import {
18  error,
19  isError,
20  isPending,
21  pending,
22  Result,
23  success,
24} from '../base/result';
25import {pluginManager, PluginManager} from '../common/plugins';
26import {raf} from '../core/raf_scheduler';
27import {MetricVisualisation} from '../public';
28import {Engine} from '../trace_processor/engine';
29import {STR} from '../trace_processor/query_result';
30import {Select} from '../widgets/select';
31import {Spinner} from '../widgets/spinner';
32import {VegaView} from '../widgets/vega_view';
33
34import {globals} from './globals';
35import {createPage} from './pages';
36
37type Format = 'json' | 'prototext' | 'proto';
38const FORMATS: Format[] = ['json', 'prototext', 'proto'];
39
40function getEngine(): Engine | undefined {
41  const engineId = globals.getCurrentEngine()?.id;
42  if (engineId === undefined) {
43    return undefined;
44  }
45  const engine = globals.engines.get(engineId)?.getProxy('MetricsPage');
46  return engine;
47}
48
49async function getMetrics(engine: Engine): Promise<string[]> {
50  const metrics: string[] = [];
51  const metricsResult = await engine.query('select name from trace_metrics');
52  for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
53    metrics.push(it.name);
54  }
55  return metrics;
56}
57
58async function getMetric(
59  engine: Engine,
60  metric: string,
61  format: Format,
62): Promise<string> {
63  const result = await engine.computeMetric([metric], format);
64  if (result instanceof Uint8Array) {
65    return `Uint8Array<len=${result.length}>`;
66  } else {
67    return result;
68  }
69}
70
71class MetricsController {
72  engine: Engine;
73  plugins: PluginManager;
74  private _metrics: string[];
75  private _selected?: string;
76  private _result: Result<string>;
77  private _format: Format;
78  // eslint-disable-next-line @typescript-eslint/no-explicit-any
79  private _json: any;
80
81  constructor(plugins: PluginManager, engine: Engine) {
82    this.plugins = plugins;
83    this.engine = engine;
84    this._metrics = [];
85    this._result = success('');
86    this._json = {};
87    this._format = 'json';
88    getMetrics(this.engine).then((metrics) => {
89      this._metrics = metrics;
90    });
91  }
92
93  get metrics(): string[] {
94    return this._metrics;
95  }
96
97  get visualisations(): MetricVisualisation[] {
98    return this.plugins
99      .metricVisualisations()
100      .filter((v) => v.metric === this.selected);
101  }
102
103  set selected(metric: string | undefined) {
104    if (this._selected === metric) {
105      return;
106    }
107    this._selected = metric;
108    this.update();
109  }
110
111  get selected(): string | undefined {
112    return this._selected;
113  }
114
115  set format(format: Format) {
116    if (this._format === format) {
117      return;
118    }
119    this._format = format;
120    this.update();
121  }
122
123  get format(): Format {
124    return this._format;
125  }
126
127  get result(): Result<string> {
128    return this._result;
129  }
130
131  // eslint-disable-next-line @typescript-eslint/no-explicit-any
132  get resultAsJson(): any {
133    return this._json;
134  }
135
136  private update() {
137    const selected = this._selected;
138    const format = this._format;
139    if (selected === undefined) {
140      this._result = success('');
141      this._json = {};
142    } else {
143      this._result = pending();
144      this._json = {};
145      getMetric(this.engine, selected, format)
146        .then((result) => {
147          if (this._selected === selected && this._format === format) {
148            this._result = success(result);
149            if (format === 'json') {
150              this._json = JSON.parse(result);
151            }
152          }
153        })
154        .catch((e) => {
155          if (this._selected === selected && this._format === format) {
156            this._result = error(e);
157            this._json = {};
158          }
159        })
160        .finally(() => {
161          raf.scheduleFullRedraw();
162        });
163    }
164    raf.scheduleFullRedraw();
165  }
166}
167
168interface MetricResultAttrs {
169  result: Result<string>;
170}
171
172class MetricResultView implements m.ClassComponent<MetricResultAttrs> {
173  view({attrs}: m.CVnode<MetricResultAttrs>) {
174    const result = attrs.result;
175    if (isPending(result)) {
176      return m(Spinner);
177    }
178
179    if (isError(result)) {
180      return m('pre.metric-error', result.error);
181    }
182
183    return m('pre', result.data);
184  }
185}
186
187interface MetricPickerAttrs {
188  controller: MetricsController;
189}
190
191class MetricPicker implements m.ClassComponent<MetricPickerAttrs> {
192  view({attrs}: m.CVnode<MetricPickerAttrs>) {
193    const {controller} = attrs;
194    return m(
195      '.metrics-page-picker',
196      m(
197        Select,
198        {
199          value: controller.selected,
200          oninput: (e: Event) => {
201            if (!e.target) return;
202            controller.selected = (e.target as HTMLSelectElement).value;
203          },
204        },
205        controller.metrics.map((metric) =>
206          m(
207            'option',
208            {
209              value: metric,
210              key: metric,
211            },
212            metric,
213          ),
214        ),
215      ),
216      m(
217        Select,
218        {
219          oninput: (e: Event) => {
220            if (!e.target) return;
221            controller.format = (e.target as HTMLSelectElement).value as Format;
222          },
223        },
224        FORMATS.map((f) => {
225          return m('option', {
226            selected: controller.format === f,
227            key: f,
228            value: f,
229            label: f,
230          });
231        }),
232      ),
233    );
234  }
235}
236
237// eslint-disable-next-line @typescript-eslint/no-explicit-any
238interface MetricVizViewAttrs {
239  visualisation: MetricVisualisation;
240  // eslint-disable-next-line @typescript-eslint/no-explicit-any
241  data: any;
242}
243
244class MetricVizView implements m.ClassComponent<MetricVizViewAttrs> {
245  view({attrs}: m.CVnode<MetricVizViewAttrs>) {
246    return m(
247      '',
248      m(VegaView, {
249        spec: attrs.visualisation.spec,
250        data: {
251          metric: attrs.data,
252        },
253      }),
254    );
255  }
256}
257
258class MetricPageContents implements m.ClassComponent {
259  controller?: MetricsController;
260
261  oncreate() {
262    const engine = getEngine();
263    if (engine !== undefined) {
264      this.controller = new MetricsController(pluginManager, engine);
265    }
266  }
267
268  view() {
269    const controller = this.controller;
270    if (controller === undefined) {
271      return m('');
272    }
273
274    const json = controller.resultAsJson;
275
276    return [
277      m(MetricPicker, {
278        controller,
279      }),
280      controller.format === 'json' &&
281        controller.visualisations.map((visualisation) => {
282          let data = json;
283          for (const p of visualisation.path) {
284            data = data[p] ?? [];
285          }
286          return m(MetricVizView, {visualisation, data});
287        }),
288      m(MetricResultView, {result: controller.result}),
289    ];
290  }
291}
292
293export const MetricsPage = createPage({
294  view() {
295    return m('.metrics-page', m(MetricPageContents));
296  },
297});
298