1// Copyright (C) 2024 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 {
16  expandProcessName,
17  CujScopedMetricData,
18  MetricHandler,
19  JankType,
20} from './metricUtils';
21import {NUM} from '../../../trace_processor/query_result';
22import {Trace} from '../../../public/trace';
23
24// TODO(primiano): make deps check stricter, we shouldn't allow plugins to
25// depend on each other.
26import {focusOnSlice} from '../../dev.perfetto.AndroidCujs/trackUtils';
27import {addDebugSliceTrack} from '../../../components/tracks/debug_tracks';
28
29const ENABLE_FOCUS_ON_FIRST_JANK = true;
30
31class PinCujScopedJank implements MetricHandler {
32  /**
33   * Matches metric key & return parsed data if successful.
34   *
35   * @param {string} metricKey The metric key to match.
36   * @returns {CujScopedMetricData | undefined} Parsed data or undefined if no match.
37   */
38  public match(metricKey: string): CujScopedMetricData | undefined {
39    const matcher =
40      /perfetto_cuj_(?<process>.*)-(?<cujName>.*)-.*-missed_(?<jankType>frames|sf_frames|app_frames)/;
41    const match = matcher.exec(metricKey);
42    if (!match?.groups) {
43      return undefined;
44    }
45    const metricData: CujScopedMetricData = {
46      process: expandProcessName(match.groups.process),
47      cujName: match.groups.cujName,
48      jankType: match.groups.jankType as JankType,
49    };
50    return metricData;
51    1;
52  }
53
54  /**
55   * Adds the debug tracks for cuj Scoped jank metrics.
56   *
57   * @param {CujScopedMetricData} metricData Parsed metric data for the cuj scoped jank
58   * @param {Trace} ctx PluginContextTrace for trace related properties and methods
59   * @returns {void} Adds one track for Jank CUJ slice and one for Janky CUJ frames
60   */
61  public async addMetricTrack(metricData: CujScopedMetricData, ctx: Trace) {
62    // TODO: b/349502258 - Refactor to single API
63    const {tableName, ...config} = await this.cujScopedTrackConfig(
64      metricData,
65      ctx,
66    );
67    addDebugSliceTrack({trace: ctx, ...config});
68    if (ENABLE_FOCUS_ON_FIRST_JANK) {
69      await this.focusOnFirstJank(ctx, tableName);
70    }
71  }
72
73  private async cujScopedTrackConfig(
74    metricData: CujScopedMetricData,
75    ctx: Trace,
76  ) {
77    let jankTypeFilter;
78    let jankTypeDisplayName = 'all';
79    if (metricData.jankType?.includes('app')) {
80      jankTypeFilter = ' AND app_missed > 0';
81      jankTypeDisplayName = 'app';
82    } else if (metricData.jankType?.includes('sf')) {
83      jankTypeFilter = ' AND sf_missed > 0';
84      jankTypeDisplayName = 'sf';
85    }
86    const cuj = metricData.cujName;
87    const processName = metricData.process;
88
89    const tableWithJankyFramesName = `_janky_frames_during_cuj_from_metric_key_${Math.floor(Math.random() * 1_000_000)}`;
90
91    const createJankyCujFrameTable = `
92      CREATE OR REPLACE PERFETTO TABLE ${tableWithJankyFramesName} AS
93      SELECT
94        f.vsync as id,
95        f.ts AS ts,
96        f.dur as dur
97      FROM android_jank_cuj_frame f LEFT JOIN android_jank_cuj cuj USING (cuj_id)
98      WHERE cuj.process_name = "${processName}"
99      AND cuj_name = "${cuj}" ${jankTypeFilter}
100    `;
101
102    await ctx.engine.query(createJankyCujFrameTable);
103
104    const jankyFramesDuringCujQuery = `
105      SELECT id, ts, dur
106      FROM ${tableWithJankyFramesName}
107    `;
108
109    const trackName = jankTypeDisplayName + ' missed frames in ' + processName;
110
111    const cujScopedJankSlice = {
112      data: {
113        sqlSource: jankyFramesDuringCujQuery,
114        columns: ['id', 'ts', 'dur'],
115      },
116      columns: {ts: 'ts', dur: 'dur', name: 'id'},
117      argColumns: ['id', 'ts', 'dur'],
118      trackName,
119    };
120
121    return {
122      ...cujScopedJankSlice,
123      tableName: tableWithJankyFramesName,
124    };
125  }
126
127  private async focusOnFirstJank(ctx: Trace, tableWithJankyFramesName: string) {
128    const queryForFirstJankyFrame = `
129      SELECT id as slice_id, track_id
130      FROM actual_frame_timeline_slice
131      WHERE name = cast_string!(
132        (SELECT id FROM ${tableWithJankyFramesName} LIMIT 1)
133      );
134    `;
135    const queryResult = await ctx.engine.query(queryForFirstJankyFrame);
136    if (queryResult.numRows() === 0) {
137      return;
138    }
139    const row = queryResult.firstRow({
140      slice_id: NUM,
141      track_id: NUM,
142    });
143    focusOnSlice(ctx, row.slice_id);
144  }
145}
146
147export const pinCujScopedJankInstance = new PinCujScopedJank();
148