1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {Actions} from '../../common/actions'; 16import { 17 AggregateData, 18 Column, 19 ColumnDef, 20 ThreadStateExtra, 21} from '../../common/aggregation_data'; 22import {Engine} from '../../common/engine'; 23import { 24 SLICE_AGGREGATION_PIVOT_TABLE_ID 25} from '../../common/pivot_table_common'; 26import {NUM} from '../../common/query_result'; 27import {Area, Sorting} from '../../common/state'; 28import {publishAggregateData} from '../../frontend/publish'; 29import {AreaSelectionHandler} from '../area_selection_handler'; 30import {Controller} from '../controller'; 31import {globals} from '../globals'; 32 33export interface AggregationControllerArgs { 34 engine: Engine; 35 kind: string; 36} 37 38function isStringColumn(column: Column): boolean { 39 return column.kind === 'STRING' || column.kind === 'STATE'; 40} 41 42export abstract class AggregationController extends Controller<'main'> { 43 readonly kind: string; 44 private areaSelectionHandler: AreaSelectionHandler; 45 private previousSorting?: Sorting; 46 private requestingData = false; 47 private queuedRequest = false; 48 49 abstract createAggregateView(engine: Engine, area: Area): Promise<boolean>; 50 51 abstract getExtra(engine: Engine, area: Area): Promise<ThreadStateExtra|void>; 52 53 abstract getTabName(): string; 54 abstract getDefaultSorting(): Sorting; 55 abstract getColumnDefinitions(): ColumnDef[]; 56 57 constructor(private args: AggregationControllerArgs) { 58 super('main'); 59 this.kind = this.args.kind; 60 this.areaSelectionHandler = new AreaSelectionHandler(); 61 } 62 63 run() { 64 const selection = globals.state.currentSelection; 65 if (selection === null || selection.kind !== 'AREA') { 66 globals.dispatch(Actions.deletePivotTable( 67 {pivotTableId: SLICE_AGGREGATION_PIVOT_TABLE_ID})); 68 publishAggregateData({ 69 data: { 70 tabName: this.getTabName(), 71 columns: [], 72 strings: [], 73 columnSums: [], 74 }, 75 kind: this.args.kind 76 }); 77 return; 78 } 79 const aggregatePreferences = 80 globals.state.aggregatePreferences[this.args.kind]; 81 82 const sortingChanged = aggregatePreferences && 83 this.previousSorting !== aggregatePreferences.sorting; 84 const [hasAreaChanged, area] = this.areaSelectionHandler.getAreaChange(); 85 if ((!hasAreaChanged && !sortingChanged) || !area) return; 86 87 if (this.requestingData) { 88 this.queuedRequest = true; 89 } else { 90 this.requestingData = true; 91 if (sortingChanged) this.previousSorting = aggregatePreferences.sorting; 92 this.getAggregateData(area, hasAreaChanged) 93 .then(data => publishAggregateData({data, kind: this.args.kind})) 94 .finally(() => { 95 this.requestingData = false; 96 if (this.queuedRequest) { 97 this.queuedRequest = false; 98 this.run(); 99 } 100 }); 101 } 102 } 103 104 async getAggregateData(area: Area, areaChanged: boolean): 105 Promise<AggregateData> { 106 if (areaChanged) { 107 const viewExists = await this.createAggregateView(this.args.engine, area); 108 if (!viewExists) { 109 return { 110 tabName: this.getTabName(), 111 columns: [], 112 strings: [], 113 columnSums: [], 114 }; 115 } 116 } 117 118 const defs = this.getColumnDefinitions(); 119 const colIds = defs.map(col => col.columnId); 120 const pref = globals.state.aggregatePreferences[this.kind]; 121 let sorting = `${this.getDefaultSorting().column} ${ 122 this.getDefaultSorting().direction}`; 123 if (pref && pref.sorting) { 124 sorting = `${pref.sorting.column} ${pref.sorting.direction}`; 125 } 126 const query = `select ${colIds} from ${this.kind} order by ${sorting}`; 127 const result = await this.args.engine.query(query); 128 129 const numRows = result.numRows(); 130 const columns = defs.map(def => this.columnFromColumnDef(def, numRows)); 131 const columnSums = await Promise.all(defs.map(def => this.getSum(def))); 132 const extraData = await this.getExtra(this.args.engine, area); 133 const extra = extraData ? extraData : undefined; 134 const data: AggregateData = 135 {tabName: this.getTabName(), columns, columnSums, strings: [], extra}; 136 137 const stringIndexes = new Map<string, number>(); 138 function internString(str: string) { 139 let idx = stringIndexes.get(str); 140 if (idx !== undefined) return idx; 141 idx = data.strings.length; 142 data.strings.push(str); 143 stringIndexes.set(str, idx); 144 return idx; 145 } 146 147 const it = result.iter({}); 148 for (let i = 0; it.valid(); it.next(), ++i) { 149 for (const column of data.columns) { 150 const item = it.get(column.columnId); 151 if (item === null) { 152 column.data[i] = isStringColumn(column) ? internString('NULL') : 0; 153 } else if (typeof item === 'string') { 154 column.data[i] = internString(item); 155 } else { 156 column.data[i] = item; 157 } 158 } 159 } 160 161 return data; 162 } 163 164 async getSum(def: ColumnDef): Promise<string> { 165 if (!def.sum) return ''; 166 const result = await this.args.engine.query( 167 `select ifnull(sum(${def.columnId}), 0) as s from ${this.kind}`); 168 let sum = result.firstRow({s: NUM}).s; 169 if (def.kind === 'TIMESTAMP_NS') { 170 sum = sum / 1e6; 171 } 172 return `${sum}`; 173 } 174 175 columnFromColumnDef(def: ColumnDef, numRows: number): Column { 176 // TODO(hjd): The Column type should be based on the 177 // ColumnDef type or vice versa to avoid this cast. 178 return { 179 title: def.title, 180 kind: def.kind, 181 data: new def.columnConstructor(numRows), 182 columnId: def.columnId, 183 } as Column; 184 } 185} 186