| // 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 '../../public/debug_tracks'; |
| import {Trace} from '../../public/trace'; |
| import {PerfettoPlugin} from '../../public/plugin'; |
| import {addQueryResultsTab} from '../../public/lib/query_table/query_result_tab'; |
| |
| /** |
| * 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 |
| */ |
| export function addJankCUJDebugTrack( |
| ctx: Trace, |
| trackName: string, |
| cujNames?: string | string[], |
| ) { |
| const jankCujTrackConfig = generateJankCujTrackConfig(cujNames); |
| addDebugSliceTrack({trace: ctx, title: trackName, ...jankCujTrackConfig}); |
| } |
| |
| const JANK_CUJ_QUERY_PRECONDITIONS = ` |
| SELECT RUN_METRIC('android/android_jank_cuj.sql'); |
| INCLUDE PERFETTO MODULE android.critical_blocking_calls; |
| `; |
| |
| /** |
| * 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 jankCujQuery = JANK_CUJ_QUERY; |
| const jankCujColumns = JANK_COLUMNS; |
| const cujNamesList = typeof cujNames === 'string' ? [cujNames] : cujNames; |
| const filterCuj = |
| cujNamesList?.length > 0 |
| ? ` AND cuj.name IN (${cujNamesList |
| .map((name) => `'J<${name}>'`) |
| .join(',')})` |
| : ''; |
| |
| return { |
| data: { |
| sqlSource: `${jankCujQuery}${filterCuj}`, |
| columns: jankCujColumns, |
| }, |
| argColumns: jankCujColumns, |
| }; |
| } |
| |
| const JANK_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 |
| ( /* e.g. J<CUJ_NAME>#FT#cancel#0 this for backward compatibility */ |
| cuj_state_marker.name GLOB(cuj.name || '#FT#cancel*') |
| OR (marker_track.name = cuj.name AND cuj_state_marker.name GLOB 'FT#cancel*') |
| ) |
| ) |
| THEN ' ❌ ' |
| 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 |
| ( /* e.g. J<CUJ_NAME>#FT#end#0 this for backward compatibility */ |
| cuj_state_marker.name GLOB(cuj.name || '#FT#end*') |
| OR (marker_track.name = cuj.name AND cuj_state_marker.name GLOB 'FT#end*') |
| ) |
| ) |
| THEN ' ✅ ' |
| ELSE NULL |
| END || cuj.name AS name, |
| total_frames, |
| missed_app_frames, |
| missed_sf_frames, |
| sf_callback_missed_frames, |
| hwui_callback_missed_frames, |
| cuj_layer.layer_name, |
| /* Boundaries table doesn't contain ts and dur when a CUJ didn't complete successfully. |
| In that case we still want to show that it was canceled, so let's take the slice timestamps. */ |
| CASE WHEN boundaries.ts IS NOT NULL THEN boundaries.ts ELSE cuj.ts END AS ts, |
| CASE WHEN boundaries.dur IS NOT NULL THEN boundaries.dur ELSE cuj.dur END AS dur, |
| cuj.track_id, |
| cuj.slice_id |
| FROM slice AS cuj |
| JOIN process_track AS pt ON cuj.track_id = pt.id |
| LEFT JOIN android_jank_cuj jc |
| ON pt.upid = jc.upid AND cuj.name = jc.cuj_slice_name AND cuj.ts = jc.ts |
| LEFT JOIN android_jank_cuj_main_thread_cuj_boundary boundaries using (cuj_id) |
| LEFT JOIN android_jank_cuj_layer_name cuj_layer USING (cuj_id) |
| LEFT JOIN android_jank_cuj_counter_metrics USING (cuj_id) |
| WHERE cuj.name GLOB 'J<*>' |
| AND cuj.dur > 0 |
| `; |
| |
| const JANK_COLUMNS = [ |
| 'name', |
| 'total_frames', |
| 'missed_app_frames', |
| 'missed_sf_frames', |
| 'sf_callback_missed_frames', |
| 'hwui_callback_missed_frames', |
| 'layer_name', |
| 'ts', |
| 'dur', |
| 'track_id', |
| 'slice_id', |
| ]; |
| |
| 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 BLOCKING_CALLS_DURING_CUJS_QUERY = ` |
| SELECT |
| s.id AS slice_id, |
| s.name, |
| max(s.ts, cuj.ts) AS ts, |
| min(s.ts + s.dur, cuj.ts_end) as ts_end, |
| min(s.ts + s.dur, cuj.ts_end) - max(s.ts, cuj.ts) AS dur, |
| cuj.cuj_id, |
| cuj.cuj_name, |
| s.process_name, |
| s.upid, |
| s.utid, |
| 'slice' AS table_name |
| FROM _android_critical_blocking_calls s |
| JOIN android_jank_cuj cuj |
| -- only when there is an overlap |
| ON s.ts + s.dur > cuj.ts AND s.ts < cuj.ts_end |
| -- and are from the same process |
| AND s.upid = cuj.upid |
| `; |
| |
| const BLOCKING_CALLS_DURING_CUJS_COLUMNS = [ |
| 'slice_id', |
| 'name', |
| 'ts', |
| 'cuj_ts', |
| 'dur', |
| 'cuj_id', |
| 'cuj_name', |
| 'process_name', |
| 'upid', |
| 'utid', |
| 'table_name', |
| ]; |
| |
| export default class implements PerfettoPlugin { |
| static readonly id = 'dev.perfetto.AndroidCujs'; |
| async onTraceLoad(ctx: Trace): Promise<void> { |
| ctx.commands.registerCommand({ |
| id: 'dev.perfetto.AndroidCujs#PinJankCUJs', |
| name: 'Add track: Android jank CUJs', |
| callback: () => { |
| ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => { |
| addJankCUJDebugTrack(ctx, 'Jank CUJs'); |
| }); |
| }, |
| }); |
| |
| ctx.commands.registerCommand({ |
| id: 'dev.perfetto.AndroidCujs#ListJankCUJs', |
| name: 'Run query: Android jank CUJs', |
| callback: () => { |
| ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => |
| addQueryResultsTab(ctx, { |
| query: JANK_CUJ_QUERY, |
| title: 'Android Jank CUJs', |
| }), |
| ); |
| }, |
| }); |
| |
| ctx.commands.registerCommand({ |
| id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs', |
| name: 'Add track: Android latency CUJs', |
| callback: () => { |
| addDebugSliceTrack({ |
| trace: ctx, |
| data: { |
| sqlSource: LATENCY_CUJ_QUERY, |
| columns: LATENCY_COLUMNS, |
| }, |
| title: 'Latency CUJs', |
| }); |
| }, |
| }); |
| |
| ctx.commands.registerCommand({ |
| id: 'dev.perfetto.AndroidCujs#ListLatencyCUJs', |
| name: 'Run query: Android Latency CUJs', |
| callback: () => |
| addQueryResultsTab(ctx, { |
| query: LATENCY_CUJ_QUERY, |
| title: 'Android Latency CUJs', |
| }), |
| }); |
| |
| ctx.commands.registerCommand({ |
| id: 'dev.perfetto.AndroidCujs#PinBlockingCalls', |
| name: 'Add track: Android Blocking calls during CUJs', |
| callback: () => { |
| ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => |
| addDebugSliceTrack({ |
| trace: ctx, |
| data: { |
| sqlSource: BLOCKING_CALLS_DURING_CUJS_QUERY, |
| columns: BLOCKING_CALLS_DURING_CUJS_COLUMNS, |
| }, |
| title: 'Blocking calls during CUJs', |
| argColumns: BLOCKING_CALLS_DURING_CUJS_COLUMNS, |
| }), |
| ); |
| }, |
| }); |
| } |
| } |