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