| // Copyright (C) 2018 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| import {assertExists, assertTrue} from '../base/logging'; |
| import {Engine} from '../common/engine'; |
| import {Registry} from '../common/registry'; |
| import {TraceTime, TrackState} from '../common/state'; |
| import {fromNs, toNs} from '../common/time'; |
| import {LIMIT, TrackData} from '../common/track_data'; |
| import {globals} from '../frontend/globals'; |
| import {publishTrackData} from '../frontend/publish'; |
| |
| import {Controller} from './controller'; |
| import {ControllerFactory} from './controller'; |
| |
| interface TrackConfig {} |
| |
| type TrackConfigWithNamespace = TrackConfig&{namespace: string}; |
| |
| // Allow to override via devtools for testing (note, needs to be done in the |
| // controller-thread). |
| (self as {} as {quantPx: number}).quantPx = 1; |
| |
| // TrackController is a base class overridden by track implementations (e.g., |
| // sched slices, nestable slices, counters). |
| export abstract class TrackController< |
| Config extends TrackConfig, Data extends TrackData = TrackData> extends |
| Controller<'main'> { |
| readonly trackId: string; |
| readonly engine: Engine; |
| private data?: TrackData; |
| private requestingData = false; |
| private queuedRequest = false; |
| private isSetup = false; |
| private lastReloadHandled = 0; |
| |
| // We choose 100000 as the table size to cache as this is roughly the point |
| // where SQLite sorts start to become expensive. |
| private static readonly MIN_TABLE_SIZE_TO_CACHE = 100000; |
| |
| constructor(args: TrackControllerArgs) { |
| super('main'); |
| this.trackId = args.trackId; |
| this.engine = args.engine; |
| } |
| |
| protected pxSize(): number { |
| return (self as {} as {quantPx: number}).quantPx; |
| } |
| |
| // Can be overriden by the track implementation to allow one time setup work |
| // to be performed before the first onBoundsChange invcation. |
| async onSetup() {} |
| |
| // Can be overriden by the track implementation to allow some one-off work |
| // when requested reload (e.g. recalculating height). |
| async onReload() {} |
| |
| // Must be overridden by the track implementation. Is invoked when the track |
| // frontend runs out of cached data. The derived track controller is expected |
| // to publish new track data in response to this call. |
| abstract onBoundsChange(start: number, end: number, resolution: number): |
| Promise<Data>; |
| |
| get trackState(): TrackState { |
| return assertExists(globals.state.tracks[this.trackId]); |
| } |
| |
| get config(): Config { |
| return this.trackState.config as Config; |
| } |
| |
| configHasNamespace(config: TrackConfig): config is TrackConfigWithNamespace { |
| return 'namespace' in config; |
| } |
| |
| namespaceTable(tableName: string): string { |
| if (this.configHasNamespace(this.config)) { |
| return this.config.namespace + '_' + tableName; |
| } else { |
| return tableName; |
| } |
| } |
| |
| publish(data: Data): void { |
| this.data = data; |
| publishTrackData({id: this.trackId, data}); |
| } |
| |
| // Returns a valid SQL table name with the given prefix that should be unique |
| // for each track. |
| tableName(prefix: string) { |
| // Derive table name from, since that is unique for each track. |
| // Track ID can be UUID but '-' is not valid for sql table name. |
| const idSuffix = this.trackId.split('-').join('_'); |
| return `${prefix}_${idSuffix}`; |
| } |
| |
| shouldSummarize(resolution: number): boolean { |
| // |resolution| is in s/px (to nearest power of 10) assuming a display |
| // of ~1000px 0.0008 is 0.8s. |
| return resolution >= 0.0008; |
| } |
| |
| protected async query(query: string) { |
| const result = await this.engine.query(query); |
| return result; |
| } |
| |
| private shouldReload(): boolean { |
| const {lastTrackReloadRequest} = globals.state; |
| return !!lastTrackReloadRequest && |
| this.lastReloadHandled < lastTrackReloadRequest; |
| } |
| |
| private markReloadHandled() { |
| this.lastReloadHandled = globals.state.lastTrackReloadRequest || 0; |
| } |
| |
| shouldRequestData(traceTime: TraceTime): boolean { |
| if (this.data === undefined) return true; |
| if (this.shouldReload()) return true; |
| |
| // If at the limit only request more data if the view has moved. |
| const atLimit = this.data.length === LIMIT; |
| if (atLimit) { |
| // We request more data than the window, so add window duration to find |
| // the previous window. |
| const prevWindowStart = |
| this.data.start + (traceTime.startSec - traceTime.endSec); |
| return traceTime.startSec !== prevWindowStart; |
| } |
| |
| // Otherwise request more data only when out of range of current data or |
| // resolution has changed. |
| const inRange = traceTime.startSec >= this.data.start && |
| traceTime.endSec <= this.data.end; |
| return !inRange || |
| this.data.resolution !== |
| globals.state.frontendLocalState.visibleState.resolution; |
| } |
| |
| // Decides, based on the length of the trace and the number of rows |
| // provided whether a TrackController subclass should cache its quantized |
| // data. Returns the bucket size (in ns) if caching should happen and |
| // undefined otherwise. |
| // Subclasses should call this in their setup function |
| cachedBucketSizeNs(numRows: number): number|undefined { |
| // Ensure that we're not caching when the table size isn't even that big. |
| if (numRows < TrackController.MIN_TABLE_SIZE_TO_CACHE) { |
| return undefined; |
| } |
| |
| const bounds = globals.state.traceTime; |
| const traceDurNs = toNs(bounds.endSec - bounds.startSec); |
| |
| // For large traces, going through the raw table in the most zoomed-out |
| // states can be very expensive as this can involve going through O(millions |
| // of rows). The cost of this becomes high even for just iteration but is |
| // especially slow as quantization involves a SQLite sort on the quantized |
| // timestamp (for the group by). |
| // |
| // To get around this, we can cache a pre-quantized table which we can then |
| // in zoomed-out situations and fall back to the real table when zoomed in |
| // (which naturally constrains the amount of data by virtue of the window |
| // covering a smaller timespan) |
| // |
| // This method computes that cached table by computing an approximation for |
| // the bucket size we would use when totally zoomed out and then going a few |
| // resolution levels down which ensures that our cached table works for more |
| // than the literally most zoomed out state. Moving down a resolution level |
| // is defined as moving down a power of 2; this matches the logic in |
| // |globals.getCurResolution|. |
| // |
| // TODO(lalitm): in the future, we should consider having a whole set of |
| // quantized tables each of which cover some portion of resolution lvel |
| // range. As each table covers a large number of resolution levels, even 3-4 |
| // tables should really cover the all concievable trace sizes. This set |
| // could be computed by looking at the number of events being processed one |
| // level below the cached table and computing another layer of caching if |
| // that count is too high (with respect to MIN_TABLE_SIZE_TO_CACHE). |
| |
| // 4k monitors have 3840 horizontal pixels so use that for a worst case |
| // approximation of the window width. |
| const approxWidthPx = 3840; |
| |
| // Compute the outermost bucket size. This acts as a starting point for |
| // computing the cached size. |
| const outermostResolutionLevel = |
| Math.ceil(Math.log2(traceDurNs / approxWidthPx)); |
| const outermostBucketNs = Math.pow(2, outermostResolutionLevel); |
| |
| // This constant decides how many resolution levels down from our outermost |
| // bucket computation we want to be able to use the cached table. |
| // We've chosen 7 as it seems to be empircally seems to be a good fit for |
| // trace data. |
| const resolutionLevelsCovered = 7; |
| |
| // If we've got less resolution levels in the trace than the number of |
| // resolution levels we want to go down, bail out because this cached |
| // table is really not going to be used enough. |
| if (outermostResolutionLevel < resolutionLevelsCovered) { |
| return Number.MAX_SAFE_INTEGER; |
| } |
| |
| // Another way to look at moving down resolution levels is to consider how |
| // many sub-intervals we are splitting the bucket into. |
| const bucketSubIntervals = Math.pow(2, resolutionLevelsCovered); |
| |
| // Calculate the smallest bucket we want our table to be able to handle by |
| // dividing the outermsot bucket by the number of subintervals we should |
| // divide by. |
| const cachedBucketSizeNs = outermostBucketNs / bucketSubIntervals; |
| |
| // Our logic above should make sure this is an integer but double check that |
| // here as an assertion before returning. |
| assertTrue(Number.isInteger(cachedBucketSizeNs)); |
| |
| return cachedBucketSizeNs; |
| } |
| |
| run() { |
| const visibleState = globals.state.frontendLocalState.visibleState; |
| if (visibleState === undefined || visibleState.resolution === undefined || |
| visibleState.resolution === Infinity) { |
| return; |
| } |
| const dur = visibleState.endSec - visibleState.startSec; |
| if (globals.state.visibleTracks.includes(this.trackId) && |
| this.shouldRequestData(visibleState)) { |
| if (this.requestingData) { |
| this.queuedRequest = true; |
| } else { |
| this.requestingData = true; |
| let promise = Promise.resolve(); |
| if (!this.isSetup) { |
| promise = this.onSetup(); |
| } else if (this.shouldReload()) { |
| promise = this.onReload().then(() => this.markReloadHandled()); |
| } |
| promise |
| .then(() => { |
| this.isSetup = true; |
| let resolution = visibleState.resolution; |
| // TODO(hjd): We shouldn't have to be so defensive here. |
| if (Math.log2(toNs(resolution)) % 1 !== 0) { |
| // resolution is in pixels per second so 1000 means |
| // 1px = 1ms. |
| resolution = |
| fromNs(Math.pow(2, Math.floor(Math.log2(toNs(1000))))); |
| } |
| return this.onBoundsChange( |
| visibleState.startSec - dur, |
| visibleState.endSec + dur, |
| resolution); |
| }) |
| .then((data) => { |
| this.publish(data); |
| }) |
| .finally(() => { |
| this.requestingData = false; |
| if (this.queuedRequest) { |
| this.queuedRequest = false; |
| this.run(); |
| } |
| }); |
| } |
| } |
| } |
| } |
| |
| export interface TrackControllerArgs { |
| trackId: string; |
| engine: Engine; |
| } |
| |
| export interface TrackControllerFactory extends |
| ControllerFactory<TrackControllerArgs> { |
| kind: string; |
| } |
| |
| export const trackControllerRegistry = |
| Registry.kindRegistry<TrackControllerFactory>(); |