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