xref: /aosp_15_r20/external/perfetto/ui/src/plugins/dev.perfetto.MetricsPage/metrics_page.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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        .finally(() => {
141          this.trace.scheduleFullRedraw();
142        });
143    }
144    this.trace.scheduleFullRedraw();
145  }
146}
147
148interface MetricResultAttrs {
149  result: Result<string> | 'pending';
150}
151
152class MetricResultView implements m.ClassComponent<MetricResultAttrs> {
153  view({attrs}: m.CVnode<MetricResultAttrs>) {
154    const result = attrs.result;
155    if (result === 'pending') {
156      return m(Spinner);
157    }
158
159    if (!result.ok) {
160      return m('pre.metric-error', result.error);
161    }
162
163    return m('pre', result.value);
164  }
165}
166
167interface MetricPickerAttrs {
168  controller: MetricsController;
169}
170
171class MetricPicker implements m.ClassComponent<MetricPickerAttrs> {
172  view({attrs}: m.CVnode<MetricPickerAttrs>) {
173    const {controller} = attrs;
174    return m(
175      '.metrics-page-picker',
176      m(
177        Select,
178        {
179          value: controller.selected,
180          oninput: (e: Event) => {
181            if (!e.target) return;
182            controller.selected = (e.target as HTMLSelectElement).value;
183          },
184        },
185        controller.metrics.map((metric) =>
186          m(
187            'option',
188            {
189              value: metric,
190              key: metric,
191            },
192            metric,
193          ),
194        ),
195      ),
196      m(
197        Select,
198        {
199          oninput: (e: Event) => {
200            if (!e.target) return;
201            controller.format = (e.target as HTMLSelectElement).value as Format;
202          },
203        },
204        FORMATS.map((f) => {
205          return m('option', {
206            selected: controller.format === f,
207            key: f,
208            value: f,
209            label: f,
210          });
211        }),
212      ),
213    );
214  }
215}
216
217interface MetricVizViewAttrs {
218  visualisation: MetricVisualisation;
219  data: unknown;
220}
221
222class MetricVizView implements m.ClassComponent<MetricVizViewAttrs> {
223  view({attrs}: m.CVnode<MetricVizViewAttrs>) {
224    return m(
225      '',
226      m(VegaView, {
227        spec: attrs.visualisation.spec,
228        data: {
229          metric: attrs.data,
230        },
231      }),
232    );
233  }
234}
235
236export class MetricsPage implements m.ClassComponent<PageWithTraceAttrs> {
237  private controller?: MetricsController;
238
239  oninit({attrs}: m.Vnode<PageWithTraceAttrs>) {
240    this.controller = new MetricsController(attrs.trace);
241  }
242
243  view() {
244    const controller = assertExists(this.controller);
245    const json = controller.resultAsJson;
246    return m(
247      '.metrics-page',
248      m(MetricPicker, {
249        controller,
250      }),
251      controller.format === 'json' &&
252        controller.visualisations.map((visualisation) => {
253          let data = json;
254          for (const p of visualisation.path) {
255            data = data[p] ?? [];
256          }
257          return m(MetricVizView, {visualisation, data});
258        }),
259      m(MetricResultView, {result: controller.result}),
260    );
261  }
262}
263