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