• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
17  TrackRenderer,
18  Track,
19  TrackManager,
20  TrackFilterCriteria,
21} from '../public/track';
22import {AsyncLimiter} from '../base/async_limiter';
23import {TrackRenderContext} from '../public/track';
24import {TrackNode} from '../public/workspace';
25import {TraceImpl} from './trace_impl';
26
27export interface TrackWithFSM {
28  readonly track: TrackRenderer;
29  desc: Track;
30  render(ctx: TrackRenderContext): void;
31  getError(): Error | undefined;
32}
33
34export class TrackFilterState {
35  public nameFilter: string = '';
36  public criteriaFilters = new Map<string, string[]>();
37
38  // Clear all filters.
39  clearAll() {
40    this.nameFilter = '';
41    this.criteriaFilters.clear();
42  }
43
44  // Returns true if any filters are set.
45  areFiltersSet() {
46    return this.nameFilter !== '' || this.criteriaFilters.size > 0;
47  }
48}
49
50/**
51 * TrackManager is responsible for managing the registry of tracks and their
52 * lifecycle of tracks over render cycles.
53 *
54 * Example usage:
55 * function render() {
56 *   const trackCache = new TrackCache();
57 *   const foo = trackCache.getTrackFSM('foo', 'exampleURI', {});
58 *   const bar = trackCache.getTrackFSM('bar', 'exampleURI', {});
59 *   trackCache.flushOldTracks(); // <-- Destroys any unused cached tracks
60 * }
61 *
62 * Example of how flushing works:
63 * First cycle
64 *   getTrackFSM('foo', ...) <-- new track 'foo' created
65 *   getTrackFSM('bar', ...) <-- new track 'bar' created
66 *   flushTracks()
67 * Second cycle
68 *   getTrackFSM('foo', ...) <-- returns cached 'foo' track
69 *   flushTracks() <-- 'bar' is destroyed, as it was not resolved this cycle
70 * Third cycle
71 *   flushTracks() <-- 'foo' is destroyed.
72 */
73export class TrackManagerImpl implements TrackManager {
74  private tracks = new Registry<TrackFSMImpl>((x) => x.desc.uri);
75
76  // This property is written by scroll_helper.ts and read&cleared by the
77  // track_panel.ts. This exist for the following use case: the user wants to
78  // scroll to track X, but X is not visible because it's in a collapsed group.
79  // So we want to stash this information in a place that track_panel.ts can
80  // access when creating dom elements.
81  //
82  // Note: this is the node id of the track node to scroll to, not the track
83  // uri, as this allows us to scroll to tracks that have no uri.
84  scrollToTrackNodeId?: string;
85
86  // List of registered filter criteria.
87  readonly filterCriteria: TrackFilterCriteria[] = [];
88
89  // Current state of the track filters.
90  readonly filters = new TrackFilterState();
91
92  registerTrack(trackDesc: Track): Disposable {
93    return this.tracks.register(new TrackFSMImpl(trackDesc));
94  }
95
96  findTrack(
97    predicate: (desc: Track) => boolean | undefined,
98  ): Track | undefined {
99    for (const t of this.tracks.values()) {
100      if (predicate(t.desc)) return t.desc;
101    }
102    return undefined;
103  }
104
105  getAllTracks(): Track[] {
106    return Array.from(this.tracks.valuesAsArray().map((t) => t.desc));
107  }
108
109  // Look up track into for a given track's URI.
110  // Returns |undefined| if no track can be found.
111  getTrack(uri: string): Track | undefined {
112    return this.tracks.tryGet(uri)?.desc;
113  }
114
115  // This is only called by the viewer_page.ts.
116  getTrackFSM(uri: string): TrackWithFSM | undefined {
117    // Search for a cached version of this track,
118    const trackFsm = this.tracks.tryGet(uri);
119    trackFsm?.markUsed();
120    return trackFsm;
121  }
122
123  // Destroys all tracks that didn't recently get a getTrackRenderer() call.
124  flushOldTracks() {
125    for (const trackFsm of this.tracks.values()) {
126      trackFsm.tick();
127    }
128  }
129
130  registerTrackFilterCriteria(filter: TrackFilterCriteria): void {
131    this.filterCriteria.push(filter);
132  }
133
134  get trackFilterCriteria(): ReadonlyArray<TrackFilterCriteria> {
135    return this.filterCriteria;
136  }
137}
138
139const DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT = 1;
140
141/**
142 * Owns all runtime information about a track and manages its lifecycle,
143 * ensuring lifecycle hooks are called synchronously and in the correct order.
144 *
145 * There are quite some subtle properties that this class guarantees:
146 * - It make sure that lifecycle methods don't overlap with each other.
147 * - It prevents a chain of onCreate > onDestroy > onCreate if the first
148 *   onCreate() is still oustanding. This is by virtue of using AsyncLimiter
149 *   which under the hoods holds only the most recent task and skips the
150 *   intermediate ones.
151 * - Ensures that a track never sees two consecutive onCreate, or onDestroy or
152 *   an onDestroy without an onCreate.
153 * - Ensures that onUpdate never overlaps or follows with onDestroy. This is
154 *   particularly important because tracks often drop tables/views onDestroy
155 *   and they shouldn't try to fetch more data onUpdate past that point.
156 */
157class TrackFSMImpl implements TrackWithFSM {
158  public readonly desc: Track;
159
160  private readonly limiter = new AsyncLimiter();
161  private error?: Error;
162  private tickSinceLastUsed = 0;
163  private created = false;
164
165  constructor(desc: Track) {
166    this.desc = desc;
167  }
168
169  markUsed(): void {
170    this.tickSinceLastUsed = 0;
171  }
172
173  // Increment the lastUsed counter, and maybe call onDestroy().
174  tick(): void {
175    if (this.tickSinceLastUsed++ === DESTROY_IF_NOT_SEEN_FOR_TICK_COUNT) {
176      // Schedule an onDestroy
177      this.limiter.schedule(async () => {
178        // Don't enter the track again once an error is has occurred
179        if (this.error !== undefined) {
180          return;
181        }
182
183        try {
184          if (this.created) {
185            await Promise.resolve(this.track.onDestroy?.());
186            this.created = false;
187          }
188        } catch (e) {
189          this.error = e;
190        }
191      });
192    }
193  }
194
195  render(ctx: TrackRenderContext): void {
196    this.limiter.schedule(async () => {
197      // Don't enter the track again once an error has occurred
198      if (this.error !== undefined) {
199        return;
200      }
201
202      try {
203        // Call onCreate() if this is our first call
204        if (!this.created) {
205          await this.track.onCreate?.(ctx);
206          this.created = true;
207        }
208        await Promise.resolve(this.track.onUpdate?.(ctx));
209      } catch (e) {
210        this.error = e;
211      }
212    });
213    this.track.render(ctx);
214  }
215
216  getError(): Error | undefined {
217    return this.error;
218  }
219
220  get track(): TrackRenderer {
221    return this.desc.track;
222  }
223}
224
225// Returns true if a track matches the configured track filters.
226export function trackMatchesFilter(
227  trace: TraceImpl,
228  track: TrackNode,
229): boolean {
230  const filters = trace.tracks.filters;
231
232  // Check the name filter.
233  if (filters.nameFilter !== '') {
234    // Split terms on commas and remove the whitespace.
235    const nameFilters = filters.nameFilter
236      .split(',')
237      .map((s) => s.trim())
238      .filter((s) => s !== '');
239
240    // At least one of the name filter terms must match.
241    const trackTitleLower = track.title.toLowerCase();
242    if (
243      !nameFilters.some((nameFilter) =>
244        trackTitleLower.includes(nameFilter.toLowerCase()),
245      )
246    ) {
247      return false;
248    }
249  }
250
251  // Check all the criteria filters.
252  for (const [criteriaName, values] of filters.criteriaFilters) {
253    const criteriaFilter = trace.tracks.trackFilterCriteria.find(
254      (c) => c.name === criteriaName,
255    );
256
257    if (!criteriaFilter) {
258      continue;
259    }
260
261    // At least one of the criteria filters must match.
262    if (!values.some((value) => criteriaFilter.predicate(track, value))) {
263      return false;
264    }
265  }
266
267  return true;
268}
269