• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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