• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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