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 { 19 Area, 20 PivotTableQuery, 21 PivotTableState, 22} from '../common/state'; 23import { 24 getSelectedTrackIds, 25} from '../controller/aggregation/slice_aggregation_controller'; 26 27import {globals} from './globals'; 28import { 29 Aggregation, 30 TableColumn, 31} from './pivot_table_types'; 32 33export interface Table { 34 name: string; 35 columns: string[]; 36} 37 38export const sliceTable = { 39 name: 'slice', 40 columns: ['type', 'ts', 'dur', 'category', 'name', 'depth'], 41}; 42 43// Columns of `slice` table available for aggregation. 44export const sliceAggregationColumns = [ 45 'ts', 46 'dur', 47 'depth', 48 'thread_ts', 49 'thread_dur', 50 'thread_instruction_count', 51 'thread_instruction_delta', 52]; 53 54// List of available tables to query, used to populate selectors of pivot 55// columns in the UI. 56export const tables: Table[] = [ 57 sliceTable, 58 { 59 name: 'process', 60 columns: [ 61 'type', 62 'pid', 63 'name', 64 'parent_upid', 65 'uid', 66 'android_appid', 67 'cmdline', 68 ], 69 }, 70 {name: 'thread', columns: ['type', 'name', 'tid', 'upid', 'is_main_thread']}, 71 {name: 'thread_track', columns: ['type', 'name', 'utid']}, 72]; 73 74// Queried "table column" is either: 75// 1. A real one, represented as object with table and column name. 76// 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like 77// `select sum(1), name from slice group by name`. 78 79export interface RegularColumn { 80 kind: 'regular'; 81 table: string; 82 column: string; 83} 84 85export interface ArgumentColumn { 86 kind: 'argument'; 87 argument: string; 88} 89 90// Exception thrown by query generator in case incoming parameters are not 91// suitable in order to build a correct query; these are caught by the UI and 92// displayed to the user. 93export class QueryGeneratorError extends Error {} 94 95// Internal column name for different rollover levels of aggregate columns. 96function aggregationAlias(aggregationIndex: number): string { 97 return `agg_${aggregationIndex}`; 98} 99 100export function areaFilter(area: Area): string { 101 return ` 102 ts + dur > ${area.start} 103 and ts < ${area.end} 104 and track_id in (${getSelectedTrackIds(area).join(', ')}) 105 `; 106} 107 108export function expression(column: TableColumn): string { 109 switch (column.kind) { 110 case 'regular': 111 return `${column.table}.${column.column}`; 112 case 'argument': 113 return extractArgumentExpression(column.argument, 'slice'); 114 } 115} 116 117function aggregationExpression(aggregation: Aggregation): string { 118 if (aggregation.aggregationFunction === 'COUNT') { 119 return 'COUNT()'; 120 } 121 return `${aggregation.aggregationFunction}(${ 122 expression(aggregation.column)})`; 123} 124 125export function extractArgumentExpression(argument: string, table?: string) { 126 const prefix = table === undefined ? '' : `${table}.`; 127 return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`; 128} 129 130export function aggregationIndex(pivotColumns: number, aggregationNo: number) { 131 return pivotColumns + aggregationNo; 132} 133 134export function generateQueryFromState(state: PivotTableState): 135 PivotTableQuery { 136 if (state.selectionArea === undefined) { 137 throw new QueryGeneratorError('Should not be called without area'); 138 } 139 140 const sliceTableAggregations = [...state.selectedAggregations.values()]; 141 if (sliceTableAggregations.length === 0) { 142 throw new QueryGeneratorError('No aggregations selected'); 143 } 144 145 const pivots = state.selectedPivots; 146 147 const aggregations = sliceTableAggregations.map( 148 (agg, index) => 149 `${aggregationExpression(agg)} as ${aggregationAlias(index)}`); 150 const countIndex = aggregations.length; 151 // Extra count aggregation, needed in order to compute combined averages. 152 aggregations.push('COUNT() as hidden_count'); 153 154 const renderedPivots = pivots.map(expression); 155 const sortClauses: string[] = []; 156 for (let i = 0; i < sliceTableAggregations.length; i++) { 157 const sortDirection = sliceTableAggregations[i].sortDirection; 158 if (sortDirection !== undefined) { 159 sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`); 160 } 161 } 162 163 const joins = ` 164 left join thread_track on thread_track.id = slice.track_id 165 left join thread using (utid) 166 left join process using (upid) 167 `; 168 169 const whereClause = state.constrainToArea ? 170 `where ${areaFilter(globals.state.areas[state.selectionArea.areaId])}` : 171 ''; 172 const text = ` 173 select 174 ${renderedPivots.concat(aggregations).join(',\n')} 175 from slice 176 ${pivots.length > 0 ? joins : ''} 177 ${whereClause} 178 group by ${renderedPivots.join(', ')} 179 ${sortClauses.length > 0 ? ('order by ' + sortClauses.join(', ')) : ''} 180 `; 181 182 return { 183 text, 184 metadata: { 185 pivotColumns: pivots, 186 aggregationColumns: sliceTableAggregations, 187 countIndex, 188 }, 189 }; 190} 191