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 15 16 17import {AggregateData, Column, ColumnDef} from '../../common/aggregation_data'; 18import {Engine} from '../../common/engine'; 19import {Sorting, TimestampedAreaSelection} from '../../common/state'; 20import {Controller} from '../controller'; 21import {globals} from '../globals'; 22 23export interface AggregationControllerArgs { 24 engine: Engine; 25 kind: string; 26} 27 28export abstract class AggregationController extends Controller<'main'> { 29 readonly kind: string; 30 private previousArea: TimestampedAreaSelection = {lastUpdate: 0}; 31 private previousSorting?: Sorting; 32 private requestingData = false; 33 private queuedRequest = false; 34 35 abstract async createAggregateView( 36 engine: Engine, area: TimestampedAreaSelection): Promise<boolean>; 37 38 abstract getTabName(): string; 39 abstract getDefaultSorting(): Sorting; 40 abstract getColumnDefinitions(): ColumnDef[]; 41 42 constructor(private args: AggregationControllerArgs) { 43 super('main'); 44 this.kind = this.args.kind; 45 } 46 47 run() { 48 const selectedArea = globals.state.frontendLocalState.selectedArea; 49 const aggregatePreferences = 50 globals.state.aggregatePreferences[this.args.kind]; 51 52 const areaChanged = this.previousArea.lastUpdate < selectedArea.lastUpdate; 53 const sortingChanged = aggregatePreferences && 54 this.previousSorting !== aggregatePreferences.sorting; 55 if (!areaChanged && !sortingChanged) return; 56 57 if (this.requestingData) { 58 this.queuedRequest = true; 59 } else { 60 this.requestingData = true; 61 if (sortingChanged) this.previousSorting = aggregatePreferences.sorting; 62 if (areaChanged) this.previousArea = selectedArea; 63 this.getAggregateData(areaChanged) 64 .then( 65 data => globals.publish( 66 'AggregateData', {data, kind: this.args.kind})) 67 .catch(reason => { 68 console.error(reason); 69 }) 70 .finally(() => { 71 this.requestingData = false; 72 if (this.queuedRequest) { 73 this.queuedRequest = false; 74 this.run(); 75 } 76 }); 77 } 78 } 79 80 async getAggregateData(areaChanged: boolean): Promise<AggregateData> { 81 const selectedArea = globals.state.frontendLocalState.selectedArea; 82 if (areaChanged) { 83 const viewExists = 84 await this.createAggregateView(this.args.engine, selectedArea); 85 if (!viewExists) { 86 return {tabName: this.getTabName(), columns: [], strings: []}; 87 } 88 } 89 90 const defs = this.getColumnDefinitions(); 91 const colIds = defs.map(col => col.columnId); 92 const pref = globals.state.aggregatePreferences[this.kind]; 93 let sorting = `${this.getDefaultSorting().column} ${ 94 this.getDefaultSorting().direction}`; 95 if (pref && pref.sorting) { 96 sorting = `${pref.sorting.column} ${pref.sorting.direction}`; 97 } 98 const query = `select ${colIds} from ${this.kind} order by ${sorting}`; 99 const result = await this.args.engine.query(query); 100 101 const numRows = +result.numRecords; 102 const columns = defs.map(def => this.columnFromColumnDef(def, numRows)); 103 104 const data: 105 AggregateData = {tabName: this.getTabName(), columns, strings: []}; 106 107 const stringIndexes = new Map<string, number>(); 108 function internString(str: string) { 109 let idx = stringIndexes.get(str); 110 if (idx !== undefined) return idx; 111 idx = data.strings.length; 112 data.strings.push(str); 113 stringIndexes.set(str, idx); 114 return idx; 115 } 116 117 for (let row = 0; row < numRows; row++) { 118 const cols = result.columns; 119 for (let col = 0; col < result.columns.length; col++) { 120 if (cols[col].stringValues && cols[col].stringValues!.length > 0) { 121 data.columns[col].data[row] = 122 internString(cols[col].stringValues![row]); 123 } else if (cols[col].longValues && cols[col].longValues!.length > 0) { 124 data.columns[col].data[row] = cols[col].longValues![row] as number; 125 } else if ( 126 cols[col].doubleValues && cols[col].doubleValues!.length > 0) { 127 data.columns[col].data[row] = cols[col].doubleValues![row]; 128 } 129 } 130 } 131 return data; 132 } 133 134 columnFromColumnDef(def: ColumnDef, numRows: number): Column { 135 // TODO(taylori): The Column type should be based on the 136 // ColumnDef type or vice versa to avoid this cast. 137 return { 138 title: def.title, 139 kind: def.kind, 140 data: new def.columnConstructor(numRows), 141 columnId: def.columnId, 142 } as Column; 143 } 144} 145