blob: b9f10036dc2c5470f5520e7e1eff9fcb653a2d73 [file]
// 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 type {
TrackRenderer,
Track,
TrackManager,
TrackFilterCriteria,
Overlay,
} from '../public/track';
import type {TrackRenderContext} from '../public/track';
import type {TrackNode} from '../public/workspace';
import type {TraceImpl} from './trace_impl';
export interface TrackWrapper {
readonly track: TrackRenderer;
readonly desc: Track;
render(ctx: TrackRenderContext): void;
getError(): Error | undefined;
}
export class TrackFilterState {
public nameFilter: string = '';
public criteriaFilters = new Map<string, string[]>();
// Clear all filters.
clearAll() {
this.nameFilter = '';
this.criteriaFilters.clear();
}
// Returns true if any filters are set.
areFiltersSet() {
return this.nameFilter !== '' || this.criteriaFilters.size > 0;
}
}
/**
* TrackManager is responsible for managing the registry of tracks.
* Tracks are registered via registerTrack() and looked up via getWrappedTrack().
* Each track is wrapped in a TrackWrapperImpl which handles error containment.
*/
export class TrackManagerImpl implements TrackManager {
private readonly tracks = new Registry<TrackWrapperImpl>((x) => x.desc.uri);
private readonly _overlays: Overlay[] = [];
// 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;
// List of registered filter criteria.
readonly filterCriteria: TrackFilterCriteria[] = [];
// Current state of the track filters.
readonly filters = new TrackFilterState();
registerTrack(trackDesc: Track): Disposable {
return this.tracks.register(new TrackWrapperImpl(trackDesc));
}
registerOverlay(overlay: Overlay): Disposable {
this._overlays.push(overlay);
return {
[Symbol.dispose]: () => {
const index = this._overlays.indexOf(overlay);
if (index !== -1) {
this._overlays.splice(index, 1);
}
},
};
}
findTrack(
predicate: (desc: Track) => boolean | undefined,
): Track | undefined {
for (const t of this.tracks.values()) {
if (predicate(t.desc)) return t.desc;
}
return undefined;
}
getAllTracks(): Track[] {
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): Track | undefined {
return this.tracks.tryGet(uri)?.desc;
}
// Returns a wrapped track that provides error containment for rendering.
getWrappedTrack(uri: string): TrackWrapper | undefined {
return this.tracks.tryGet(uri);
}
registerTrackFilterCriteria(filter: TrackFilterCriteria): void {
this.filterCriteria.push(filter);
}
get trackFilterCriteria(): ReadonlyArray<TrackFilterCriteria> {
return this.filterCriteria;
}
get overlays(): ReadonlyArray<Overlay> {
return this._overlays;
}
}
/**
* Wraps a track and provides error containment. If render() throws, the error
* is captured and subsequent render() calls become no-ops. This prevents a
* crashing track from breaking the entire UI.
*/
class TrackWrapperImpl implements TrackWrapper {
public readonly desc: Track;
private error?: Error;
constructor(desc: Track) {
this.desc = desc;
}
render(ctx: TrackRenderContext): void {
// Don't enter the track again once an error is has occurred
if (this.error) {
return;
}
try {
this.track.render(ctx);
} catch (e) {
this.error = e;
}
}
getError(): Error | undefined {
return this.error;
}
get track(): TrackRenderer {
return this.desc.renderer;
}
}
// Returns true if a track matches the configured track filters.
export function trackMatchesFilter(
trace: TraceImpl,
track: TrackNode,
): boolean {
const filters = trace.tracks.filters;
// Check the name filter.
if (filters.nameFilter !== '') {
// Split terms on commas and remove the whitespace.
const nameFilters = filters.nameFilter
.split(',')
.map((s) => s.trim())
.filter((s) => s !== '');
// At least one of the name filter terms must match.
const trackTitleLower = track.name.toLowerCase();
if (
!nameFilters.some((nameFilter) =>
trackTitleLower.includes(nameFilter.toLowerCase()),
)
) {
return false;
}
}
// Check all the criteria filters.
for (const [criteriaName, values] of filters.criteriaFilters) {
const criteriaFilter = trace.tracks.trackFilterCriteria.find(
(c) => c.name === criteriaName,
);
if (!criteriaFilter) {
continue;
}
// At least one of the criteria filters must match.
if (!values.some((value) => criteriaFilter.predicate(track, value))) {
return false;
}
}
return true;
}