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