1// Copyright (C) 2023 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 {Registry} from '../base/registry'; 16import {Track, TrackDescriptor, TrackManager} from '../public/track'; 17import {AsyncLimiter} from '../base/async_limiter'; 18import {TrackRenderContext} from '../public/track'; 19 20export interface TrackRenderer { 21 readonly track: Track; 22 desc: TrackDescriptor; 23 render(ctx: TrackRenderContext): void; 24 getError(): Error | undefined; 25} 26 27/** 28 * TrackManager is responsible for managing the registry of tracks and their 29 * lifecycle of tracks over render cycles. 30 * 31 * Example usage: 32 * function render() { 33 * const trackCache = new TrackCache(); 34 * const foo = trackCache.getTrackRenderer('foo', 'exampleURI', {}); 35 * const bar = trackCache.getTrackRenderer('bar', 'exampleURI', {}); 36 * trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks 37 * } 38 * 39 * Example of how flushing works: 40 * First cycle 41 * getTrackRenderer('foo', ...) <-- new track 'foo' created 42 * getTrackRenderer('bar', ...) <-- new track 'bar' created 43 * flushTracks() 44 * Second cycle 45 * getTrackRenderer('foo', ...) <-- returns cached 'foo' track 46 * flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle 47 * Third cycle 48 * flushTracks() <-- 'foo' is destroyed. 49 */ 50export class TrackManagerImpl implements TrackManager { 51 private tracks = new Registry<TrackFSM>((x) => x.desc.uri); 52 53 // This property is written by scroll_helper.ts and read&cleared by the 54 // track_panel.ts. This exist for the following use case: the user wants to 55 // scroll to track X, but X is not visible because it's in a collapsed group. 56 // So we want to stash this information in a place that track_panel.ts can 57 // access when creating dom elements. 58 // 59 // Note: this is the node id of the track node to scroll to, not the track 60 // uri, as this allows us to scroll to tracks that have no uri. 61 scrollToTrackNodeId?: string; 62 63 registerTrack(trackDesc: TrackDescriptor): Disposable { 64 return this.tracks.register(new TrackFSM(trackDesc)); 65 } 66 67 findTrack( 68 predicate: (desc: TrackDescriptor) => boolean | undefined, 69 ): TrackDescriptor | undefined { 70 for (const t of this.tracks.values()) { 71 if (predicate(t.desc)) return t.desc; 72 } 73 return undefined; 74 } 75 76 getAllTracks(): TrackDescriptor[] { 77 return Array.from(this.tracks.valuesAsArray().map((t) => t.desc)); 78 } 79 80 // Look up track into for a given track's URI. 81 // Returns |undefined| if no track can be found. 82 getTrack(uri: string): TrackDescriptor | undefined { 83 return this.tracks.tryGet(uri)?.desc; 84 } 85 86 // This is only called by the viewer_page.ts. 87 getTrackRenderer(uri: string): TrackRenderer | undefined { 88 // Search for a cached version of this track, 89 const trackFsm = this.tracks.tryGet(uri); 90 trackFsm?.markUsed(); 91 return trackFsm; 92 } 93 94 // Destroys all tracks that didn't recently get a getTrackRenderer() call. 95 flushOldTracks() { 96 for (const trackFsm of this.tracks.values()) { 97 trackFsm.tick(); 98 } 99 } 100} 101 102const DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT = 1; 103 104/** 105 * Owns all runtime information about a track and manages its lifecycle, 106 * ensuring lifecycle hooks are called synchronously and in the correct order. 107 * 108 * There are quite some subtle properties that this class guarantees: 109 * - It make sure that lifecycle methods don't overlap with each other. 110 * - It prevents a chain of onCreate > onDestroy > onCreate if the first 111 * onCreate() is still oustanding. This is by virtue of using AsyncLimiter 112 * which under the hoods holds only the most recent task and skips the 113 * intermediate ones. 114 * - Ensures that a track never sees two consecutive onCreate, or onDestroy or 115 * an onDestroy without an onCreate. 116 * - Ensures that onUpdate never overlaps or follows with onDestroy. This is 117 * particularly important because tracks often drop tables/views onDestroy 118 * and they shouldn't try to fetch more data onUpdate past that point. 119 */ 120class TrackFSM implements TrackRenderer { 121 public readonly desc: TrackDescriptor; 122 123 private readonly limiter = new AsyncLimiter(); 124 private error?: Error; 125 private tickSinceLastUsed = 0; 126 private created = false; 127 128 constructor(desc: TrackDescriptor) { 129 this.desc = desc; 130 } 131 132 markUsed(): void { 133 this.tickSinceLastUsed = 0; 134 } 135 136 // Increment the lastUsed counter, and maybe call onDestroy(). 137 tick(): void { 138 if (this.tickSinceLastUsed++ === DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT) { 139 // Schedule an onDestroy 140 this.limiter.schedule(async () => { 141 // Don't enter the track again once an error is has occurred 142 if (this.error !== undefined) { 143 return; 144 } 145 146 try { 147 if (this.created) { 148 await Promise.resolve(this.track.onDestroy?.()); 149 this.created = false; 150 } 151 } catch (e) { 152 this.error = e; 153 } 154 }); 155 } 156 } 157 158 render(ctx: TrackRenderContext): void { 159 this.limiter.schedule(async () => { 160 // Don't enter the track again once an error has occurred 161 if (this.error !== undefined) { 162 return; 163 } 164 165 try { 166 // Call onCreate() if this is our first call 167 if (!this.created) { 168 await this.track.onCreate?.(ctx); 169 this.created = true; 170 } 171 await Promise.resolve(this.track.onUpdate?.(ctx)); 172 } catch (e) { 173 this.error = e; 174 } 175 }); 176 this.track.render(ctx); 177 } 178 179 getError(): Error | undefined { 180 return this.error; 181 } 182 183 get track(): Track { 184 return this.desc.track; 185 } 186} 187