• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/*
2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17import {Actions} from '../common/actions';
18import {Engine} from '../common/engine';
19import {featureFlags} from '../common/feature_flags';
20import {ColumnType} from '../common/query_result';
21import {
22  PivotTableReduxQueryMetadata,
23  PivotTableReduxResult
24} from '../common/state';
25import {aggregationIndex} from '../frontend/pivot_table_redux_query_generator';
26
27import {Controller} from './controller';
28import {globals} from './globals';
29
30export const PIVOT_TABLE_REDUX_FLAG = featureFlags.register({
31  id: 'pivotTableRedux',
32  name: 'Pivot tables V2',
33  description: 'Second version of pivot table',
34  defaultValue: false,
35});
36
37// Node in the hierarchical pivot tree. Only leaf nodes contain data from the
38// query result.
39export interface PivotTree {
40  // Whether the node should be collapsed in the UI, false by default and can
41  // be toggled with the button.
42  isCollapsed: boolean;
43
44  // Non-empty only in internal nodes.
45  children: Map<ColumnType, PivotTree>;
46  aggregates: ColumnType[];
47
48  // Non-empty only in leaf nodes.
49  rows: ColumnType[][];
50}
51
52// Auxiliary class to build the tree from query response.
53class TreeBuilder {
54  private readonly root: PivotTree;
55  lastRow: ColumnType[];
56  pivotColumns: number;
57  aggregateColumns: number;
58
59  constructor(
60      pivotColumns: number, aggregateColumns: number, firstRow: ColumnType[]) {
61    this.pivotColumns = pivotColumns;
62    this.aggregateColumns = aggregateColumns;
63    this.root = this.createNode(0, firstRow);
64    let tree = this.root;
65    for (let i = 0; i + 1 < this.pivotColumns; i++) {
66      const value = firstRow[i];
67      tree = TreeBuilder.insertChild(
68          tree, value, this.createNode(i + 1, firstRow));
69    }
70    this.lastRow = firstRow;
71  }
72
73  // Add incoming row to the tree being built.
74  ingestRow(row: ColumnType[]) {
75    let tree = this.root;
76    for (let i = 0; i + 1 < this.pivotColumns; i++) {
77      const nextTree = tree.children.get(row[i]);
78      if (nextTree === undefined) {
79        // Insert the new node into the tree, and make variable `tree` point
80        // to the newly created node.
81        tree =
82            TreeBuilder.insertChild(tree, row[i], this.createNode(i + 1, row));
83      } else {
84        tree = nextTree;
85      }
86    }
87    tree.rows.push(row);
88    this.lastRow = row;
89  }
90
91  build(): PivotTree {
92    return this.root;
93  }
94
95  // Helper method that inserts child node into the tree and returns it, used
96  // for more concise modification of local variable pointing to the current
97  // node being built.
98  static insertChild(tree: PivotTree, key: ColumnType, child: PivotTree):
99      PivotTree {
100    tree.children.set(key, child);
101    return child;
102  }
103
104  // Initialize PivotTree from a row.
105  createNode(depth: number, row: ColumnType[]): PivotTree {
106    const aggregates = [];
107
108    for (let j = 0; j < this.aggregateColumns; j++) {
109      aggregates.push(row[aggregationIndex(this.pivotColumns, j, depth)]);
110    }
111
112    return {
113      isCollapsed: false,
114      children: new Map(),
115      aggregates,
116      rows: [],
117    };
118  }
119}
120
121function createEmptyQueryResult(metadata: PivotTableReduxQueryMetadata):
122    PivotTableReduxResult {
123  return {
124    tree: {
125      aggregates: [],
126      isCollapsed: false,
127      children: new Map(),
128      rows: [],
129    },
130    metadata
131  };
132}
133
134
135// Controller responsible for showing the panel with pivot table, as well as
136// executing its queries and post-processing query results.
137export class PivotTableReduxController extends Controller<{}> {
138  engine: Engine;
139  lastStartedQueryId: number;
140
141  constructor(args: {engine: Engine}) {
142    super({});
143    this.engine = args.engine;
144    this.lastStartedQueryId = 0;
145  }
146
147  run() {
148    if (!PIVOT_TABLE_REDUX_FLAG.get()) {
149      return;
150    }
151
152    const pivotTableState = globals.state.pivotTableRedux;
153    if (pivotTableState.queryId > this.lastStartedQueryId &&
154        pivotTableState.query !== null) {
155      this.lastStartedQueryId = pivotTableState.queryId;
156      const query = pivotTableState.query;
157
158      this.engine.query(query.text).then(async (result) => {
159        try {
160          await result.waitAllRows();
161        } catch {
162          // waitAllRows() frequently throws an exception, which is ignored in
163          // its other calls, so it's ignored here as well.
164        }
165
166        const columns = result.columns();
167
168        const it = result.iter({});
169        function nextRow(): ColumnType[] {
170          const row: ColumnType[] = [];
171          for (const column of columns) {
172            row.push(it.get(column));
173          }
174          it.next();
175          return row;
176        }
177
178        if (!it.valid()) {
179          // Iterator is invalid after creation; means that there are no rows
180          // satisfying filtering criteria. Return an empty tree.
181          globals.dispatch(Actions.setPivotStateReduxState({
182            pivotTableState: {
183              queryId: this.lastStartedQueryId,
184              query: null,
185              queryResult: createEmptyQueryResult(query.metadata),
186              selectionArea: pivotTableState.selectionArea
187            }
188          }));
189          return;
190        }
191
192        const treeBuilder = new TreeBuilder(
193            query.metadata.pivotColumns.length,
194            query.metadata.aggregationColumns.length,
195            nextRow());
196        while (it.valid()) {
197          treeBuilder.ingestRow(nextRow());
198        }
199
200        globals.dispatch(Actions.setPivotStateReduxState({
201          pivotTableState: {
202            queryId: this.lastStartedQueryId,
203            query: null,
204            queryResult: {
205              tree: treeBuilder.build(),
206              metadata: query.metadata,
207            },
208            selectionArea: pivotTableState.selectionArea
209          }
210        }));
211      });
212    }
213
214    const selection = globals.state.currentSelection;
215    if (selection !== null && selection.kind === 'AREA') {
216      const enabledArea = globals.state.areas[selection.areaId];
217      globals.dispatch(
218          Actions.togglePivotTableRedux({selectionArea: enabledArea}));
219    } else {
220      globals.dispatch(Actions.togglePivotTableRedux({selectionArea: null}));
221    }
222  }
223}