| // Copyright (C) 2023 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 {BigintMath} from '../base/bigint_math'; | 
 |  | 
 | import {assertTrue} from '../base/logging'; | 
 | import {TPDuration, TPTime} from '../common/time'; | 
 |  | 
 | export const BUCKETS_PER_PIXEL = 2; | 
 |  | 
 | // CacheKey is a specific region of the timeline defined by the | 
 | // following four properties: | 
 | // - startNs | 
 | // - endNs | 
 | // - bucketNs | 
 | // - windowSizePx | 
 | // startNs is the beginning of the region in ns | 
 | // endNs is the end of the region in ns | 
 | // bucketNs is the size of a single bucket within the region which is | 
 | //          used for quantizing the timeline. | 
 | // windowSizePx is the size of the whole window in pixels. | 
 | // | 
 | // In the nominal case bucketNs is | 
 | // set so that 1px of the screen corresponds to N bucketNs worth of | 
 | // time where 1 < N < 10. This ensures that we show the maximum | 
 | // amount of data given the available screen real estate. | 
 | // We shouldn't rely on this property when rendering however since in | 
 | // some situations (i.e. after zooming before new data has loaded) it | 
 | // may not be the case. | 
 | // | 
 | // CacheKey's can be 'normalized' - rounding the interval up and the | 
 | // bucket size down. For a given CacheKey key ('foo') the normalized | 
 | // version ('normal') has the properties: | 
 | //   normal.startNs <= foo.startNs | 
 | //   normal.endNs => foo.endNs | 
 | //   normal.bucketNs <= foo.bucketNs | 
 | //   normal.windowSizePx ~= windowSizePx (we round to the nearest 100px) | 
 | //   foo.isCoveredBy(foo) == true | 
 | //   foo.isCoveredBy(normal) == true | 
 | //   normal.isCoveredBy(normal) == true | 
 | //   normal.isCoveredBy(foo) == false unless normal == foo | 
 | //   normalize(normal) == normal | 
 | // | 
 | // In other words the normal window is a superset of the data of the | 
 | // non-normal window at a higher resolution. Normalization is used to | 
 | // avoid re-fetching data on tiny zooms/moves/resizes. | 
 | export class CacheKey { | 
 |   readonly start: TPTime; | 
 |   readonly end: TPTime; | 
 |   readonly bucketSize: TPDuration; | 
 |   readonly windowSizePx: number; | 
 |  | 
 |   static create(startNs: TPTime, endNs: TPTime, windowSizePx: number): | 
 |       CacheKey { | 
 |     const bucketNs = (endNs - startNs) / | 
 |         BigInt(Math.round(windowSizePx * BUCKETS_PER_PIXEL)); | 
 |     return new CacheKey(startNs, endNs, bucketNs, windowSizePx); | 
 |   } | 
 |  | 
 |   private constructor( | 
 |       startNs: TPTime, endNs: TPTime, bucketNs: TPDuration, | 
 |       windowSizePx: number) { | 
 |     this.start = startNs; | 
 |     this.end = endNs; | 
 |     this.bucketSize = bucketNs; | 
 |     this.windowSizePx = windowSizePx; | 
 |   } | 
 |  | 
 |   static zero(): CacheKey { | 
 |     return new CacheKey(0n, 0n, 0n, 100); | 
 |   } | 
 |  | 
 |   get normalizedBucketNs(): bigint { | 
 |     // Round bucketNs down to the nearest smaller power of 2 (minimum 1): | 
 |     return BigintMath.max(1n, BigintMath.bitFloor(this.bucketSize)); | 
 |   } | 
 |  | 
 |   get normalizedWindowSizePx(): number { | 
 |     return Math.max(100, Math.round(this.windowSizePx / 100) * 100); | 
 |   } | 
 |  | 
 |   normalize(): CacheKey { | 
 |     const windowSizePx = this.normalizedWindowSizePx; | 
 |     const bucketNs = this.normalizedBucketNs; | 
 |     const windowNs = BigInt(windowSizePx * BUCKETS_PER_PIXEL) * bucketNs; | 
 |     const startNs = BigintMath.quantFloor(this.start, windowNs); | 
 |     const endNs = BigintMath.quantCeil(this.end, windowNs); | 
 |     return new CacheKey(startNs, endNs, bucketNs, windowSizePx); | 
 |   } | 
 |  | 
 |   isNormalized(): boolean { | 
 |     return this.toString() === this.normalize().toString(); | 
 |   } | 
 |  | 
 |   isCoveredBy(other: CacheKey): boolean { | 
 |     let r = true; | 
 |     r = r && other.start <= this.start; | 
 |     r = r && other.end >= this.end; | 
 |     r = r && other.normalizedBucketNs === this.normalizedBucketNs; | 
 |     r = r && other.normalizedWindowSizePx === this.normalizedWindowSizePx; | 
 |     return r; | 
 |   } | 
 |  | 
 |   // toString is 'load bearing' in that it's used to key e.g. caches | 
 |   // with CacheKey's. | 
 |   toString() { | 
 |     const start = this.start; | 
 |     const end = this.end; | 
 |     const bucket = this.bucketSize; | 
 |     const size = this.windowSizePx; | 
 |     return `CacheKey<${start}, ${end}, ${bucket}, ${size}>`; | 
 |   } | 
 | } | 
 |  | 
 |  | 
 | interface CacheItem<T> { | 
 |   t: T; | 
 |   lastAccessId: number; | 
 | } | 
 |  | 
 |  | 
 | // LRU cache for the tracks. | 
 | // T is all the data needed for a displaying the track in a given | 
 | // CacheKey area - generally an array of slices. | 
 | export class TrackCache<T> { | 
 |   private cacheSize: number; | 
 |   private cache: Map<string, CacheItem<T>>; | 
 |   private lastAccessId: number; | 
 |  | 
 |   constructor(cacheSize: number) { | 
 |     assertTrue(cacheSize >= 2); | 
 |     this.cacheSize = cacheSize; | 
 |     this.cache = new Map(); | 
 |     this.lastAccessId = 0; | 
 |   } | 
 |  | 
 |   insert(cacheKey: CacheKey, t: T): void { | 
 |     assertTrue(cacheKey.isNormalized()); | 
 |     const key = cacheKey.toString(); | 
 |     this.cache.set(key, { | 
 |       t, | 
 |       lastAccessId: this.lastAccessId++, | 
 |     }); | 
 |     this.updateLru(); | 
 |   } | 
 |  | 
 |   lookup(cacheKey: CacheKey): undefined|T { | 
 |     assertTrue(cacheKey.isNormalized()); | 
 |     const key = cacheKey.toString(); | 
 |     const item = this.cache.get(key); | 
 |     if (item) { | 
 |       item.lastAccessId = this.lastAccessId++; | 
 |       this.updateLru(); | 
 |     } | 
 |     return item === undefined ? undefined : item.t; | 
 |   } | 
 |  | 
 |   private updateLru(): void { | 
 |     while (this.cache.size > this.cacheSize) { | 
 |       let oldestKey = ''; | 
 |       let oldestAccessId = Number.MAX_SAFE_INTEGER; | 
 |       for (const [k, v] of this.cache.entries()) { | 
 |         if (v.lastAccessId < oldestAccessId) { | 
 |           oldestAccessId = v.lastAccessId; | 
 |           oldestKey = k; | 
 |         } | 
 |       } | 
 |       this.cache.delete(oldestKey); | 
 |     } | 
 |   } | 
 | } |