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 { 16 AggregateData, 17 Column, 18 ColumnDef, 19 ThreadStateExtra, 20} from '../../common/aggregation_data'; 21import {Engine} from '../../common/engine'; 22import {slowlyCountRows} from '../../common/query_iterator'; 23import {Area, Sorting} from '../../common/state'; 24import {Controller} from '../controller'; 25import {globals} from '../globals'; 26 27export interface AggregationControllerArgs { 28 engine: Engine; 29 kind: string; 30} 31 32export abstract class AggregationController extends Controller<'main'> { 33 readonly kind: string; 34 private previousArea?: Area; 35 private previousSorting?: Sorting; 36 private requestingData = false; 37 private queuedRequest = false; 38 39 abstract async createAggregateView(engine: Engine, area: Area): 40 Promise<boolean>; 41 42 abstract async getExtra(engine: Engine, area: Area): 43 Promise<ThreadStateExtra|void>; 44 45 abstract getTabName(): string; 46 abstract getDefaultSorting(): Sorting; 47 abstract getColumnDefinitions(): ColumnDef[]; 48 49 constructor(private args: AggregationControllerArgs) { 50 super('main'); 51 this.kind = this.args.kind; 52 } 53 54 run() { 55 const selection = globals.state.currentSelection; 56 if (selection === null || selection.kind !== 'AREA') { 57 globals.publish('AggregateData', { 58 data: { 59 tabName: this.getTabName(), 60 columns: [], 61 strings: [], 62 columnSums: [], 63 }, 64 kind: this.args.kind 65 }); 66 return; 67 } 68 const selectedArea = globals.state.areas[selection.areaId]; 69 const aggregatePreferences = 70 globals.state.aggregatePreferences[this.args.kind]; 71 72 const areaChanged = this.previousArea !== selectedArea; 73 const sortingChanged = aggregatePreferences && 74 this.previousSorting !== aggregatePreferences.sorting; 75 if (!areaChanged && !sortingChanged) return; 76 77 if (this.requestingData) { 78 this.queuedRequest = true; 79 } else { 80 this.requestingData = true; 81 if (sortingChanged) this.previousSorting = aggregatePreferences.sorting; 82 if (areaChanged) this.previousArea = Object.assign({}, selectedArea); 83 this.getAggregateData(selectedArea, areaChanged) 84 .then( 85 data => globals.publish( 86 'AggregateData', {data, kind: this.args.kind})) 87 .finally(() => { 88 this.requestingData = false; 89 if (this.queuedRequest) { 90 this.queuedRequest = false; 91 this.run(); 92 } 93 }); 94 } 95 } 96 97 async getAggregateData(area: Area, areaChanged: boolean): 98 Promise<AggregateData> { 99 if (areaChanged) { 100 const viewExists = await this.createAggregateView(this.args.engine, area); 101 if (!viewExists) { 102 return { 103 tabName: this.getTabName(), 104 columns: [], 105 strings: [], 106 columnSums: [], 107 }; 108 } 109 } 110 111 const defs = this.getColumnDefinitions(); 112 const colIds = defs.map(col => col.columnId); 113 const pref = globals.state.aggregatePreferences[this.kind]; 114 let sorting = `${this.getDefaultSorting().column} ${ 115 this.getDefaultSorting().direction}`; 116 if (pref && pref.sorting) { 117 sorting = `${pref.sorting.column} ${pref.sorting.direction}`; 118 } 119 const query = `select ${colIds} from ${this.kind} order by ${sorting}`; 120 const result = await this.args.engine.query(query); 121 122 const numRows = slowlyCountRows(result); 123 const columns = defs.map(def => this.columnFromColumnDef(def, numRows)); 124 const columnSums = await Promise.all(defs.map(def => this.getSum(def))); 125 const extraData = await this.getExtra(this.args.engine, area); 126 const extra = extraData ? extraData : undefined; 127 const data: AggregateData = 128 {tabName: this.getTabName(), columns, columnSums, strings: [], extra}; 129 130 const stringIndexes = new Map<string, number>(); 131 function internString(str: string) { 132 let idx = stringIndexes.get(str); 133 if (idx !== undefined) return idx; 134 idx = data.strings.length; 135 data.strings.push(str); 136 stringIndexes.set(str, idx); 137 return idx; 138 } 139 140 for (let row = 0; row < numRows; row++) { 141 const cols = result.columns; 142 for (let col = 0; col < result.columns.length; col++) { 143 if (cols[col].stringValues && cols[col].stringValues!.length > 0) { 144 data.columns[col].data[row] = 145 internString(cols[col].stringValues![row]); 146 } else if (cols[col].longValues && cols[col].longValues!.length > 0) { 147 data.columns[col].data[row] = cols[col].longValues![row]; 148 } else if ( 149 cols[col].doubleValues && cols[col].doubleValues!.length > 0) { 150 data.columns[col].data[row] = cols[col].doubleValues![row]; 151 } 152 } 153 } 154 return data; 155 } 156 157 async getSum(def: ColumnDef): Promise<string> { 158 if (!def.sum) return ''; 159 const result = await this.args.engine.queryOneRow( 160 `select sum(${def.columnId}) from ${this.kind}`); 161 let sum = result[0]; 162 if (def.kind === 'TIMESTAMP_NS') { 163 sum = sum / 1e6; 164 } 165 return `${sum}`; 166 } 167 168 columnFromColumnDef(def: ColumnDef, numRows: number): Column { 169 // TODO(hjd): The Column type should be based on the 170 // ColumnDef type or vice versa to avoid this cast. 171 return { 172 title: def.title, 173 kind: def.kind, 174 data: new def.columnConstructor(numRows), 175 columnId: def.columnId, 176 } as Column; 177 } 178} 179