blob: 6d7a19f78f471c7d7d05ad3697d47f890481b8de [file]
// Copyright (C) 2023 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 {addDebugSliceTrack} from '../../components/tracks/debug_tracks';
import type {Trace} from '../../public/trace';
import type {PerfettoPlugin} from '../../public/plugin';
import {STR} from '../../trace_processor/query_result';
import QueryPagePlugin from '../dev.perfetto.QueryPage';
/**
* Adds the Debug Slice Track for given Jank CUJ name
*
* @param {Trace} ctx For properties and methods of trace viewer
* @param {string} trackName Display Name of the track
* @param {string | string[]} cujNames List of Jank CUJs to pin
* @returns Returns true if the track was added, false otherwise
*/
export async function addJankCUJDebugTrack(
ctx: Trace,
trackName: string,
cujNames?: string | string[],
) {
const jankCujTrackConfig = generateJankCujTrackConfig(cujNames);
const result = await ctx.engine.query(jankCujTrackConfig.data.sqlSource);
// Check if query produces any results to prevent pinning an empty track
if (result.numRows() !== 0) {
await addDebugSliceTrack({
trace: ctx,
title: trackName,
...jankCujTrackConfig,
});
return true;
}
return false;
}
const JANK_CUJ_QUERY_PRECONDITIONS = `
INCLUDE PERFETTO MODULE android.cujs.frames;
INCLUDE PERFETTO MODULE android.cujs.sysui_cujs;
INCLUDE PERFETTO MODULE android.critical_blocking_calls;
`;
/**
* Builds the comma separated list of quoted CUJ slice names (both latency 'L<>'
* and jank 'J<>' variants) to use in a SQL `IN (...)` clause, or empty string
* if no names were requested.
*
* @param {string | string[]} cujNames List of CUJs to filter by
* @returns Returns the SQL `IN` list contents, or '' if unfiltered
*/
function cujNameInList(cujNames: string | string[]): string {
const cujNamesList = typeof cujNames === 'string' ? [cujNames] : cujNames;
return cujNamesList.length > 0
? cujNamesList.map((name) => `'L<${name}>','J<${name}>'`).join(',')
: '';
}
/**
* Generate the Track config for a multiple Jank CUJ slices
*
* @param {string | string[]} cujNames List of Jank CUJs to pin, default empty
* @returns Returns the track config for given CUJs
*/
function generateJankCujTrackConfig(cujNames: string | string[] = []) {
// This method expects the caller to have run JANK_CUJ_QUERY_PRECONDITIONS
// Not running the precondition query here to save time in case already run
const inList = cujNameInList(cujNames);
// The stdlib table exposes the raw (undecorated) CUJ name, so we can filter on
// it directly even though the selected `name` column is emoji-decorated.
const filterCuj = inList ? ` WHERE name IN (${inList})` : '';
return {
data: {
sqlSource: `${JANK_CUJ_QUERY}${filterCuj}`,
columns: JANK_COLUMNS,
},
rawColumns: JANK_COLUMNS,
};
}
// Maps the stdlib CUJ `state` column to a status indicator prefixed to each
// pinned slice name. The icons live here (a display concern) so that the SQL
// stays a plain query over the stdlib table.
const CUJ_STATUS_INDICATOR = {
canceled: '❌',
completed: '✅',
unknown: '❓',
};
const JANK_CUJ_QUERY = `
SELECT
CASE state
WHEN 'canceled' THEN ' ${CUJ_STATUS_INDICATOR.canceled} '
WHEN 'completed' THEN ' ${CUJ_STATUS_INDICATOR.completed} '
ELSE ' ${CUJ_STATUS_INDICATOR.unknown} '
END || name AS name,
process_name,
total_frames,
missed_app_frames,
missed_sf_frames,
layer_name,
ts,
dur,
track_id,
slice_id
FROM android_jank_cuj_slice_summary
`;
const JANK_COLUMNS = [
'name',
'process_name',
'total_frames',
'missed_app_frames',
'missed_sf_frames',
'layer_name',
'ts',
'dur',
'track_id',
'slice_id',
];
/**
* Adds the Debug Slice Track for given Jank CUJ name
*
* @param {Trace} ctx For properties and methods of trace viewer
* @param {string} trackName Display Name of the track
* @param {string | string[]} cujNames List of Jank CUJs to pin
* @returns Returns true if the track was added, false otherwise
*/
export async function addLatencyCUJDebugTrack(
ctx: Trace,
trackName: string,
cujNames?: string | string[],
) {
const latencyCujTrackConfig = generateLatencyCujTrackConfig(cujNames);
const result = await ctx.engine.query(latencyCujTrackConfig.data.sqlSource);
// Check if query produces any results to prevent pinning an empty track
if (result.numRows() !== 0) {
await addDebugSliceTrack({
trace: ctx,
title: trackName,
...latencyCujTrackConfig,
});
return true;
}
return false;
}
/**
* Generate the Track config for a multiple Latency CUJ slices
*
* @param {string | string[]} cujNames List of Latency CUJs to pin, default empty
* @returns Returns the track config for given CUJs
*/
function generateLatencyCujTrackConfig(cujNames: string | string[] = []) {
return generateCujTrackConfig(cujNames, LATENCY_CUJ_QUERY, LATENCY_COLUMNS);
}
const LATENCY_CUJ_QUERY = `
SELECT
CASE
WHEN
EXISTS(
SELECT 1
FROM slice AS cuj_state_marker
JOIN track marker_track
ON marker_track.id = cuj_state_marker.track_id
WHERE
cuj_state_marker.ts >= cuj.ts
AND cuj_state_marker.ts + cuj_state_marker.dur <= cuj.ts + cuj.dur
AND marker_track.name = cuj.name AND (
cuj_state_marker.name GLOB 'cancel'
OR cuj_state_marker.name GLOB 'timeout')
)
THEN ' ❌ '
ELSE ' ✅ '
END || cuj.name AS name,
cuj.dur / 1e6 as dur_ms,
cuj.ts,
cuj.dur,
cuj.track_id,
cuj.slice_id
FROM slice AS cuj
JOIN process_track AS pt
ON cuj.track_id = pt.id
WHERE cuj.name GLOB 'L<*>'
AND cuj.dur > 0
`;
const LATENCY_COLUMNS = ['name', 'dur_ms', 'ts', 'dur', 'track_id', 'slice_id'];
const ALL_BLOCKING_CALLS_OPTION = 'all blocking calls';
const BLOCKING_CALLS_PRECONDITIONS = `
INCLUDE PERFETTO MODULE android.cujs.sysui_cujs;
`;
const BLOCKING_CALLS_PROCESSES_QUERY = `
INCLUDE PERFETTO MODULE android.cujs.base;
SELECT DISTINCT process.name AS process_name
FROM process_track
JOIN slice ON slice.track_id = process_track.id
JOIN process USING (upid)
WHERE _is_cuj_slice!(slice)
AND slice.dur > 0
AND process.name IS NOT NULL
ORDER BY process.name
`;
function blockingCallNamesForProcessQuery(processName: string): string {
const escapedProcess = processName.replace(/'/g, "''");
return `
SELECT DISTINCT bc.name
FROM android_cuj_blocking_calls bc
JOIN android_jank_latency_cujs cuj USING (cuj_id, cuj_type, upid)
WHERE bc.utid = cuj.ui_thread
AND bc.process_name = '${escapedProcess}'
AND bc.name IS NOT NULL
ORDER BY bc.name
`;
}
function blockingCallsDuringCujsQuery(
processName: string,
blockingCallName?: string,
): string {
const escapedProcess = processName.replace(/'/g, "''");
const blockingCallFilter =
blockingCallName !== undefined
? `AND bc.name = '${blockingCallName.replace(/'/g, "''")}'`
: '';
return `
SELECT DISTINCT
bc.slice_id,
bc.name,
bc.ts,
bc.ts_end,
bc.dur,
bc.cuj_id,
bc.cuj_name,
bc.process_name,
bc.upid,
bc.utid,
'slice' AS table_name
FROM android_cuj_blocking_calls bc
JOIN android_jank_latency_cujs cuj USING (cuj_id, cuj_type, upid)
WHERE bc.utid = cuj.ui_thread
AND bc.process_name = '${escapedProcess}'
${blockingCallFilter}
`;
}
const BLOCKING_CALLS_DURING_CUJS_COLUMNS = [
'slice_id',
'name',
'ts',
'ts_end',
'dur',
'cuj_id',
'cuj_name',
'process_name',
'upid',
'utid',
'table_name',
];
/**
* Generate the Track config for a multiple CUJ slices
*
* @param {string | string[]} cujNames List of Latency CUJs to pin, default empty
* @param {string} cujQuery The query of the CUJ track
* @param {string} cujColumns SQL Columns for the CUJ track
* @returns Returns the track config for given CUJs
*/
function generateCujTrackConfig(
cujNames: string | string[] = [],
cujQuery: string,
cujColumns: string[],
) {
// This method expects the caller to have run JANK_CUJ_QUERY_PRECONDITIONS
// Not running the precondition query here to save time in case already run
const inList = cujNameInList(cujNames);
const filterCuj = inList ? ` AND cuj.name IN (${inList})` : '';
return {
data: {
sqlSource: `${cujQuery}${filterCuj}`,
columns: cujColumns,
},
rawColumns: cujColumns,
};
}
export default class implements PerfettoPlugin {
static readonly id = 'com.android.AndroidCujs';
static readonly dependencies = [QueryPagePlugin];
async onTraceLoad(ctx: Trace): Promise<void> {
ctx.commands.registerCommand({
id: 'com.android.PinJankCUJs',
name: 'Add track: Android jank CUJs',
callback: async () => {
await this.pinJankCujs(ctx);
},
});
ctx.commands.registerCommand({
id: 'com.android.ListJankCUJs',
name: 'Run query: Android jank CUJs',
callback: async () => {
await ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS);
ctx.plugins.getPlugin(QueryPagePlugin).addQueryResultsTab({
query: JANK_CUJ_QUERY,
title: 'Android Jank CUJs',
});
},
});
ctx.commands.registerCommand({
id: 'com.android.PinLatencyCUJs',
name: 'Add track: Android latency CUJs',
callback: async () => {
await this.pinLatencyCujs(ctx);
},
});
ctx.commands.registerCommand({
id: 'com.android.ListLatencyCUJs',
name: 'Run query: Android Latency CUJs',
callback: () =>
ctx.plugins.getPlugin(QueryPagePlugin).addQueryResultsTab({
query: LATENCY_CUJ_QUERY,
title: 'Android Latency CUJs',
}),
});
ctx.commands.registerCommand({
id: 'com.android.PinBlockingCalls',
name: 'Add track: Android Blocking calls during CUJs',
callback: async () => {
await this.pinBlockingCalls(ctx);
},
});
}
async pinJankCujs(ctx: Trace) {
await ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS);
await addJankCUJDebugTrack(ctx, 'Jank CUJs');
}
async pinLatencyCujs(ctx: Trace) {
await addDebugSliceTrack({
trace: ctx,
data: {
sqlSource: LATENCY_CUJ_QUERY,
columns: LATENCY_COLUMNS,
},
title: 'Latency CUJs',
});
}
async pinBlockingCalls(ctx: Trace) {
const result = await ctx.engine.query(BLOCKING_CALLS_PROCESSES_QUERY);
const processes: string[] = [];
const iter = result.iter({process_name: STR});
for (; iter.valid(); iter.next()) {
processes.push(iter.process_name);
}
if (processes.length === 0) {
ctx.omnibox.showStatusMessage('No blocking calls during CUJs found');
return;
}
// Preload stdlib tables in the background while the user picks a process.
const preloadPromise = ctx.engine.query(BLOCKING_CALLS_PRECONDITIONS);
const selectedProcess = await ctx.omnibox.prompt(
'Choose a process...',
processes,
);
if (selectedProcess === undefined) {
return;
}
await preloadPromise;
const namesResult = await ctx.engine.query(
blockingCallNamesForProcessQuery(selectedProcess),
);
const blockingCallNames: string[] = [];
const namesIter = namesResult.iter({name: STR});
for (; namesIter.valid(); namesIter.next()) {
blockingCallNames.push(namesIter.name);
}
if (blockingCallNames.length === 0) {
ctx.omnibox.showStatusMessage(
`No blocking calls during CUJs found for ${selectedProcess}`,
);
return;
}
const blockingCallOptions: string[] = [
ALL_BLOCKING_CALLS_OPTION,
...blockingCallNames,
];
const selectedBlockingCall = await ctx.omnibox.prompt(
'Choose a blocking call...',
blockingCallOptions,
);
if (selectedBlockingCall === undefined) {
return;
}
const filterBlockingCall =
selectedBlockingCall === ALL_BLOCKING_CALLS_OPTION
? undefined
: selectedBlockingCall;
const title =
filterBlockingCall === undefined
? `Blocking calls during CUJs (${selectedProcess})`
: `Blocking calls during CUJs (${selectedProcess} - ${filterBlockingCall})`;
await addDebugSliceTrack({
trace: ctx,
data: {
sqlSource: blockingCallsDuringCujsQuery(
selectedProcess,
filterBlockingCall,
),
columns: BLOCKING_CALLS_DURING_CUJS_COLUMNS,
},
title,
rawColumns: BLOCKING_CALLS_DURING_CUJS_COLUMNS,
});
}
}