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