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