blob: 827056488347da812af33fa8f950a46b91f0039b [file] [log] [blame]
// 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 {Registry} from '../base/registry';
import {Track, TrackDescriptor, TrackManager} from '../public/track';
import {AsyncLimiter} from '../base/async_limiter';
import {TrackRenderContext} from '../public/track';
export interface TrackRenderer {
readonly track: Track;
desc: TrackDescriptor;
render(ctx: TrackRenderContext): void;
getError(): Error | undefined;
}
/**
* TrackManager is responsible for managing the registry of tracks and their
* lifecycle of tracks over render cycles.
*
* Example usage:
* function render() {
* const trackCache = new TrackCache();
* const foo = trackCache.getTrackRenderer('foo', 'exampleURI', {});
* const bar = trackCache.getTrackRenderer('bar', 'exampleURI', {});
* trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
* }
*
* Example of how flushing works:
* First cycle
* getTrackRenderer('foo', ...) <-- new track 'foo' created
* getTrackRenderer('bar', ...) <-- new track 'bar' created
* flushTracks()
* Second cycle
* getTrackRenderer('foo', ...) <-- returns cached 'foo' track
* flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
* Third cycle
* flushTracks() <-- 'foo' is destroyed.
*/
export class TrackManagerImpl implements TrackManager {
private tracks = new Registry<TrackFSM>((x) => x.desc.uri);
// This property is written by scroll_helper.ts and read&cleared by the
// track_panel.ts. This exist for the following use case: the user wants to
// scroll to track X, but X is not visible because it's in a collapsed group.
// So we want to stash this information in a place that track_panel.ts can
// access when creating dom elements.
//
// Note: this is the node id of the track node to scroll to, not the track
// uri, as this allows us to scroll to tracks that have no uri.
scrollToTrackNodeId?: string;
registerTrack(trackDesc: TrackDescriptor): Disposable {
return this.tracks.register(new TrackFSM(trackDesc));
}
findTrack(
predicate: (desc: TrackDescriptor) => boolean | undefined,
): TrackDescriptor | undefined {
for (const t of this.tracks.values()) {
if (predicate(t.desc)) return t.desc;
}
return undefined;
}
getAllTracks(): TrackDescriptor[] {
return Array.from(this.tracks.valuesAsArray().map((t) => t.desc));
}
// Look up track into for a given track's URI.
// Returns |undefined| if no track can be found.
getTrack(uri: string): TrackDescriptor | undefined {
return this.tracks.tryGet(uri)?.desc;
}
// This is only called by the viewer_page.ts.
getTrackRenderer(uri: string): TrackRenderer | undefined {
// Search for a cached version of this track,
const trackFsm = this.tracks.tryGet(uri);
trackFsm?.markUsed();
return trackFsm;
}
// Destroys all tracks that didn't recently get a getTrackRenderer() call.
flushOldTracks() {
for (const trackFsm of this.tracks.values()) {
trackFsm.tick();
}
}
}
const DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT = 1;
/**
* Owns all runtime information about a track and manages its lifecycle,
* ensuring lifecycle hooks are called synchronously and in the correct order.
*
* There are quite some subtle properties that this class guarantees:
* - It make sure that lifecycle methods don't overlap with each other.
* - It prevents a chain of onCreate > onDestroy > onCreate if the first
* onCreate() is still oustanding. This is by virtue of using AsyncLimiter
* which under the hoods holds only the most recent task and skips the
* intermediate ones.
* - Ensures that a track never sees two consecutive onCreate, or onDestroy or
* an onDestroy without an onCreate.
* - Ensures that onUpdate never overlaps or follows with onDestroy. This is
* particularly important because tracks often drop tables/views onDestroy
* and they shouldn't try to fetch more data onUpdate past that point.
*/
class TrackFSM implements TrackRenderer {
public readonly desc: TrackDescriptor;
private readonly limiter = new AsyncLimiter();
private error?: Error;
private tickSinceLastUsed = 0;
private created = false;
constructor(desc: TrackDescriptor) {
this.desc = desc;
}
markUsed(): void {
this.tickSinceLastUsed = 0;
}
// Increment the lastUsed counter, and maybe call onDestroy().
tick(): void {
if (this.tickSinceLastUsed++ === DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT) {
// Schedule an onDestroy
this.limiter.schedule(async () => {
// Don't enter the track again once an error is has occurred
if (this.error !== undefined) {
return;
}
try {
if (this.created) {
await Promise.resolve(this.track.onDestroy?.());
this.created = false;
}
} catch (e) {
this.error = e;
}
});
}
}
render(ctx: TrackRenderContext): void {
this.limiter.schedule(async () => {
// Don't enter the track again once an error has occurred
if (this.error !== undefined) {
return;
}
try {
// Call onCreate() if this is our first call
if (!this.created) {
await this.track.onCreate?.(ctx);
this.created = true;
}
await Promise.resolve(this.track.onUpdate?.(ctx));
} catch (e) {
this.error = e;
}
});
this.track.render(ctx);
}
getError(): Error | undefined {
return this.error;
}
get track(): Track {
return this.desc.track;
}
}