1// Copyright (C) 2018 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  ComputeMetricArgs,
17  ComputeMetricResult,
18  RawQueryArgs,
19  RawQueryResult
20} from './protos';
21import {iter, NUM_NULL, slowlyCountRows, STR} from './query_iterator';
22import {TimeSpan} from './time';
23
24export interface LoadingTracker {
25  beginLoading(): void;
26  endLoading(): void;
27}
28
29export class NullLoadingTracker implements LoadingTracker {
30  beginLoading(): void {}
31  endLoading(): void {}
32}
33
34export class QueryError extends Error {}
35
36/**
37 * Abstract interface of a trace proccessor.
38 * This is the TypeScript equivalent of src/trace_processor/rpc.h.
39 *
40 * Engine also defines helpers for the most common service methods
41 * (e.g. query).
42 */
43export abstract class Engine {
44  abstract readonly id: string;
45  private _cpus?: number[];
46  private _numGpus?: number;
47  private loadingTracker: LoadingTracker;
48
49  constructor(tracker?: LoadingTracker) {
50    this.loadingTracker = tracker ? tracker : new NullLoadingTracker();
51  }
52
53  /**
54   * Push trace data into the engine. The engine is supposed to automatically
55   * figure out the type of the trace (JSON vs Protobuf).
56   */
57  abstract parse(data: Uint8Array): Promise<void>;
58
59  /**
60   * Notify the engine no more data is coming.
61   */
62  abstract notifyEof(): void;
63
64  /**
65   * Resets the trace processor state by destroying any table/views created by
66   * the UI after loading.
67   */
68  abstract restoreInitialTables(): void;
69
70  /*
71   * Performs a SQL query and retruns a proto-encoded RawQueryResult object.
72   */
73  abstract rawQuery(rawQueryArgs: Uint8Array): Promise<Uint8Array>;
74
75  /*
76   * Performs computation of metrics and returns metric result and any errors.
77   * Metric result is a proto binary or text encoded TraceMetrics object.
78   */
79  abstract rawComputeMetric(computeMetricArgs: Uint8Array): Promise<Uint8Array>;
80
81  /**
82   * Shorthand for sending a SQL query to the engine.
83   * Deals with {,un}marshalling of request/response args.
84   */
85  async query(sqlQuery: string): Promise<RawQueryResult> {
86    const result = await this.uncheckedQuery(sqlQuery);
87    if (result.error) {
88      throw new QueryError(`Query error "${sqlQuery}": ${result.error}`);
89    }
90    return result;
91  }
92
93  // This method is for noncritical queries that shouldn't throw an error
94  // on failure. The caller must handle the failure.
95  async uncheckedQuery(sqlQuery: string): Promise<RawQueryResult> {
96    this.loadingTracker.beginLoading();
97    try {
98      const args = new RawQueryArgs();
99      args.sqlQuery = sqlQuery;
100      args.timeQueuedNs = Math.floor(performance.now() * 1e6);
101      const argsEncoded = RawQueryArgs.encode(args).finish();
102      const respEncoded = await this.rawQuery(argsEncoded);
103      const result = RawQueryResult.decode(respEncoded);
104      return result;
105    } finally {
106      this.loadingTracker.endLoading();
107    }
108  }
109
110  /**
111   * Shorthand for sending a compute metrics request to the engine.
112   * Deals with {,un}marshalling of request/response args.
113   */
114  async computeMetric(metrics: string[]): Promise<ComputeMetricResult> {
115    const args = new ComputeMetricArgs();
116    args.metricNames = metrics;
117    args.format = ComputeMetricArgs.ResultFormat.TEXTPROTO;
118    const argsEncoded = ComputeMetricArgs.encode(args).finish();
119    const respEncoded = await this.rawComputeMetric(argsEncoded);
120    const result = ComputeMetricResult.decode(respEncoded);
121    if (result.error.length > 0) {
122      throw new QueryError(result.error);
123    }
124    return result;
125  }
126
127  async queryOneRow(query: string): Promise<number[]> {
128    const result = await this.query(query);
129    const res: number[] = [];
130    if (slowlyCountRows(result) === 0) return res;
131    for (const col of result.columns) {
132      if (col.longValues!.length === 0) {
133        console.error(
134            `queryOneRow should only be used for queries that return long values
135             : ${query}`);
136        throw new Error(
137            `queryOneRow should only be used for queries that return long values
138             : ${query}`);
139      }
140      res.push(+col.longValues![0]);
141    }
142    return res;
143  }
144
145  // TODO(hjd): When streaming must invalidate this somehow.
146  async getCpus(): Promise<number[]> {
147    if (!this._cpus) {
148      const result =
149          await this.query('select distinct(cpu) from sched order by cpu;');
150      if (slowlyCountRows(result) === 0) return [];
151      this._cpus = result.columns[0].longValues!.map(n => +n);
152    }
153    return this._cpus;
154  }
155
156  async getNumberOfGpus(): Promise<number> {
157    if (!this._numGpus) {
158      const result = await this.query(`
159        select count(distinct(gpu_id)) as gpuCount
160        from gpu_counter_track
161        where name = 'gpufreq';
162      `);
163      this._numGpus = +result.columns[0].longValues![0];
164    }
165    return this._numGpus;
166  }
167
168  // TODO: This should live in code that's more specific to chrome, instead of
169  // in engine.
170  async getNumberOfProcesses(): Promise<number> {
171    const result = await this.query('select count(*) from process;');
172    return +result.columns[0].longValues![0];
173  }
174
175  async getTraceTimeBounds(): Promise<TimeSpan> {
176    const query = `select start_ts, end_ts from trace_bounds`;
177    const res = (await this.queryOneRow(query));
178    return new TimeSpan(res[0] / 1e9, res[1] / 1e9);
179  }
180
181  async getTracingMetadataTimeBounds(): Promise<TimeSpan> {
182    const query = await this.query(`select name, int_value from metadata
183         where name = 'tracing_started_ns' or name = 'tracing_disabled_ns'
184         or name = 'all_data_source_started_ns'`);
185    let startBound = -Infinity;
186    let endBound = Infinity;
187    const it = iter({'name': STR, 'int_value': NUM_NULL}, query);
188    for (; it.valid(); it.next()) {
189      const columnName = it.row.name;
190      const timestamp = it.row.int_value;
191      if (timestamp === null) continue;
192      if (columnName === 'tracing_disabled_ns') {
193        endBound = Math.min(endBound, timestamp / 1e9);
194      } else {
195        startBound = Math.max(startBound, timestamp / 1e9);
196      }
197    }
198
199    return new TimeSpan(startBound, endBound);
200  }
201}
202