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