1// Copyright (C) 2023 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {addDebugSliceTrack} from '../../components/tracks/debug_tracks'; 16import {Trace} from '../../public/trace'; 17import {PerfettoPlugin} from '../../public/plugin'; 18import {addQueryResultsTab} from '../../components/query_table/query_result_tab'; 19 20/** 21 * Adds the Debug Slice Track for given Jank CUJ name 22 * 23 * @param {Trace} ctx For properties and methods of trace viewer 24 * @param {string} trackName Display Name of the track 25 * @param {string | string[]} cujNames List of Jank CUJs to pin 26 * @returns Returns true if the track was added, false otherwise 27 */ 28export async function addJankCUJDebugTrack( 29 ctx: Trace, 30 trackName: string, 31 cujNames?: string | string[], 32) { 33 const jankCujTrackConfig = generateJankCujTrackConfig(cujNames); 34 const result = await ctx.engine.query(jankCujTrackConfig.data.sqlSource); 35 36 // Check if query produces any results to prevent pinning an empty track 37 if (result.numRows() !== 0) { 38 addDebugSliceTrack({trace: ctx, title: trackName, ...jankCujTrackConfig}); 39 return true; 40 } 41 return false; 42} 43 44const JANK_CUJ_QUERY_PRECONDITIONS = ` 45 SELECT RUN_METRIC('android/android_jank_cuj.sql'); 46 INCLUDE PERFETTO MODULE android.critical_blocking_calls; 47`; 48 49/** 50 * Generate the Track config for a multiple Jank CUJ slices 51 * 52 * @param {string | string[]} cujNames List of Jank CUJs to pin, default empty 53 * @returns Returns the track config for given CUJs 54 */ 55function generateJankCujTrackConfig(cujNames: string | string[] = []) { 56 // This method expects the caller to have run JANK_CUJ_QUERY_PRECONDITIONS 57 // Not running the precondition query here to save time in case already run 58 return generateCujTrackConfig(cujNames, JANK_CUJ_QUERY, JANK_COLUMNS); 59} 60 61const JANK_CUJ_QUERY = ` 62 SELECT 63 CASE 64 WHEN 65 EXISTS( 66 SELECT 1 67 FROM slice AS cuj_state_marker 68 JOIN track marker_track 69 ON marker_track.id = cuj_state_marker.track_id 70 WHERE 71 cuj_state_marker.ts >= cuj.ts 72 AND cuj_state_marker.ts + cuj_state_marker.dur <= cuj.ts + cuj.dur 73 AND 74 ( /* e.g. J<CUJ_NAME>#FT#cancel#0 this for backward compatibility */ 75 cuj_state_marker.name GLOB(cuj.name || '#FT#cancel*') 76 OR (marker_track.name = cuj.name AND cuj_state_marker.name GLOB 'FT#cancel*') 77 ) 78 ) 79 THEN ' ❌ ' 80 WHEN 81 EXISTS( 82 SELECT 1 83 FROM slice AS cuj_state_marker 84 JOIN track marker_track 85 ON marker_track.id = cuj_state_marker.track_id 86 WHERE 87 cuj_state_marker.ts >= cuj.ts 88 AND cuj_state_marker.ts + cuj_state_marker.dur <= cuj.ts + cuj.dur 89 AND 90 ( /* e.g. J<CUJ_NAME>#FT#end#0 this for backward compatibility */ 91 cuj_state_marker.name GLOB(cuj.name || '#FT#end*') 92 OR (marker_track.name = cuj.name AND cuj_state_marker.name GLOB 'FT#end*') 93 ) 94 ) 95 THEN ' ✅ ' 96 ELSE ' ❓ ' 97 END || cuj.name AS name, 98 total_frames, 99 missed_app_frames, 100 missed_sf_frames, 101 sf_callback_missed_frames, 102 hwui_callback_missed_frames, 103 cuj_layer.layer_name, 104 /* Boundaries table doesn't contain ts and dur when a CUJ didn't complete successfully. 105 In that case we still want to show that it was canceled, so let's take the slice timestamps. */ 106 CASE WHEN boundaries.ts IS NOT NULL THEN boundaries.ts ELSE cuj.ts END AS ts, 107 CASE WHEN boundaries.dur IS NOT NULL THEN boundaries.dur ELSE cuj.dur END AS dur, 108 cuj.track_id, 109 cuj.slice_id 110 FROM slice AS cuj 111 JOIN process_track AS pt ON cuj.track_id = pt.id 112 LEFT JOIN android_jank_cuj jc 113 ON pt.upid = jc.upid AND cuj.name = jc.cuj_slice_name AND cuj.ts = jc.ts 114 LEFT JOIN android_jank_cuj_main_thread_cuj_boundary boundaries using (cuj_id) 115 LEFT JOIN android_jank_cuj_layer_name cuj_layer USING (cuj_id) 116 LEFT JOIN android_jank_cuj_counter_metrics USING (cuj_id) 117 WHERE cuj.name GLOB 'J<*>' 118 AND cuj.dur > 0 119`; 120 121const JANK_COLUMNS = [ 122 'name', 123 'total_frames', 124 'missed_app_frames', 125 'missed_sf_frames', 126 'sf_callback_missed_frames', 127 'hwui_callback_missed_frames', 128 'layer_name', 129 'ts', 130 'dur', 131 'track_id', 132 'slice_id', 133]; 134 135/** 136 * Adds the Debug Slice Track for given Jank CUJ name 137 * 138 * @param {Trace} ctx For properties and methods of trace viewer 139 * @param {string} trackName Display Name of the track 140 * @param {string | string[]} cujNames List of Jank CUJs to pin 141 * @returns Returns true if the track was added, false otherwise 142 */ 143export async function addLatencyCUJDebugTrack( 144 ctx: Trace, 145 trackName: string, 146 cujNames?: string | string[], 147) { 148 const latencyCujTrackConfig = generateLatencyCujTrackConfig(cujNames); 149 const result = await ctx.engine.query(latencyCujTrackConfig.data.sqlSource); 150 151 // Check if query produces any results to prevent pinning an empty track 152 if (result.numRows() !== 0) { 153 addDebugSliceTrack({trace: ctx, title: trackName, ...latencyCujTrackConfig}); 154 return true; 155 } 156 return false; 157} 158 159/** 160 * Generate the Track config for a multiple Latency CUJ slices 161 * 162 * @param {string | string[]} cujNames List of Latency CUJs to pin, default empty 163 * @returns Returns the track config for given CUJs 164 */ 165function generateLatencyCujTrackConfig(cujNames: string | string[] = []) { 166 return generateCujTrackConfig(cujNames, LATENCY_CUJ_QUERY, LATENCY_COLUMNS); 167} 168 169const LATENCY_CUJ_QUERY = ` 170 SELECT 171 CASE 172 WHEN 173 EXISTS( 174 SELECT 1 175 FROM slice AS cuj_state_marker 176 JOIN track marker_track 177 ON marker_track.id = cuj_state_marker.track_id 178 WHERE 179 cuj_state_marker.ts >= cuj.ts 180 AND cuj_state_marker.ts + cuj_state_marker.dur <= cuj.ts + cuj.dur 181 AND marker_track.name = cuj.name AND ( 182 cuj_state_marker.name GLOB 'cancel' 183 OR cuj_state_marker.name GLOB 'timeout') 184 ) 185 THEN ' ❌ ' 186 ELSE ' ✅ ' 187 END || cuj.name AS name, 188 cuj.dur / 1e6 as dur_ms, 189 cuj.ts, 190 cuj.dur, 191 cuj.track_id, 192 cuj.slice_id 193 FROM slice AS cuj 194 JOIN process_track AS pt 195 ON cuj.track_id = pt.id 196 WHERE cuj.name GLOB 'L<*>' 197 AND cuj.dur > 0 198`; 199 200const LATENCY_COLUMNS = ['name', 'dur_ms', 'ts', 'dur', 'track_id', 'slice_id']; 201 202const BLOCKING_CALLS_DURING_CUJS_QUERY = ` 203 SELECT 204 s.id AS slice_id, 205 s.name, 206 max(s.ts, cuj.ts) AS ts, 207 min(s.ts + s.dur, cuj.ts_end) as ts_end, 208 min(s.ts + s.dur, cuj.ts_end) - max(s.ts, cuj.ts) AS dur, 209 cuj.cuj_id, 210 cuj.cuj_name, 211 s.process_name, 212 s.upid, 213 s.utid, 214 'slice' AS table_name 215 FROM _android_critical_blocking_calls s 216 JOIN android_jank_cuj cuj 217 -- only when there is an overlap 218 ON s.ts + s.dur > cuj.ts AND s.ts < cuj.ts_end 219 -- and are from the same process 220 AND s.upid = cuj.upid 221`; 222 223const BLOCKING_CALLS_DURING_CUJS_COLUMNS = [ 224 'slice_id', 225 'name', 226 'ts', 227 'cuj_ts', 228 'dur', 229 'cuj_id', 230 'cuj_name', 231 'process_name', 232 'upid', 233 'utid', 234 'table_name', 235]; 236 237/** 238 * Generate the Track config for a multiple CUJ slices 239 * 240 * @param {string | string[]} cujNames List of Latency CUJs to pin, default empty 241 * @param {string} cujQuery The query of the CUJ track 242 * @param {string} cujColumns SQL Columns for the CUJ track 243 * @returns Returns the track config for given CUJs 244 */ 245function generateCujTrackConfig(cujNames: string | string[] = [], cujQuery: string, cujColumns: string[]) { 246 // This method expects the caller to have run JANK_CUJ_QUERY_PRECONDITIONS 247 // Not running the precondition query here to save time in case already run 248 const cujNamesList = typeof cujNames === 'string' ? [cujNames] : cujNames; 249 const filterCuj = 250 cujNamesList?.length > 0 251 ? ` AND cuj.name IN (${cujNamesList 252 .map((name) => `'L<${name}>','J<${name}>'`) 253 .join(',')})` 254 : ''; 255 256 return { 257 data: { 258 sqlSource: `${cujQuery}${filterCuj}`, 259 columns: cujColumns, 260 }, 261 argColumns: cujColumns, 262 }; 263} 264 265export default class implements PerfettoPlugin { 266 static readonly id = 'dev.perfetto.AndroidCujs'; 267 async onTraceLoad(ctx: Trace): Promise<void> { 268 ctx.commands.registerCommand({ 269 id: 'dev.perfetto.AndroidCujs#PinJankCUJs', 270 name: 'Add track: Android jank CUJs', 271 callback: () => { 272 ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => { 273 addJankCUJDebugTrack(ctx, 'Jank CUJs'); 274 }); 275 }, 276 }); 277 278 ctx.commands.registerCommand({ 279 id: 'dev.perfetto.AndroidCujs#ListJankCUJs', 280 name: 'Run query: Android jank CUJs', 281 callback: () => { 282 ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => 283 addQueryResultsTab(ctx, { 284 query: JANK_CUJ_QUERY, 285 title: 'Android Jank CUJs', 286 }), 287 ); 288 }, 289 }); 290 291 ctx.commands.registerCommand({ 292 id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs', 293 name: 'Add track: Android latency CUJs', 294 callback: () => { 295 addDebugSliceTrack({ 296 trace: ctx, 297 data: { 298 sqlSource: LATENCY_CUJ_QUERY, 299 columns: LATENCY_COLUMNS, 300 }, 301 title: 'Latency CUJs', 302 }); 303 }, 304 }); 305 306 ctx.commands.registerCommand({ 307 id: 'dev.perfetto.AndroidCujs#ListLatencyCUJs', 308 name: 'Run query: Android Latency CUJs', 309 callback: () => 310 addQueryResultsTab(ctx, { 311 query: LATENCY_CUJ_QUERY, 312 title: 'Android Latency CUJs', 313 }), 314 }); 315 316 ctx.commands.registerCommand({ 317 id: 'dev.perfetto.AndroidCujs#PinBlockingCalls', 318 name: 'Add track: Android Blocking calls during CUJs', 319 callback: () => { 320 ctx.engine.query(JANK_CUJ_QUERY_PRECONDITIONS).then(() => 321 addDebugSliceTrack({ 322 trace: ctx, 323 data: { 324 sqlSource: BLOCKING_CALLS_DURING_CUJS_QUERY, 325 columns: BLOCKING_CALLS_DURING_CUJS_COLUMNS, 326 }, 327 title: 'Blocking calls during CUJs', 328 argColumns: BLOCKING_CALLS_DURING_CUJS_COLUMNS, 329 }), 330 ); 331 }, 332 }); 333 } 334} 335