• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2025 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 {sqliteString} from '../../base/string_utils';
16import {uuidv4} from '../../base/uuid';
17import {DatasetSliceTrack} from '../../components/tracks/dataset_slice_track';
18import {
19  createQueryCounterTrack,
20  SqlTableCounterTrack,
21} from '../../components/tracks/query_counter_track';
22import {createQuerySliceTrack} from '../../components/tracks/query_slice_track';
23import {Trace} from '../../public/trace';
24import {TrackNode} from '../../public/workspace';
25import {ColumnType, NUM_NULL} from '../../trace_processor/query_result';
26
27/**
28 * Aggregation types for the BreakdownTracks.
29 * These aggregations will be displayed in a set of counter tracks.
30 */
31export enum BreakdownTrackAggType {
32  COUNT = 'COUNT',
33  MAX = 'MAX',
34  SUM = 'SUM',
35}
36
37/**
38 * Breakdown Tracks will always be shown first as
39 * a counter track with the aggregation.
40 *
41 * Slice and pivot tracks will be slice tracks.
42 */
43enum BreakdownTrackType {
44  AGGREGATION,
45  SLICE,
46  PIVOT,
47}
48
49interface BreakdownTrackSqlInfo {
50  /**
51   * Table columns of interest.Tracks are always filtered/evaluated
52   * based on the ordering within this array.
53   */
54  columns: string[];
55  /**
56   * Table name for the data to be queried from.
57   */
58  tableName: string;
59  /**
60   * This is the value that should be displayed in the
61   * aggregation counter track.
62   */
63  valueCol?: string;
64  /**
65   * Timestamp column name. Usually this is `ts` in a table but
66   * it could also be something else such as `client_ts`, etc.
67   */
68  tsCol?: string;
69  /**
70   * Duration column name. Usually this is `dur` in a table
71   * but it could also be something like `client_dur`, etc.
72   */
73  durCol?: string;
74  /**
75   * Optional join values for values tables that should
76   * be joined to the one specified in `tableName`.
77   *
78   * Usage:
79   *  pivots: {
80        columns: [`(aidl_name || ' blocked on ' || reason)`],
81        tableName: 'android_binder_txns',
82        tsCol: 'ts',
83        durCol: 'dur',
84        joins: [
85          {
86            joinTableName: 'android_binder_client_server_breakdown',
87            joinColumns: ['binder_txn_id'],
88          },
89        ],
90      },
91   */
92  joins?: BreakdownTrackJoins[]; // To be used for joining with other tables
93}
94
95interface BreakdownTrackJoins {
96  joinTableName: string;
97  joinColumns: string[];
98}
99
100export interface BreakdownTrackProps {
101  trace: Trace;
102  /**
103   * This title will display only at the top most (root) track.
104   * Best practice is to include the aggregation type.
105   *
106   * Ex: Max RSS Usage or Binder Txn Counts
107   */
108  trackTitle: string;
109  /**
110   * This is the aggregation type used for the counter tracks
111   * (described below). For example: COUNT, SUM, MAX, etc.
112   */
113  aggregationType: BreakdownTrackAggType;
114  /**
115   * Specified aggregation values are then used to populate
116   * a set of counter tracks where values for each counter track
117   * will be filtered to the values specified in the columns array.
118   */
119  aggregation: BreakdownTrackSqlInfo;
120  /**
121   * The Perfetto modules that should be included in order
122   * to query the specified tables in the aggregation, slice, or
123   * pivot tracks.
124   */
125  modules?: string[];
126  /**
127   * Data that should be displayed as slices after the aggregation
128   * tracks are shown. The aggregation tracks will always display first
129   * and data specified in this property will be displayed as a child
130   * slice track.
131   */
132  slice?: BreakdownTrackSqlInfo;
133  /**
134   * Data to be pivoted. This is simlar to the debug pivot tracks where
135   * the values of each column will be displayed in a separate track with
136   * the corresponding slices.
137   */
138  pivots?: BreakdownTrackSqlInfo;
139}
140
141interface Filter {
142  columnName: string;
143  value?: string;
144}
145
146export class BreakdownTracks {
147  private readonly props;
148  private uri: string;
149  private modulesClause: string;
150  private sliceJoinClause?: string;
151  private pivotJoinClause?: string;
152
153  constructor(props: BreakdownTrackProps) {
154    this.props = props;
155    this.uri = `/breakdown_tracks_${this.props.aggregation.tableName}`;
156
157    this.modulesClause = props.modules
158      ? props.modules.map((m) => `INCLUDE PERFETTO MODULE ${m};`).join('\n')
159      : '';
160
161    if (this.props.aggregationType === BreakdownTrackAggType.COUNT) {
162      this.modulesClause += `\nINCLUDE PERFETTO MODULE intervals.overlap;`;
163    }
164
165    if (this.props.slice?.joins !== undefined) {
166      this.sliceJoinClause = this.getJoinClause(this.props.slice.joins);
167    }
168
169    if (this.props.pivots?.joins !== undefined) {
170      this.pivotJoinClause = this.getJoinClause(this.props.pivots.joins);
171    }
172  }
173
174  private getAggregationQuery(filtersClause: string) {
175    if (this.props.aggregationType === BreakdownTrackAggType.COUNT) {
176      return `
177        intervals_overlap_count
178        !((
179            SELECT ${this.props.aggregation.tsCol} AS ts,
180            ${this.props.aggregation.durCol} AS dur
181            FROM ${this.props.aggregation.tableName}
182            ${filtersClause}
183        ), ts, dur)
184      `;
185    }
186
187    return `
188      SELECT
189      ${this.props.aggregation.tsCol} AS ts,
190      ${this.props.aggregation.durCol} dur,
191      ${this.props.aggregationType}(${this.props.aggregation.valueCol}) AS value
192      FROM _ui_dev_perfetto_breakdown_tracks_intervals
193      ${filtersClause}
194      GROUP BY ${this.props.aggregation.tsCol}
195    `;
196  }
197
198  // TODO: Modify this to use self_interval_intersect when it is available.
199  private getIntervals() {
200    const {tsCol, durCol, valueCol, columns, tableName} =
201      this.props.aggregation;
202
203    return `
204      CREATE OR REPLACE PERFETTO TABLE _ui_dev_perfetto_breakdown_tracks_intervals
205      AS
206      WITH
207        x AS (
208          SELECT overlap.*,
209          lead(${tsCol}) OVER (PARTITION BY group_name ORDER BY ${tsCol}) - ${tsCol} AS dur
210          FROM intervals_overlap_count_by_group!(${tableName}, ${tsCol}, ${durCol}, ${columns[columns.length - 1]}) overlap
211        )
212      SELECT x.ts, x.dur,
213        ${columns.map((col) => `${tableName}.${col}`).join(', ')},
214        ${tableName}.${valueCol}
215      FROM x
216      JOIN ${tableName}
217        ON
218          ${tableName}.${columns[columns.length - 1]} = x.group_name
219          AND _ui_dev_perfetto_breakdown_tracks_is_spans_overlapping(x.ts, x.ts + x.dur, ${tableName}.${tsCol}, ${tableName}.${tsCol} + ${tableName}.${durCol});
220    `;
221  }
222
223  private getJoinClause(joins: BreakdownTrackJoins[]) {
224    return joins
225      .map(
226        ({joinTableName, joinColumns}) =>
227          `JOIN ${joinTableName} USING(${joinColumns.join(', ')})`,
228      )
229      .join('\n');
230  }
231
232  async createTracks() {
233    if (this.modulesClause !== '') {
234      await this.props.trace.engine.query(this.modulesClause);
235    }
236
237    if (this.props.aggregationType !== BreakdownTrackAggType.COUNT) {
238      await this.props.trace.engine.query(`
239        CREATE OR REPLACE PERFETTO FUNCTION _ui_dev_perfetto_breakdown_tracks_is_spans_overlapping(
240          ts1 LONG,
241          ts_end1 LONG,
242          ts2 LONG,
243          ts_end2 LONG)
244        RETURNS BOOL
245        AS
246        SELECT (IIF($ts1 < $ts2, $ts2, $ts1) < IIF($ts_end1 < $ts_end2, $ts_end1, $ts_end2));
247
248        ${this.getIntervals()}
249      `);
250    }
251
252    const rootTrackNode = await this.createCounterTrackNode(
253      `${this.props.trackTitle}`,
254      [],
255    );
256
257    this.createBreakdownHierarchy(
258      [],
259      rootTrackNode,
260      this.props.aggregation,
261      0,
262      BreakdownTrackType.AGGREGATION,
263    );
264
265    return rootTrackNode;
266  }
267
268  private async createBreakdownHierarchy(
269    filters: Filter[],
270    parent: TrackNode,
271    sqlInfo: BreakdownTrackSqlInfo,
272    colIndex: number,
273    trackType: BreakdownTrackType,
274  ) {
275    const {columns} = sqlInfo;
276    if (colIndex === columns.length) {
277      return;
278    }
279
280    const currColName = columns[colIndex];
281    const joinClause = this.getTrackSpecificJoinClause(trackType);
282
283    const query = `
284      ${this.modulesClause}
285
286      SELECT DISTINCT ${currColName}
287      FROM ${this.props.aggregation.tableName}
288      ${joinClause !== undefined ? joinClause : ''}
289      ${filters.length > 0 ? `WHERE ${buildFilterSqlClause(filters)}` : ''}
290    `;
291
292    const res = await this.props.trace.engine.query(query);
293
294    for (const iter = res.iter({}); iter.valid(); iter.next()) {
295      const colRaw = iter.get(currColName);
296      const colValue = colRaw === null ? 'NULL' : colRaw.toString();
297      const title = colValue;
298
299      const newFilters = [
300        ...filters,
301        {
302          columnName: currColName,
303          value: colValue,
304        },
305      ];
306
307      let currNode;
308      let nextTrackType = trackType;
309      let nextColIndex = colIndex + 1;
310      let nextSqlInfo = sqlInfo;
311
312      switch (trackType) {
313        case BreakdownTrackType.AGGREGATION:
314          currNode = await this.createCounterTrackNode(title, newFilters);
315          if (this.props.slice && colIndex === columns.length - 1) {
316            nextTrackType = BreakdownTrackType.SLICE;
317            nextColIndex = 0;
318            nextSqlInfo = this.props.slice;
319          }
320          break;
321        case BreakdownTrackType.SLICE:
322          currNode = await this.createSliceTrackNode(
323            title,
324            newFilters,
325            colIndex,
326            sqlInfo,
327            trackType,
328          );
329          if (this.props.pivots && colIndex === columns.length - 1) {
330            nextTrackType = BreakdownTrackType.PIVOT;
331            nextColIndex = 0;
332            nextSqlInfo = this.props.pivots;
333          }
334          break;
335        default:
336          currNode = await this.createSliceTrackNode(
337            title,
338            newFilters,
339            colIndex,
340            sqlInfo,
341            trackType,
342          );
343      }
344
345      parent.addChildInOrder(currNode);
346      this.createBreakdownHierarchy(
347        newFilters,
348        currNode,
349        nextSqlInfo,
350        nextColIndex,
351        nextTrackType,
352      );
353    }
354  }
355
356  private getTrackSpecificJoinClause(trackType: BreakdownTrackType) {
357    switch (trackType) {
358      case BreakdownTrackType.SLICE:
359        return this.sliceJoinClause;
360      case BreakdownTrackType.PIVOT:
361        return this.pivotJoinClause;
362      default:
363        return undefined;
364    }
365  }
366
367  private async createSliceTrackNode(
368    title: string,
369    newFilters: Filter[],
370    columnIndex: number,
371    sqlInfo: BreakdownTrackSqlInfo,
372    trackType: BreakdownTrackType,
373  ) {
374    let joinClause = '';
375
376    if (this.sliceJoinClause && trackType === BreakdownTrackType.SLICE) {
377      joinClause = this.sliceJoinClause;
378    } else if (this.pivotJoinClause && trackType === BreakdownTrackType.PIVOT) {
379      joinClause = this.pivotJoinClause;
380    }
381
382    return await this.createTrackNode(
383      title,
384      newFilters,
385      (uri: string, filtersClause: string) => {
386        return createQuerySliceTrack({
387          trace: this.props.trace,
388          uri,
389          data: {
390            sqlSource: `
391            SELECT ${sqlInfo.tsCol} AS ts,
392              ${sqlInfo.durCol} AS dur,
393              ${sqlInfo.columns[columnIndex]} AS name
394            FROM ${this.props.aggregation.tableName}
395            ${joinClause}
396            ${filtersClause}
397          `,
398            columns: ['ts', 'dur', 'name'],
399          },
400        });
401      },
402    );
403  }
404
405  private async getCounterTrackSortOrder(filtersClause: string) {
406    const aggregationQuery = this.getAggregationQuery(filtersClause);
407    const result = await this.props.trace.engine.query(`
408      SELECT MAX(value) as max_value FROM (${aggregationQuery})
409    `);
410    const maxValue = result.firstRow({max_value: NUM_NULL}).max_value;
411    return maxValue === null ? 0 : maxValue;
412  }
413
414  private async createCounterTrackNode(title: string, newFilters: Filter[]) {
415    return await this.createTrackNode(
416      title,
417      newFilters,
418      (uri: string, filtersClause: string) => {
419        return createQueryCounterTrack({
420          trace: this.props.trace,
421          uri,
422          data: {
423            sqlSource: `
424              SELECT ts, value FROM
425              (${this.getAggregationQuery(filtersClause)})
426            `,
427          },
428          columns: {
429            ts: 'ts',
430            value: 'value',
431          },
432        });
433      },
434      (filterClause) => this.getCounterTrackSortOrder(filterClause),
435    );
436  }
437
438  private async createTrackNode(
439    title: string,
440    filters: Filter[],
441    createTrack: (
442      uri: string,
443      filtersClause: string,
444    ) => Promise<
445      | SqlTableCounterTrack
446      | DatasetSliceTrack<{
447          id: number;
448          ts: bigint;
449          dur: bigint;
450          name: string;
451        }>
452    >,
453    getSortOrder?: (filterClause: string) => Promise<number>,
454  ) {
455    const filtersClause =
456      filters.length > 0 ? `\nWHERE ${buildFilterSqlClause(filters)}` : '';
457    const uri = `${this.uri}_${uuidv4()}`;
458
459    const track = await createTrack(uri, filtersClause);
460
461    this.props.trace.tracks.registerTrack({
462      uri,
463      title,
464      track,
465    });
466
467    const sortOrder = await getSortOrder?.(filtersClause);
468
469    return new TrackNode({
470      title,
471      uri,
472      sortOrder: sortOrder !== undefined ? -sortOrder : undefined,
473    });
474  }
475}
476
477function buildFilterSqlClause(filters: Filter[]) {
478  return filters.map((filter) => `${filterToSql(filter)}`).join(' AND ');
479}
480
481function filterToSql(filter: Filter) {
482  const {columnName, value} = filter;
483
484  const filterValue: ColumnType | undefined = toSqlValue(value);
485  return `${columnName} = ${filterValue === undefined ? '' : filterValue}`;
486}
487
488function toSqlValue(input: string | undefined): string | number | bigint {
489  if (input === undefined || !input.trim()) {
490    return '';
491  }
492
493  const num = Number(input);
494  if (!isNaN(num) && String(num) == input.trim()) {
495    return num;
496  }
497
498  try {
499    return BigInt(input);
500  } catch {
501    return sqliteString(input);
502  }
503}
504