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