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 {AsyncLimiter} from '../../base/async_limiter'; 16import {Monitor} from '../../base/monitor'; 17import {isString} from '../../base/object_utils'; 18import { 19 AggregateData, 20 Column, 21 ColumnDef, 22 ThreadStateExtra, 23} from '../../common/aggregation_data'; 24import {Area, Sorting} from '../../common/state'; 25import {globals} from '../../frontend/globals'; 26import {publishAggregateData} from '../../frontend/publish'; 27import {Engine} from '../../trace_processor/engine'; 28import {NUM} from '../../trace_processor/query_result'; 29import {Controller} from '../controller'; 30 31export interface AggregationControllerArgs { 32 engine: Engine; 33 kind: string; 34} 35 36function isStringColumn(column: Column): boolean { 37 return column.kind === 'STRING' || column.kind === 'STATE'; 38} 39 40export abstract class AggregationController extends Controller<'main'> { 41 readonly kind: string; 42 private readonly monitor: Monitor; 43 private readonly limiter = new AsyncLimiter(); 44 45 abstract createAggregateView(engine: Engine, area: Area): Promise<boolean>; 46 47 abstract getExtra( 48 engine: Engine, 49 area: Area, 50 ): Promise<ThreadStateExtra | void>; 51 52 abstract getTabName(): string; 53 abstract getDefaultSorting(): Sorting; 54 abstract getColumnDefinitions(): ColumnDef[]; 55 56 constructor(private args: AggregationControllerArgs) { 57 super('main'); 58 this.kind = this.args.kind; 59 this.monitor = new Monitor([ 60 () => globals.state.selection, 61 () => globals.state.aggregatePreferences[this.args.kind], 62 ]); 63 } 64 65 run() { 66 if (this.monitor.ifStateChanged()) { 67 const selection = globals.state.selection; 68 if (selection.kind !== 'area') { 69 publishAggregateData({ 70 data: { 71 tabName: this.getTabName(), 72 columns: [], 73 strings: [], 74 columnSums: [], 75 }, 76 kind: this.args.kind, 77 }); 78 return; 79 } else { 80 this.limiter.schedule(async () => { 81 const data = await this.getAggregateData(selection, true); 82 publishAggregateData({data, kind: this.args.kind}); 83 }); 84 } 85 } 86 } 87 88 async getAggregateData( 89 area: Area, 90 areaChanged: boolean, 91 ): Promise<AggregateData> { 92 if (areaChanged) { 93 const viewExists = await this.createAggregateView(this.args.engine, area); 94 if (!viewExists) { 95 return { 96 tabName: this.getTabName(), 97 columns: [], 98 strings: [], 99 columnSums: [], 100 }; 101 } 102 } 103 104 const defs = this.getColumnDefinitions(); 105 const colIds = defs.map((col) => col.columnId); 106 const pref = globals.state.aggregatePreferences[this.kind]; 107 let sorting = `${this.getDefaultSorting().column} ${ 108 this.getDefaultSorting().direction 109 }`; 110 // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions 111 if (pref && pref.sorting) { 112 sorting = `${pref.sorting.column} ${pref.sorting.direction}`; 113 } 114 const query = `select ${colIds} from ${this.kind} order by ${sorting}`; 115 const result = await this.args.engine.query(query); 116 117 const numRows = result.numRows(); 118 const columns = defs.map((def) => this.columnFromColumnDef(def, numRows)); 119 const columnSums = await Promise.all(defs.map((def) => this.getSum(def))); 120 const extraData = await this.getExtra(this.args.engine, area); 121 const extra = extraData ? extraData : undefined; 122 const data: AggregateData = { 123 tabName: this.getTabName(), 124 columns, 125 columnSums, 126 strings: [], 127 extra, 128 }; 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 const it = result.iter({}); 141 for (let i = 0; it.valid(); it.next(), ++i) { 142 for (const column of data.columns) { 143 const item = it.get(column.columnId); 144 if (item === null) { 145 column.data[i] = isStringColumn(column) ? internString('NULL') : 0; 146 } else if (isString(item)) { 147 column.data[i] = internString(item); 148 } else if (item instanceof Uint8Array) { 149 column.data[i] = internString('<Binary blob>'); 150 } else if (typeof item === 'bigint') { 151 // TODO(stevegolton) It would be nice to keep bigints as bigints for 152 // the purposes of aggregation, however the aggregation infrastructure 153 // is likely to be significantly reworked when we introduce EventSet, 154 // and the complexity of supporting bigints throughout the aggregation 155 // panels in its current form is not worth it. Thus, we simply 156 // convert bigints to numbers. 157 column.data[i] = Number(item); 158 } else { 159 column.data[i] = item; 160 } 161 } 162 } 163 164 return data; 165 } 166 167 async getSum(def: ColumnDef): Promise<string> { 168 if (!def.sum) return ''; 169 const result = await this.args.engine.query( 170 `select ifnull(sum(${def.columnId}), 0) as s from ${this.kind}`, 171 ); 172 let sum = result.firstRow({s: NUM}).s; 173 if (def.kind === 'TIMESTAMP_NS') { 174 sum = sum / 1e6; 175 } 176 return `${sum}`; 177 } 178 179 columnFromColumnDef(def: ColumnDef, numRows: number): Column { 180 // TODO(hjd): The Column type should be based on the 181 // ColumnDef type or vice versa to avoid this cast. 182 return { 183 title: def.title, 184 kind: def.kind, 185 data: new def.columnConstructor(numRows), 186 columnId: def.columnId, 187 } as Column; 188 } 189} 190