xref: /aosp_15_r20/external/perfetto/ui/src/core/track_manager.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
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