blob: 8bd520b75a1a6c10c6bd425ba7c53ce1b2a081a6 [file] [log] [blame]
// Copyright (C) 2020 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import m from 'mithril';
import {
error,
isError,
isPending,
pending,
Result,
success,
} from '../base/result';
import {pluginManager, PluginManager} from '../common/plugins';
import {raf} from '../core/raf_scheduler';
import {MetricVisualisation} from '../public/plugin';
import {Engine} from '../trace_processor/engine';
import {STR} from '../trace_processor/query_result';
import {Select} from '../widgets/select';
import {Spinner} from '../widgets/spinner';
import {VegaView} from '../widgets/vega_view';
import {globals} from './globals';
import {createPage} from './pages';
type Format = 'json' | 'prototext' | 'proto';
const FORMATS: Format[] = ['json', 'prototext', 'proto'];
function getEngine(): Engine | undefined {
const engineId = globals.getCurrentEngine()?.id;
if (engineId === undefined) {
return undefined;
}
const engine = globals.engines.get(engineId)?.getProxy('MetricsPage');
return engine;
}
async function getMetrics(engine: Engine): Promise<string[]> {
const metrics: string[] = [];
const metricsResult = await engine.query('select name from trace_metrics');
for (const it = metricsResult.iter({name: STR}); it.valid(); it.next()) {
metrics.push(it.name);
}
return metrics;
}
async function getMetric(
engine: Engine,
metric: string,
format: Format,
): Promise<string> {
const result = await engine.computeMetric([metric], format);
if (result instanceof Uint8Array) {
return `Uint8Array<len=${result.length}>`;
} else {
return result;
}
}
class MetricsController {
engine: Engine;
plugins: PluginManager;
private _metrics: string[];
private _selected?: string;
private _result: Result<string>;
private _format: Format;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private _json: any;
constructor(plugins: PluginManager, engine: Engine) {
this.plugins = plugins;
this.engine = engine;
this._metrics = [];
this._result = success('');
this._json = {};
this._format = 'json';
getMetrics(this.engine).then((metrics) => {
this._metrics = metrics;
});
}
get metrics(): string[] {
return this._metrics;
}
get visualisations(): MetricVisualisation[] {
return this.plugins
.metricVisualisations()
.filter((v) => v.metric === this.selected);
}
set selected(metric: string | undefined) {
if (this._selected === metric) {
return;
}
this._selected = metric;
this.update();
}
get selected(): string | undefined {
return this._selected;
}
set format(format: Format) {
if (this._format === format) {
return;
}
this._format = format;
this.update();
}
get format(): Format {
return this._format;
}
get result(): Result<string> {
return this._result;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
get resultAsJson(): any {
return this._json;
}
private update() {
const selected = this._selected;
const format = this._format;
if (selected === undefined) {
this._result = success('');
this._json = {};
} else {
this._result = pending();
this._json = {};
getMetric(this.engine, selected, format)
.then((result) => {
if (this._selected === selected && this._format === format) {
this._result = success(result);
if (format === 'json') {
this._json = JSON.parse(result);
}
}
})
.catch((e) => {
if (this._selected === selected && this._format === format) {
this._result = error(e);
this._json = {};
}
})
.finally(() => {
raf.scheduleFullRedraw();
});
}
raf.scheduleFullRedraw();
}
}
interface MetricResultAttrs {
result: Result<string>;
}
class MetricResultView implements m.ClassComponent<MetricResultAttrs> {
view({attrs}: m.CVnode<MetricResultAttrs>) {
const result = attrs.result;
if (isPending(result)) {
return m(Spinner);
}
if (isError(result)) {
return m('pre.metric-error', result.error);
}
return m('pre', result.data);
}
}
interface MetricPickerAttrs {
controller: MetricsController;
}
class MetricPicker implements m.ClassComponent<MetricPickerAttrs> {
view({attrs}: m.CVnode<MetricPickerAttrs>) {
const {controller} = attrs;
return m(
'.metrics-page-picker',
m(
Select,
{
value: controller.selected,
oninput: (e: Event) => {
if (!e.target) return;
controller.selected = (e.target as HTMLSelectElement).value;
},
},
controller.metrics.map((metric) =>
m(
'option',
{
value: metric,
key: metric,
},
metric,
),
),
),
m(
Select,
{
oninput: (e: Event) => {
if (!e.target) return;
controller.format = (e.target as HTMLSelectElement).value as Format;
},
},
FORMATS.map((f) => {
return m('option', {
selected: controller.format === f,
key: f,
value: f,
label: f,
});
}),
),
);
}
}
interface MetricVizViewAttrs {
visualisation: MetricVisualisation;
data: unknown;
}
class MetricVizView implements m.ClassComponent<MetricVizViewAttrs> {
view({attrs}: m.CVnode<MetricVizViewAttrs>) {
return m(
'',
m(VegaView, {
spec: attrs.visualisation.spec,
data: {
metric: attrs.data,
},
}),
);
}
}
class MetricPageContents implements m.ClassComponent {
controller?: MetricsController;
oncreate() {
const engine = getEngine();
if (engine !== undefined) {
this.controller = new MetricsController(pluginManager, engine);
}
}
view() {
const controller = this.controller;
if (controller === undefined) {
return m('');
}
const json = controller.resultAsJson;
return [
m(MetricPicker, {
controller,
}),
controller.format === 'json' &&
controller.visualisations.map((visualisation) => {
let data = json;
for (const p of visualisation.path) {
data = data[p] ?? [];
}
return m(MetricVizView, {visualisation, data});
}),
m(MetricResultView, {result: controller.result}),
];
}
}
export const MetricsPage = createPage({
view() {
return m('.metrics-page', m(MetricPageContents));
},
});