/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import {sqliteString} from '../base/string_utils'; import { Area, PivotTableQuery, PivotTableState, } from '../common/state'; import { getSelectedTrackIds, } from '../controller/aggregation/slice_aggregation_controller'; import {globals} from './globals'; import { Aggregation, TableColumn, } from './pivot_table_types'; export interface Table { name: string; columns: string[]; } export const sliceTable = { name: 'slice', columns: ['type', 'ts', 'dur', 'category', 'name', 'depth'], }; // Columns of `slice` table available for aggregation. export const sliceAggregationColumns = [ 'ts', 'dur', 'depth', 'thread_ts', 'thread_dur', 'thread_instruction_count', 'thread_instruction_delta', ]; // List of available tables to query, used to populate selectors of pivot // columns in the UI. export const tables: Table[] = [ sliceTable, { name: 'process', columns: [ 'type', 'pid', 'name', 'parent_upid', 'uid', 'android_appid', 'cmdline', ], }, {name: 'thread', columns: ['type', 'name', 'tid', 'upid', 'is_main_thread']}, {name: 'thread_track', columns: ['type', 'name', 'utid']}, ]; // Queried "table column" is either: // 1. A real one, represented as object with table and column name. // 2. Pseudo-column 'count' that's rendered as '1' in SQL to use in queries like // `select sum(1), name from slice group by name`. export interface RegularColumn { kind: 'regular'; table: string; column: string; } export interface ArgumentColumn { kind: 'argument'; argument: string; } // Exception thrown by query generator in case incoming parameters are not // suitable in order to build a correct query; these are caught by the UI and // displayed to the user. export class QueryGeneratorError extends Error {} // Internal column name for different rollover levels of aggregate columns. function aggregationAlias(aggregationIndex: number): string { return `agg_${aggregationIndex}`; } export function areaFilter(area: Area): string { return ` ts + dur > ${area.start} and ts < ${area.end} and track_id in (${getSelectedTrackIds(area).join(', ')}) `; } export function expression(column: TableColumn): string { switch (column.kind) { case 'regular': return `${column.table}.${column.column}`; case 'argument': return extractArgumentExpression(column.argument, 'slice'); } } function aggregationExpression(aggregation: Aggregation): string { if (aggregation.aggregationFunction === 'COUNT') { return 'COUNT()'; } return `${aggregation.aggregationFunction}(${ expression(aggregation.column)})`; } export function extractArgumentExpression(argument: string, table?: string) { const prefix = table === undefined ? '' : `${table}.`; return `extract_arg(${prefix}arg_set_id, ${sqliteString(argument)})`; } export function aggregationIndex(pivotColumns: number, aggregationNo: number) { return pivotColumns + aggregationNo; } export function generateQueryFromState(state: PivotTableState): PivotTableQuery { if (state.selectionArea === undefined) { throw new QueryGeneratorError('Should not be called without area'); } const sliceTableAggregations = [...state.selectedAggregations.values()]; if (sliceTableAggregations.length === 0) { throw new QueryGeneratorError('No aggregations selected'); } const pivots = state.selectedPivots; const aggregations = sliceTableAggregations.map( (agg, index) => `${aggregationExpression(agg)} as ${aggregationAlias(index)}`); const countIndex = aggregations.length; // Extra count aggregation, needed in order to compute combined averages. aggregations.push('COUNT() as hidden_count'); const renderedPivots = pivots.map(expression); const sortClauses: string[] = []; for (let i = 0; i < sliceTableAggregations.length; i++) { const sortDirection = sliceTableAggregations[i].sortDirection; if (sortDirection !== undefined) { sortClauses.push(`${aggregationAlias(i)} ${sortDirection}`); } } const joins = ` left join thread_track on thread_track.id = slice.track_id left join thread using (utid) left join process using (upid) `; const whereClause = state.constrainToArea ? `where ${areaFilter(globals.state.areas[state.selectionArea.areaId])}` : ''; const text = ` select ${renderedPivots.concat(aggregations).join(',\n')} from slice ${pivots.length > 0 ? joins : ''} ${whereClause} group by ${renderedPivots.join(', ')} ${sortClauses.length > 0 ? ('order by ' + sortClauses.join(', ')) : ''} `; return { text, metadata: { pivotColumns: pivots, aggregationColumns: sliceTableAggregations, countIndex, }, }; }