1/* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17import {sqliteString} from '../base/string_utils'; 18import {Area, PivotTableQuery, PivotTableState} from '../common/state'; 19import {getSelectedTrackKeys} from '../controller/aggregation/slice_aggregation_controller'; 20 21import {Aggregation, TableColumn} from './pivot_table_types'; 22import {SqlTables} from './sql_table/well_known_tables'; 23 24export interface Table { 25 name: string; 26 displayName: string; 27 columns: string[]; 28} 29 30export const sliceTable = { 31 name: SqlTables.slice.name, 32 displayName: 'slice', 33 columns: [ 34 'type', 35 'ts', 36 'dur', 37 'category', 38 'name', 39 'depth', 40 'pid', 41 'process_name', 42 'tid', 43 'thread_name', 44 ], 45}; 46 47// Columns of `slice` table available for aggregation. 48export const sliceAggregationColumns = [ 49 'ts', 50 'dur', 51 'depth', 52 'thread_ts', 53 'thread_dur', 54 'thread_instruction_count', 55 'thread_instruction_delta', 56]; 57 58// List of available tables to query, used to populate selectors of pivot 59// columns in the UI. 60export const tables: Table[] = [sliceTable]; 61 62// Queried "table column" is either: 63// 1. A real one, represented as object with table and column name. 64// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like 65// `select sum(1), name from slice group by name`. 66 67export interface RegularColumn { 68 kind: 'regular'; 69 table: string; 70 column: string; 71} 72 73export interface ArgumentColumn { 74 kind: 'argument'; 75 argument: string; 76} 77 78// Exception thrown by query generator in case incoming parameters are not 79// suitable in order to build a correct query; these are caught by the UI and 80// displayed to the user. 81export class QueryGeneratorError extends Error {} 82 83// Internal column name for different rollover levels of aggregate columns. 84function aggregationAlias(aggregationIndex: number): string { 85 return `agg_${aggregationIndex}`; 86} 87 88export function areaFilters(area: Area): string[] { 89 return [ 90 `ts + dur > ${area.start}`, 91 `ts < ${area.end}`, 92 `track_id in (${getSelectedTrackKeys(area).join(', ')})`, 93 ]; 94} 95 96export function expression(column: TableColumn): string { 97 switch (column.kind) { 98 case 'regular': 99 return `${column.table}.${column.column}`; 100 case 'argument': 101 return extractArgumentExpression(column.argument, SqlTables.slice.name); 102 } 103} 104 105function aggregationExpression(aggregation: Aggregation): string { 106 if (aggregation.aggregationFunction === 'COUNT') { 107 return 'COUNT()'; 108 } 109 return `${aggregation.aggregationFunction}(${expression( 110 aggregation.column, 111 )})`; 112} 113 114export function extractArgumentExpression(argument: string, table?: string) { 115 const prefix = table === undefined ? '' : `${table}.`; 116 return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`; 117} 118 119export function aggregationIndex(pivotColumns: number, aggregationNo: number) { 120 return pivotColumns + aggregationNo; 121} 122 123export function generateQueryFromState( 124 state: PivotTableState, 125): PivotTableQuery { 126 if (state.selectionArea === undefined) { 127 throw new QueryGeneratorError('Should not be called without area'); 128 } 129 130 const sliceTableAggregations = [...state.selectedAggregations.values()]; 131 if (sliceTableAggregations.length === 0) { 132 throw new QueryGeneratorError('No aggregations selected'); 133 } 134 135 const pivots = state.selectedPivots; 136 137 const aggregations = sliceTableAggregations.map( 138 (agg, index) => 139 `${aggregationExpression(agg)} as ${aggregationAlias(index)}`, 140 ); 141 const countIndex = aggregations.length; 142 // Extra count aggregation, needed in order to compute combined averages. 143 aggregations.push('COUNT() as hidden_count'); 144 145 const renderedPivots = pivots.map(expression); 146 const sortClauses: string[] = []; 147 for (let i = 0; i < sliceTableAggregations.length; i++) { 148 const sortDirection = sliceTableAggregations[i].sortDirection; 149 if (sortDirection !== undefined) { 150 sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`); 151 } 152 } 153 154 const whereClause = state.constrainToArea 155 ? `where ${areaFilters(state.selectionArea).join(' and\n')}` 156 : ''; 157 const text = ` 158 INCLUDE PERFETTO MODULE slices.slices; 159 160 select 161 ${renderedPivots.concat(aggregations).join(',\n')} 162 from ${SqlTables.slice.name} 163 ${whereClause} 164 group by ${renderedPivots.join(', ')} 165 ${sortClauses.length > 0 ? 'order by ' + sortClauses.join(', ') : ''} 166 `; 167 168 return { 169 text, 170 metadata: { 171 pivotColumns: pivots, 172 aggregationColumns: sliceTableAggregations, 173 countIndex, 174 }, 175 }; 176} 177