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