• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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