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