xref: /aosp_15_r20/external/perfetto/ui/src/core/pivot_table_types.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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 {SortDirection} from '../base/comparison_utils';
16import {AreaSelection} from '../public/selection';
17import {ColumnType} from '../trace_processor/query_result';
18
19// Auxiliary metadata needed to parse the query result, as well as to render it
20// correctly. Generated together with the text of query and passed without the
21// change to the query response.
22
23export interface PivotTableQueryMetadata {
24  pivotColumns: TableColumn[];
25  aggregationColumns: Aggregation[];
26  countIndex: number;
27}
28// Everything that's necessary to run the query for pivot table
29
30export interface PivotTableQuery {
31  text: string;
32  metadata: PivotTableQueryMetadata;
33}
34// Pivot table query result
35
36export interface PivotTableResult {
37  // Hierarchical pivot structure on top of rows
38  tree: PivotTree;
39  // Copy of the query metadata from the request, bundled up with the query
40  // result to ensure the correct rendering.
41  metadata: PivotTableQueryMetadata;
42}
43// Input parameters to check whether the pivot table needs to be re-queried.
44
45export interface PivotTableState {
46  // Currently selected area, if null, pivot table is not going to be visible.
47  selectionArea?: AreaSelection;
48
49  // Query response
50  queryResult: PivotTableResult | undefined;
51
52  // Selected pivots for tables other than slice.
53  // Because of the query generation, pivoting happens first on non-slice
54  // pivots; therefore, those can't be put after slice pivots. In order to
55  // maintain the separation more clearly, slice and non-slice pivots are
56  // located in separate arrays.
57  selectedPivots: TableColumn[];
58
59  // Selected aggregation columns. Stored same way as pivots.
60  selectedAggregations: Aggregation[];
61
62  // Whether the pivot table results should be constrained to the selected area.
63  constrainToArea: boolean;
64}
65
66// Node in the hierarchical pivot tree. Only leaf nodes contain data from the
67// query result.
68
69export interface PivotTree {
70  // Whether the node should be collapsed in the UI, false by default and can
71  // be toggled with the button.
72  isCollapsed: boolean;
73
74  // Non-empty only in internal nodes.
75  children: Map<ColumnType, PivotTree>;
76  aggregates: ColumnType[];
77
78  // Non-empty only in leaf nodes.
79  rows: ColumnType[][];
80}
81
82export type AggregationFunction = 'COUNT' | 'SUM' | 'MIN' | 'MAX' | 'AVG';
83// Queried "table column" is either:
84// 1. A real one, represented as object with table and column name.
85// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like
86// `select sum(1), name from slice group by name`.
87
88export interface RegularColumn {
89  kind: 'regular';
90  table: string;
91  column: string;
92}
93
94export interface ArgumentColumn {
95  kind: 'argument';
96  argument: string;
97}
98
99export type TableColumn = RegularColumn | ArgumentColumn;
100
101export function tableColumnEquals(t1: TableColumn, t2: TableColumn): boolean {
102  switch (t1.kind) {
103    case 'argument': {
104      return t2.kind === 'argument' && t1.argument === t2.argument;
105    }
106    case 'regular': {
107      return (
108        t2.kind === 'regular' &&
109        t1.table === t2.table &&
110        t1.column === t2.column
111      );
112    }
113  }
114}
115
116export function toggleEnabled<T>(
117  compare: (fst: T, snd: T) => boolean,
118  arr: T[],
119  column: T,
120  enabled: boolean,
121): void {
122  if (enabled && arr.find((value) => compare(column, value)) === undefined) {
123    arr.push(column);
124  }
125  if (!enabled) {
126    const index = arr.findIndex((value) => compare(column, value));
127    if (index !== -1) {
128      arr.splice(index, 1);
129    }
130  }
131}
132
133export interface Aggregation {
134  aggregationFunction: AggregationFunction;
135  column: TableColumn;
136
137  // If the aggregation is sorted, the field contains a sorting direction.
138  sortDirection?: SortDirection;
139}
140
141export function aggregationEquals(agg1: Aggregation, agg2: Aggregation) {
142  return new EqualsBuilder(agg1, agg2)
143    .comparePrimitive((agg) => agg.aggregationFunction)
144    .compare(tableColumnEquals, (agg) => agg.column)
145    .equals();
146}
147
148// Used to convert TableColumn to a string in order to store it in a Map, as
149// ES6 does not support compound Set/Map keys. This function should only be used
150// for interning keys, and does not have any requirements beyond different
151// TableColumn objects mapping to different strings.
152
153export function columnKey(tableColumn: TableColumn): string {
154  switch (tableColumn.kind) {
155    case 'argument': {
156      return `argument:${tableColumn.argument}`;
157    }
158    case 'regular': {
159      return `${tableColumn.table}.${tableColumn.column}`;
160    }
161  }
162}
163
164export function aggregationKey(aggregation: Aggregation): string {
165  return `${aggregation.aggregationFunction}:${columnKey(aggregation.column)}`;
166}
167
168export const COUNT_AGGREGATION: Aggregation = {
169  aggregationFunction: 'COUNT',
170  // Exact column is ignored for count aggregation because it does not matter
171  // what to count, use empty strings.
172  column: {kind: 'regular', table: '', column: ''},
173};
174
175// Simple builder-style class to implement object equality more succinctly.
176class EqualsBuilder<T> {
177  result = true;
178  first: T;
179  second: T;
180
181  constructor(first: T, second: T) {
182    this.first = first;
183    this.second = second;
184  }
185
186  comparePrimitive(getter: (arg: T) => string | number): EqualsBuilder<T> {
187    if (this.result) {
188      this.result = getter(this.first) === getter(this.second);
189    }
190    return this;
191  }
192
193  compare<S>(
194    comparator: (first: S, second: S) => boolean,
195    getter: (arg: T) => S,
196  ): EqualsBuilder<T> {
197    if (this.result) {
198      this.result = comparator(getter(this.first), getter(this.second));
199    }
200    return this;
201  }
202
203  equals(): boolean {
204    return this.result;
205  }
206}
207