blob: 829a40dbb016fc0059e84f5edea29202262a2707 [file]
// 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 {
expandProcessName,
type BlockingCallMetricData,
type MetricHandler,
} from './metricUtils';
import type {Trace} from '../../../public/trace';
import {
addJankCUJDebugTrack,
addLatencyCUJDebugTrack,
} from '../../com.android.AndroidCujs';
import {
addDebugSliceTrack,
type DebugSliceTrackArgs,
} from '../../../components/tracks/debug_tracks';
import {
LONG,
type QueryResult,
type Row,
} from '../../../trace_processor/query_result';
class BlockingCallMetricHandler implements MetricHandler {
/**
* Matches metric key for blocking call and per-frame blocking call metrics & return parsed data
* if successful.
*
* @param {string} metricKey The metric key to match.
* @returns {BlockingCallMetricData | undefined} Parsed data or undefined if no match.
*/
public match(metricKey: string): BlockingCallMetricData | undefined {
const matcher =
/perfetto_android_blocking_call(?:_per_frame)?-cuj-name-(?<process>.*)-name-(?<cujName>.*)-blocking_calls-name-(?<blockingCallName>([^\-]*))-(?<aggregation>.*)/;
const match = matcher.exec(metricKey);
if (!match?.groups) {
return undefined;
}
const metricData: BlockingCallMetricData = {
process: expandProcessName(match.groups.process),
cujName: match.groups.cujName,
blockingCallName: match.groups.blockingCallName,
aggregation: match.groups.aggregation,
};
return metricData;
}
/**
* Adds the debug tracks for Blocking Call metrics
*
* @param {BlockingCallMetricData} metricData Parsed metric data for the cuj scoped jank
* @param {Trace} ctx PluginContextTrace for trace related properties and methods
* @returns {void} Adds one track for Jank CUJ slice and one for Janky CUJ frames
*/
public async addMetricTrack(metricData: BlockingCallMetricData, ctx: Trace) {
this.pinSingleCuj(ctx, metricData);
const config = this.blockingCallTrackConfig(metricData);
addDebugSliceTrack({trace: ctx, ...config});
// Only trigger adding track for frame when the aggregation is for max duration per frame.
const MAX_DUR_PER_FRAME_NS_MEAN = 'max_dur_per_frame_ns-mean';
if (metricData.aggregation === MAX_DUR_PER_FRAME_NS_MEAN) {
const frameConfigArgs = await this.frameWithMaxDurBlockingCallTrackConfig(
ctx,
metricData,
);
addDebugSliceTrack({trace: ctx, ...frameConfigArgs});
}
}
private async pinSingleCuj(ctx: Trace, metricData: BlockingCallMetricData) {
const jankTrackName = `Jank CUJ: ${metricData.cujName}`;
const latencyTrackName = `Latency CUJ: ${metricData.cujName}`;
// TODO: b/296349525 - Refactor once CUJ tables are migrated to stdlib
// Currently, we try to pin a Jank CUJ track and if that fails we add
// a Latency CUJ track. We can instead look up a single CUJ table to
// better determine what to query and pin.
const jankCujPinned = await addJankCUJDebugTrack(
ctx,
jankTrackName,
metricData.cujName,
);
if (!jankCujPinned) {
addLatencyCUJDebugTrack(ctx, latencyTrackName, metricData.cujName);
}
}
private blockingCallTrackConfig(metricData: BlockingCallMetricData) {
const cuj = metricData.cujName;
const processName = metricData.process;
const blockingCallName = metricData.blockingCallName;
// TODO: b/296349525 - Migrate jank tables from run metrics to stdlib
const blockingCallDuringCujQuery = `
SELECT name, ts, dur
FROM blocking_call_slices_scoped_to_cujs
WHERE process_name = "${processName}"
AND cuj_name = "${cuj}"
AND name = "${blockingCallName}"
`;
const trackName = 'Blocking calls in ' + processName;
return {
data: {
sqlSource: blockingCallDuringCujQuery,
columns: ['name', 'ts', 'dur'],
},
columns: {ts: 'ts', dur: 'dur', name: 'name'},
rawColumns: ['name', 'ts', 'dur'],
title: trackName,
};
}
private async getFrameIdWithMaxDurationBlockingCall(
ctx: Trace,
metricData: BlockingCallMetricData,
): Promise<QueryResult> {
const cuj = metricData.cujName;
const processName = metricData.process;
const blockingCallName = metricData.blockingCallName;
// Fetch the frame_id of the frame with the max duration blocking call.
return ctx.engine.query(`
INCLUDE PERFETTO MODULE android.frame_blocking_calls.blocking_calls_aggregation;
SELECT
frame_id
FROM _blocking_calls_frame_cuj
WHERE
process_name = '${processName}'
AND name = '${blockingCallName}'
AND cuj_name = '${cuj}'
-- select frame_id for the metric with the maximum duration.
ORDER BY dur DESC
LIMIT 1`);
}
private async frameWithMaxDurBlockingCallTrackConfig(
ctx: Trace,
metricData: BlockingCallMetricData,
): Promise<
Pick<DebugSliceTrackArgs, 'data' | 'columns' | 'rawColumns' | 'title'>
> {
let row: Row = {
frame_id: null,
};
try {
row = (
await this.getFrameIdWithMaxDurationBlockingCall(ctx, metricData)
).firstRow({frame_id: LONG});
} catch (e) {
throw new Error(
`${e.message} caused by: No frame found for:
process: ${metricData.process}
CUJ: ${metricData.cujName}
blocking call: ${metricData.blockingCallName}`,
);
}
// Fetch the ts and dur for the extended frame boundary corresponding to the above frame_id.
const frameWithMaxDurBlockingCallQuery = `
SELECT
frame_id,
ts,
(ts_end - ts) AS dur
FROM _extended_frame_boundary
WHERE frame_id = ${row.frame_id}
`;
return {
data: {
sqlSource: frameWithMaxDurBlockingCallQuery,
columns: ['frame_id', 'ts', 'dur'],
},
columns: {ts: 'ts', dur: 'dur', name: 'frame_id'},
rawColumns: ['frame_id', 'ts', 'dur'],
title: 'Frame with max duration blocking call',
};
}
}
export const pinBlockingCallHandlerInstance = new BlockingCallMetricHandler();