• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {BigintMath} from '../base/bigint_math';
16import {assertExists, assertTrue} from '../base/logging';
17import {Engine} from '../common/engine';
18import {Registry} from '../common/registry';
19import {TraceTime, TrackState} from '../common/state';
20import {
21  TPDuration,
22  TPTime,
23  tpTimeFromSeconds,
24  TPTimeSpan,
25} from '../common/time';
26import {LIMIT, TrackData} from '../common/track_data';
27import {globals} from '../frontend/globals';
28import {publishTrackData} from '../frontend/publish';
29
30import {Controller} from './controller';
31import {ControllerFactory} from './controller';
32
33interface TrackConfig {}
34
35type TrackConfigWithNamespace = TrackConfig&{namespace: string};
36
37// Allow to override via devtools for testing (note, needs to be done in the
38// controller-thread).
39(self as {} as {quantPx: number}).quantPx = 1;
40
41// TrackController is a base class overridden by track implementations (e.g.,
42// sched slices, nestable slices, counters).
43export abstract class TrackController<
44    Config extends TrackConfig, Data extends TrackData = TrackData> extends
45    Controller<'main'> {
46  readonly trackId: string;
47  readonly engine: Engine;
48  private data?: TrackData;
49  private requestingData = false;
50  private queuedRequest = false;
51  private isSetup = false;
52  private lastReloadHandled = 0;
53
54  // We choose 100000 as the table size to cache as this is roughly the point
55  // where SQLite sorts start to become expensive.
56  private static readonly MIN_TABLE_SIZE_TO_CACHE = 100000;
57
58  constructor(args: TrackControllerArgs) {
59    super('main');
60    this.trackId = args.trackId;
61    this.engine = args.engine;
62  }
63
64  protected pxSize(): number {
65    return (self as {} as {quantPx: number}).quantPx;
66  }
67
68  // Can be overriden by the track implementation to allow one time setup work
69  // to be performed before the first onBoundsChange invcation.
70  async onSetup() {}
71
72  // Can be overriden by the track implementation to allow some one-off work
73  // when requested reload (e.g. recalculating height).
74  async onReload() {}
75
76  // Must be overridden by the track implementation. Is invoked when the track
77  // frontend runs out of cached data. The derived track controller is expected
78  // to publish new track data in response to this call.
79  abstract onBoundsChange(start: TPTime, end: TPTime, resolution: TPDuration):
80      Promise<Data>;
81
82  get trackState(): TrackState {
83    return assertExists(globals.state.tracks[this.trackId]);
84  }
85
86  get config(): Config {
87    return this.trackState.config as Config;
88  }
89
90  configHasNamespace(config: TrackConfig): config is TrackConfigWithNamespace {
91    return 'namespace' in config;
92  }
93
94  namespaceTable(tableName: string): string {
95    if (this.configHasNamespace(this.config)) {
96      return this.config.namespace + '_' + tableName;
97    } else {
98      return tableName;
99    }
100  }
101
102  publish(data: Data): void {
103    this.data = data;
104    publishTrackData({id: this.trackId, data});
105  }
106
107  // Returns a valid SQL table name with the given prefix that should be unique
108  // for each track.
109  tableName(prefix: string) {
110    // Derive table name from, since that is unique for each track.
111    // Track ID can be UUID but '-' is not valid for sql table name.
112    const idSuffix = this.trackId.split('-').join('_');
113    return `${prefix}_${idSuffix}`;
114  }
115
116  shouldSummarize(resolution: number): boolean {
117    // |resolution| is in s/px (to nearest power of 10) assuming a display
118    // of ~1000px 0.0008 is 0.8s.
119    return resolution >= 0.0008;
120  }
121
122  protected async query(query: string) {
123    const result = await this.engine.query(query);
124    return result;
125  }
126
127  private shouldReload(): boolean {
128    const {lastTrackReloadRequest} = globals.state;
129    return !!lastTrackReloadRequest &&
130        this.lastReloadHandled < lastTrackReloadRequest;
131  }
132
133  private markReloadHandled() {
134    this.lastReloadHandled = globals.state.lastTrackReloadRequest || 0;
135  }
136
137  shouldRequestData(traceTime: TraceTime): boolean {
138    const tspan = new TPTimeSpan(traceTime.start, traceTime.end);
139    if (this.data === undefined) return true;
140    if (this.shouldReload()) return true;
141
142    // If at the limit only request more data if the view has moved.
143    const atLimit = this.data.length === LIMIT;
144    if (atLimit) {
145      // We request more data than the window, so add window duration to find
146      // the previous window.
147      const prevWindowStart = this.data.start + tspan.duration;
148      return tspan.start !== prevWindowStart;
149    }
150
151    // Otherwise request more data only when out of range of current data or
152    // resolution has changed.
153    const inRange =
154        tspan.start >= this.data.start && tspan.end <= this.data.end;
155    return !inRange ||
156        this.data.resolution !==
157        globals.state.frontendLocalState.visibleState.resolution;
158  }
159
160  // Decides, based on the length of the trace and the number of rows
161  // provided whether a TrackController subclass should cache its quantized
162  // data. Returns the bucket size (in ns) if caching should happen and
163  // undefined otherwise.
164  // Subclasses should call this in their setup function
165  cachedBucketSizeNs(numRows: number): number|undefined {
166    // Ensure that we're not caching when the table size isn't even that big.
167    if (numRows < TrackController.MIN_TABLE_SIZE_TO_CACHE) {
168      return undefined;
169    }
170
171    const traceDuration = globals.stateTraceTime().duration;
172
173    // For large traces, going through the raw table in the most zoomed-out
174    // states can be very expensive as this can involve going through O(millions
175    // of rows). The cost of this becomes high even for just iteration but is
176    // especially slow as quantization involves a SQLite sort on the quantized
177    // timestamp (for the group by).
178    //
179    // To get around this, we can cache a pre-quantized table which we can then
180    // in zoomed-out situations and fall back to the real table when zoomed in
181    // (which naturally constrains the amount of data by virtue of the window
182    // covering a smaller timespan)
183    //
184    // This method computes that cached table by computing an approximation for
185    // the bucket size we would use when totally zoomed out and then going a few
186    // resolution levels down which ensures that our cached table works for more
187    // than the literally most zoomed out state. Moving down a resolution level
188    // is defined as moving down a power of 2; this matches the logic in
189    // |globals.getCurResolution|.
190    //
191    // TODO(lalitm): in the future, we should consider having a whole set of
192    // quantized tables each of which cover some portion of resolution lvel
193    // range. As each table covers a large number of resolution levels, even 3-4
194    // tables should really cover the all concievable trace sizes. This set
195    // could be computed by looking at the number of events being processed one
196    // level below the cached table and computing another layer of caching if
197    // that count is too high (with respect to MIN_TABLE_SIZE_TO_CACHE).
198
199    // 4k monitors have 3840 horizontal pixels so use that for a worst case
200    // approximation of the window width.
201    const approxWidthPx = 3840;
202
203    // Compute the outermost bucket size. This acts as a starting point for
204    // computing the cached size.
205    const outermostResolutionLevel =
206        Math.ceil(Math.log2(traceDuration.nanos / approxWidthPx));
207    const outermostBucketNs = Math.pow(2, outermostResolutionLevel);
208
209    // This constant decides how many resolution levels down from our outermost
210    // bucket computation we want to be able to use the cached table.
211    // We've chosen 7 as it seems to be empircally seems to be a good fit for
212    // trace data.
213    const resolutionLevelsCovered = 7;
214
215    // If we've got less resolution levels in the trace than the number of
216    // resolution levels we want to go down, bail out because this cached
217    // table is really not going to be used enough.
218    if (outermostResolutionLevel < resolutionLevelsCovered) {
219      return Number.MAX_SAFE_INTEGER;
220    }
221
222    // Another way to look at moving down resolution levels is to consider how
223    // many sub-intervals we are splitting the bucket into.
224    const bucketSubIntervals = Math.pow(2, resolutionLevelsCovered);
225
226    // Calculate the smallest bucket we want our table to be able to handle by
227    // dividing the outermsot bucket by the number of subintervals we should
228    // divide by.
229    const cachedBucketSizeNs = outermostBucketNs / bucketSubIntervals;
230
231    // Our logic above should make sure this is an integer but double check that
232    // here as an assertion before returning.
233    assertTrue(Number.isInteger(cachedBucketSizeNs));
234
235    return cachedBucketSizeNs;
236  }
237
238  run() {
239    const visibleState = globals.state.frontendLocalState.visibleState;
240    if (visibleState === undefined) {
241      return;
242    }
243    const visibleTimeSpan = globals.stateVisibleTime();
244    const dur = visibleTimeSpan.duration;
245    if (globals.state.visibleTracks.includes(this.trackId) &&
246        this.shouldRequestData(visibleState)) {
247      if (this.requestingData) {
248        this.queuedRequest = true;
249      } else {
250        this.requestingData = true;
251        let promise = Promise.resolve();
252        if (!this.isSetup) {
253          promise = this.onSetup();
254        } else if (this.shouldReload()) {
255          promise = this.onReload().then(() => this.markReloadHandled());
256        }
257        promise
258            .then(() => {
259              this.isSetup = true;
260              let resolution = visibleState.resolution;
261
262              if (BigintMath.popcount(resolution) !== 1) {
263                resolution = BigintMath.bitFloor(tpTimeFromSeconds(1000));
264              }
265
266              return this.onBoundsChange(
267                  visibleTimeSpan.start - dur,
268                  visibleTimeSpan.end + dur,
269                  resolution);
270            })
271            .then((data) => {
272              this.publish(data);
273            })
274            .finally(() => {
275              this.requestingData = false;
276              if (this.queuedRequest) {
277                this.queuedRequest = false;
278                this.run();
279              }
280            });
281      }
282    }
283  }
284}
285
286export interface TrackControllerArgs {
287  trackId: string;
288  engine: Engine;
289}
290
291export interface TrackControllerFactory extends
292    ControllerFactory<TrackControllerArgs> {
293  kind: string;
294}
295
296export const trackControllerRegistry =
297    Registry.kindRegistry<TrackControllerFactory>();
298