xref: /aosp_15_r20/external/perfetto/ui/src/core/pivot_table_query_generator.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1// Copyright (C) 2022 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 {
17  PivotTableQuery,
18  PivotTableState,
19  Aggregation,
20  TableColumn,
21} from './pivot_table_types';
22import {AreaSelection} from '../public/selection';
23import {SLICE_TRACK_KIND} from '../public/track_kinds';
24
25interface Table {
26  name: string;
27  displayName: string;
28  columns: string[];
29}
30
31const sliceTable = {
32  name: '_slice_with_thread_and_process_info',
33  displayName: 'slice',
34  columns: [
35    'type',
36    'ts',
37    'dur',
38    'category',
39    'name',
40    'depth',
41    'pid',
42    'process_name',
43    'tid',
44    'thread_name',
45  ],
46};
47
48// Columns of `slice` table available for aggregation.
49export const sliceAggregationColumns = [
50  'ts',
51  'dur',
52  'depth',
53  'thread_ts',
54  'thread_dur',
55  'thread_instruction_count',
56  'thread_instruction_delta',
57];
58
59// List of available tables to query, used to populate selectors of pivot
60// columns in the UI.
61export const tables: Table[] = [sliceTable];
62
63// Exception thrown by query generator in case incoming parameters are not
64// suitable in order to build a correct query; these are caught by the UI and
65// displayed to the user.
66export class QueryGeneratorError extends Error {}
67
68// Internal column name for different rollover levels of aggregate columns.
69function aggregationAlias(aggregationIndex: number): string {
70  return `agg_${aggregationIndex}`;
71}
72
73export function areaFilters(
74  area: AreaSelection,
75): {op: (cols: string[]) => string; columns: string[]}[] {
76  return [
77    {
78      op: (cols) => `${cols[0]} + ${cols[1]} > ${area.start}`,
79      columns: ['ts', 'dur'],
80    },
81    {op: (cols) => `${cols[0]} < ${area.end}`, columns: ['ts']},
82    {
83      op: (cols) =>
84        `${cols[0]} in (${getSelectedTrackSqlIds(area).join(', ')})`,
85      columns: ['track_id'],
86    },
87  ];
88}
89
90function expression(column: TableColumn): string {
91  switch (column.kind) {
92    case 'regular':
93      return `${column.table}.${column.column}`;
94    case 'argument':
95      return extractArgumentExpression(column.argument, sliceTable.name);
96  }
97}
98
99function aggregationExpression(aggregation: Aggregation): string {
100  if (aggregation.aggregationFunction === 'COUNT') {
101    return 'COUNT()';
102  }
103  return `${aggregation.aggregationFunction}(${expression(
104    aggregation.column,
105  )})`;
106}
107
108function extractArgumentExpression(argument: string, table?: string) {
109  const prefix = table === undefined ? '' : `${table}.`;
110  return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`;
111}
112
113export function aggregationIndex(pivotColumns: number, aggregationNo: number) {
114  return pivotColumns + aggregationNo;
115}
116
117export function generateQueryFromState(
118  state: PivotTableState,
119): PivotTableQuery {
120  if (state.selectionArea === undefined) {
121    throw new QueryGeneratorError('Should not be called without area');
122  }
123
124  const sliceTableAggregations = [...state.selectedAggregations.values()];
125  if (sliceTableAggregations.length === 0) {
126    throw new QueryGeneratorError('No aggregations selected');
127  }
128
129  const pivots = state.selectedPivots;
130
131  const aggregations = sliceTableAggregations.map(
132    (agg, index) =>
133      `${aggregationExpression(agg)} as ${aggregationAlias(index)}`,
134  );
135  const countIndex = aggregations.length;
136  // Extra count aggregation, needed in order to compute combined averages.
137  aggregations.push('COUNT() as hidden_count');
138
139  const renderedPivots = pivots.map(expression);
140  const sortClauses: string[] = [];
141  for (let i = 0; i < sliceTableAggregations.length; i++) {
142    const sortDirection = sliceTableAggregations[i].sortDirection;
143    if (sortDirection !== undefined) {
144      sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`);
145    }
146  }
147
148  const whereClause = state.constrainToArea
149    ? `where ${areaFilters(state.selectionArea)
150        .map((f) => f.op(f.columns))
151        .join(' and\n')}`
152    : '';
153  const text = `
154    INCLUDE PERFETTO MODULE slices.slices;
155
156    select
157      ${renderedPivots.concat(aggregations).join(',\n')}
158    from ${sliceTable.name}
159    ${whereClause}
160    group by ${renderedPivots.join(', ')}
161    ${sortClauses.length > 0 ? 'order by ' + sortClauses.join(', ') : ''}
162  `;
163
164  return {
165    text,
166    metadata: {
167      pivotColumns: pivots,
168      aggregationColumns: sliceTableAggregations,
169      countIndex,
170    },
171  };
172}
173
174function getSelectedTrackSqlIds(area: AreaSelection): number[] {
175  const selectedTrackKeys: number[] = [];
176  for (const trackInfo of area.tracks) {
177    if (trackInfo?.tags?.kind === SLICE_TRACK_KIND) {
178      trackInfo.tags.trackIds &&
179        selectedTrackKeys.push(...trackInfo.tags.trackIds);
180    }
181  }
182  return selectedTrackKeys;
183}
184