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