blob: c92243d6e0bb41b9550d30a7ba32eac006b49a3b [file] [log] [blame]
/*
* 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 {GenericSet} from '../base/generic_set';
import {Area, PivotTableReduxQuery} from '../common/state';
import {toNs} from '../common/time';
import {
getSelectedTrackIds
} from '../controller/aggregation/slice_aggregation_controller';
export interface Table {
name: string;
columns: string[];
}
export const sliceTable = {
name: 'slice',
columns: ['type', 'ts', 'dur', 'category', 'name']
};
// Columns of `slice` table available for aggregation.
export const sliceAggregationColumns = ['ts', 'dur', 'depth'];
// Columns of `thread_slice` table available for aggregation.
export const threadSliceAggregationColumns = [
'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']},
];
// Pair of table name and column name.
export type TableColumn = [string, string];
export function createColumnSet(): GenericSet<TableColumn> {
return new GenericSet((column: TableColumn) => `${column[0]}.${column[1]}`);
}
// 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, rolloverLevel: number): string {
return `agg_${aggregationIndex}_level_${rolloverLevel}`;
}
export function areaFilter(area: Area): string {
return `
ts > ${toNs(area.startSec)}
and ts < ${toNs(area.endSec)}
and track_id in (${getSelectedTrackIds(area).join(', ')})
`;
}
function generateInnerQuery(
pivots: string[],
aggregations: string[],
table: string,
includeTrack: boolean,
area: Area,
constrainToArea: boolean): string {
const pivotColumns = pivots.concat(includeTrack ? ['track_id'] : []);
const aggregationColumns: string[] = [];
for (let i = 0; i < aggregations.length; i++) {
const agg = aggregations[i];
aggregationColumns.push(`SUM(${agg}) as ${aggregationAlias(i, 0)}`);
}
// The condition is inverted because flipped order of literals makes JS
// formatter insert huge amounts of whitespace for no good reason.
return `
select
${pivotColumns.concat(aggregationColumns).join(',\n')}
from ${table}
${(constrainToArea ? `where ${areaFilter(area)}` : '')}
group by ${pivotColumns.join(', ')}
`;
}
function computeSliceTableAggregations(
selectedAggregations: GenericSet<TableColumn>):
{tableName: string, flatAggregations: string[]} {
let hasThreadSliceColumn = false;
const allColumns = [];
for (const [table, column] of selectedAggregations.values()) {
if (table === 'thread_slice') {
hasThreadSliceColumn = true;
}
allColumns.push(column);
}
return {
// If any aggregation column from `thread_slice` is present, it's going to
// be the base table for the pivot table query. Otherwise, `slice` is used.
// This later is going to be controllable by a UI element.
tableName: hasThreadSliceColumn ? 'thread_slice' : 'slice',
flatAggregations: allColumns
};
}
// Every aggregation in the request is contained in the result in (number of
// pivots + 1) times for each rollover level. This helper function returs an
// index of the necessary column in the response.
export function aggregationIndex(
pivotColumns: number, aggregationNo: number, depth: number) {
return pivotColumns + aggregationNo * (pivotColumns + 1) +
(pivotColumns - depth);
}
export function generateQuery(
selectedPivots: GenericSet<TableColumn>,
selectedAggregations: GenericSet<TableColumn>,
area: Area,
constrainToArea: boolean): PivotTableReduxQuery {
const sliceTableAggregations =
computeSliceTableAggregations(selectedAggregations);
const slicePivots: string[] = [];
const nonSlicePivots: string[] = [];
if (sliceTableAggregations.flatAggregations.length === 0) {
throw new QueryGeneratorError('No aggregations selected');
}
for (const [table, pivot] of selectedPivots.values()) {
if (table === 'slice' || table === 'thread_slice') {
slicePivots.push(pivot);
} else {
nonSlicePivots.push(`${table}.${pivot}`);
}
}
if (slicePivots.length === 0 && nonSlicePivots.length === 0) {
throw new QueryGeneratorError('No pivots selected');
}
const outerAggregations = [];
const prefixedSlicePivots = slicePivots.map(p => `preaggregated.${p}`);
const totalPivotsArray = nonSlicePivots.concat(prefixedSlicePivots);
for (let i = 0; i < sliceTableAggregations.flatAggregations.length; i++) {
const agg = `preaggregated.${aggregationAlias(i, 0)}`;
outerAggregations.push(`SUM(${agg}) as ${aggregationAlias(i, 0)}`);
for (let level = 1; level < totalPivotsArray.length; level++) {
// Peculiar form "SUM(SUM(agg)) over (partition by columns)" here means
// following: inner SUM(agg) is an aggregation that is going to collapse
// tracks with the same pivot values, which is going to be post-aggregated
// by the set of columns by outer **window** SUM function.
// Need to use complicated query syntax can be avoided by having yet
// another nested subquery computing only aggregation values with window
// functions in the wrapper, but the generation code is going to be more
// complex; so complexity of the query is traded for complexity of the
// query generator.
outerAggregations.push(`SUM(SUM(${agg})) over (partition by ${
totalPivotsArray.slice(0, totalPivotsArray.length - level)
.join(', ')}) as ${aggregationAlias(i, level)}`);
}
outerAggregations.push(`SUM(SUM(${agg})) over () as ${
aggregationAlias(i, totalPivotsArray.length)}`);
}
const joins = `
join thread_track on thread_track.id = preaggregated.track_id
join thread using (utid)
join process using (upid)
`;
const text = `
select
${
nonSlicePivots.concat(prefixedSlicePivots, outerAggregations).join(',\n')}
from (
${
generateInnerQuery(
slicePivots,
sliceTableAggregations.flatAggregations,
sliceTableAggregations.tableName,
nonSlicePivots.length > 0,
area,
constrainToArea)}
) preaggregated
${nonSlicePivots.length > 0 ? joins : ''}
group by ${nonSlicePivots.concat(prefixedSlicePivots).join(', ')}
`;
return {
text,
metadata: {
tableName: sliceTableAggregations.tableName,
pivotColumns: nonSlicePivots.concat(slicePivots.map(
column => `${sliceTableAggregations.tableName}.${column}`)),
aggregationColumns: sliceTableAggregations.flatAggregations.map(
agg => `SUM(${sliceTableAggregations.tableName}.${agg})`)
}
};
}