// Copyright (C) 2024 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 {Trace} from '../../public/trace'; import {PerfettoPlugin} from '../../public/plugin'; import {METRIC_HANDLERS} from './handlers/handlerRegistry'; import {MetricData, MetricHandlerMatch} from './handlers/metricUtils'; import {PLUGIN_ID} from './pluginId'; import AndroidCujsPlugin from '../dev.perfetto.AndroidCujs'; const JANK_CUJ_QUERY_PRECONDITIONS = ` SELECT RUN_METRIC('android/android_blocking_calls_cuj_metric.sql'); `; function getMetricsFromHash(): string[] { const metricVal = location.hash; const regex = new RegExp(`${PLUGIN_ID}:metrics=(.*)`); const match = metricVal.match(regex); if (match === null) { return []; } const capturedString = match[1]; let metricList: string[] = []; if (capturedString.includes('--')) { metricList = capturedString.split('--'); } else { metricList = [capturedString]; } return metricList.map((metric) => decodeURIComponent(metric)); } let metrics: string[]; /** * Plugin that adds and pins the debug track for the metric passed * For more context - * This plugin reads the names of regressed metrics from the url upon loading * It then checks the metric names against some handlers and if they * match it accordingly adds the debug tracks for them * This way when comparing two different perfetto traces before and after * the regression, the user will not have to manually search for the * slices related to the regressed metric */ export default class implements PerfettoPlugin { static readonly id = PLUGIN_ID; static readonly dependencies = [AndroidCujsPlugin]; static onActivate(): void { metrics = getMetricsFromHash(); } async onTraceLoad(ctx: Trace) { ctx.commands.registerCommand({ id: 'dev.perfetto.PinAndroidPerfMetrics#PinAndroidPerfMetrics', name: 'Add and Pin: Jank Metric Slice', callback: async (metric) => { metric = prompt('Metrics names (separated by comma)', ''); if (metric === null) return; const metricList = metric.split(','); this.callHandlers(metricList, ctx); }, }); if (metrics.length !== 0) { this.callHandlers(metrics, ctx); } } private async callHandlers(metricsList: string[], ctx: Trace) { // List of metrics that actually match some handler const metricsToShow: MetricHandlerMatch[] = this.getMetricsToShow(metricsList); if (metricsToShow.length === 0) { return; } await ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS); for (const {metricData, metricHandler} of metricsToShow) { metricHandler.addMetricTrack(metricData, ctx); } } private getMetricsToShow(metricList: string[]): MetricHandlerMatch[] { const sortedMetricList = [...metricList].sort(); const validMetrics: MetricHandlerMatch[] = []; const alreadyMatchedMetricData: Set = new Set(); for (const metric of sortedMetricList) { for (const metricHandler of METRIC_HANDLERS) { const metricData = metricHandler.match(metric); if (!metricData) continue; const jsonMetricData = this.metricDataToJson(metricData); if (!alreadyMatchedMetricData.has(jsonMetricData)) { alreadyMatchedMetricData.add(jsonMetricData); validMetrics.push({ metricData: metricData, metricHandler: metricHandler, }); } } } return validMetrics; } private metricDataToJson(metricData: MetricData): string { // Used to have a deterministic keys order. return JSON.stringify(metricData, Object.keys(metricData).sort()); } }