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 {GenericSet} from '../base/generic_set'; 18import {Area, PivotTableReduxQuery} from '../common/state'; 19import {toNs} from '../common/time'; 20import { 21 getSelectedTrackIds 22} from '../controller/aggregation/slice_aggregation_controller'; 23 24export interface Table { 25 name: string; 26 columns: string[]; 27} 28 29export const sliceTable = { 30 name: 'slice', 31 columns: ['type', 'ts', 'dur', 'category', 'name'] 32}; 33 34// Columns of `slice` table available for aggregation. 35export const sliceAggregationColumns = ['ts', 'dur', 'depth']; 36 37// Columns of `thread_slice` table available for aggregation. 38export const threadSliceAggregationColumns = [ 39 'thread_ts', 40 'thread_dur', 41 'thread_instruction_count', 42 'thread_instruction_delta' 43]; 44 45// List of available tables to query, used to populate selectors of pivot 46// columns in the UI. 47export const tables: Table[] = [ 48 sliceTable, 49 { 50 name: 'process', 51 columns: [ 52 'type', 53 'pid', 54 'name', 55 'parent_upid', 56 'uid', 57 'android_appid', 58 'cmdline' 59 ] 60 }, 61 {name: 'thread', columns: ['type', 'name', 'tid', 'upid', 'is_main_thread']}, 62 {name: 'thread_track', columns: ['type', 'name', 'utid']}, 63]; 64 65// Pair of table name and column name. 66export type TableColumn = [string, string]; 67 68export function createColumnSet(): GenericSet<TableColumn> { 69 return new GenericSet((column: TableColumn) => `${column[0]}.${column[1]}`); 70} 71 72// Exception thrown by query generator in case incoming parameters are not 73// suitable in order to build a correct query; these are caught by the UI and 74// displayed to the user. 75export class QueryGeneratorError extends Error {} 76 77// Internal column name for different rollover levels of aggregate columns. 78function aggregationAlias( 79 aggregationIndex: number, rolloverLevel: number): string { 80 return `agg_${aggregationIndex}_level_${rolloverLevel}`; 81} 82 83export function areaFilter(area: Area): string { 84 return ` 85 ts > ${toNs(area.startSec)} 86 and ts < ${toNs(area.endSec)} 87 and track_id in (${getSelectedTrackIds(area).join(', ')}) 88 `; 89} 90 91function generateInnerQuery( 92 pivots: string[], 93 aggregations: string[], 94 table: string, 95 includeTrack: boolean, 96 area: Area, 97 constrainToArea: boolean): string { 98 const pivotColumns = pivots.concat(includeTrack ? ['track_id'] : []); 99 const aggregationColumns: string[] = []; 100 101 for (let i = 0; i < aggregations.length; i++) { 102 const agg = aggregations[i]; 103 aggregationColumns.push(`SUM(${agg}) as ${aggregationAlias(i, 0)}`); 104 } 105 106 // The condition is inverted because flipped order of literals makes JS 107 // formatter insert huge amounts of whitespace for no good reason. 108 return ` 109 select 110 ${pivotColumns.concat(aggregationColumns).join(',\n')} 111 from ${table} 112 ${(constrainToArea ? `where ${areaFilter(area)}` : '')} 113 group by ${pivotColumns.join(', ')} 114 `; 115} 116 117function computeSliceTableAggregations( 118 selectedAggregations: GenericSet<TableColumn>): 119 {tableName: string, flatAggregations: string[]} { 120 let hasThreadSliceColumn = false; 121 const allColumns = []; 122 for (const [table, column] of selectedAggregations.values()) { 123 if (table === 'thread_slice') { 124 hasThreadSliceColumn = true; 125 } 126 allColumns.push(column); 127 } 128 129 return { 130 // If any aggregation column from `thread_slice` is present, it's going to 131 // be the base table for the pivot table query. Otherwise, `slice` is used. 132 // This later is going to be controllable by a UI element. 133 tableName: hasThreadSliceColumn ? 'thread_slice' : 'slice', 134 flatAggregations: allColumns 135 }; 136} 137 138// Every aggregation in the request is contained in the result in (number of 139// pivots + 1) times for each rollover level. This helper function returs an 140// index of the necessary column in the response. 141export function aggregationIndex( 142 pivotColumns: number, aggregationNo: number, depth: number) { 143 return pivotColumns + aggregationNo * (pivotColumns + 1) + 144 (pivotColumns - depth); 145} 146 147export function generateQuery( 148 selectedPivots: GenericSet<TableColumn>, 149 selectedAggregations: GenericSet<TableColumn>, 150 area: Area, 151 constrainToArea: boolean): PivotTableReduxQuery { 152 const sliceTableAggregations = 153 computeSliceTableAggregations(selectedAggregations); 154 const slicePivots: string[] = []; 155 const nonSlicePivots: string[] = []; 156 157 if (sliceTableAggregations.flatAggregations.length === 0) { 158 throw new QueryGeneratorError('No aggregations selected'); 159 } 160 161 for (const [table, pivot] of selectedPivots.values()) { 162 if (table === 'slice' || table === 'thread_slice') { 163 slicePivots.push(pivot); 164 } else { 165 nonSlicePivots.push(`${table}.${pivot}`); 166 } 167 } 168 169 if (slicePivots.length === 0 && nonSlicePivots.length === 0) { 170 throw new QueryGeneratorError('No pivots selected'); 171 } 172 173 const outerAggregations = []; 174 const prefixedSlicePivots = slicePivots.map(p => `preaggregated.${p}`); 175 const totalPivotsArray = nonSlicePivots.concat(prefixedSlicePivots); 176 for (let i = 0; i < sliceTableAggregations.flatAggregations.length; i++) { 177 const agg = `preaggregated.${aggregationAlias(i, 0)}`; 178 outerAggregations.push(`SUM(${agg}) as ${aggregationAlias(i, 0)}`); 179 180 for (let level = 1; level < totalPivotsArray.length; level++) { 181 // Peculiar form "SUM(SUM(agg)) over (partition by columns)" here means 182 // following: inner SUM(agg) is an aggregation that is going to collapse 183 // tracks with the same pivot values, which is going to be post-aggregated 184 // by the set of columns by outer **window** SUM function. 185 186 // Need to use complicated query syntax can be avoided by having yet 187 // another nested subquery computing only aggregation values with window 188 // functions in the wrapper, but the generation code is going to be more 189 // complex; so complexity of the query is traded for complexity of the 190 // query generator. 191 outerAggregations.push(`SUM(SUM(${agg})) over (partition by ${ 192 totalPivotsArray.slice(0, totalPivotsArray.length - level) 193 .join(', ')}) as ${aggregationAlias(i, level)}`); 194 } 195 196 outerAggregations.push(`SUM(SUM(${agg})) over () as ${ 197 aggregationAlias(i, totalPivotsArray.length)}`); 198 } 199 200 const joins = ` 201 join thread_track on thread_track.id = preaggregated.track_id 202 join thread using (utid) 203 join process using (upid) 204 `; 205 206 const text = ` 207 select 208 ${ 209 nonSlicePivots.concat(prefixedSlicePivots, outerAggregations).join(',\n')} 210 from ( 211 ${ 212 generateInnerQuery( 213 slicePivots, 214 sliceTableAggregations.flatAggregations, 215 sliceTableAggregations.tableName, 216 nonSlicePivots.length > 0, 217 area, 218 constrainToArea)} 219 ) preaggregated 220 ${nonSlicePivots.length > 0 ? joins : ''} 221 group by ${nonSlicePivots.concat(prefixedSlicePivots).join(', ')} 222 `; 223 224 return { 225 text, 226 metadata: { 227 tableName: sliceTableAggregations.tableName, 228 pivotColumns: nonSlicePivots.concat(slicePivots.map( 229 column => `${sliceTableAggregations.tableName}.${column}`)), 230 aggregationColumns: sliceTableAggregations.flatAggregations.map( 231 agg => `SUM(${sliceTableAggregations.tableName}.${agg})`) 232 } 233 }; 234} 235