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