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