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