• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 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 {
16  Plugin,
17  PluginContext,
18  PluginContextTrace,
19  PluginDescriptor,
20  TrackRef,
21} from '../../public';
22
23const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
24const SAVED_TRACKS_KEY = `${PLUGIN_ID}#savedPerfettoTracks`;
25
26/**
27 * Fuzzy save and restore of pinned tracks.
28 *
29 * Tries to persist pinned tracks. Uses full string matching between track name
30 * and group name. When no match is found for a saved track, it tries again
31 * without numbers.
32 */
33class RestorePinnedTrack implements Plugin {
34  onActivate(_ctx: PluginContext): void {}
35
36  private ctx!: PluginContextTrace;
37
38  async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
39    this.ctx = ctx;
40    ctx.registerCommand({
41      id: `${PLUGIN_ID}#save`,
42      name: 'Save: Pinned tracks',
43      callback: () => {
44        this.saveTracks();
45      },
46    });
47    ctx.registerCommand({
48      id: `${PLUGIN_ID}#restore`,
49      name: 'Restore: Pinned tracks',
50      callback: () => {
51        this.restoreTracks();
52      },
53    });
54  }
55
56  private saveTracks() {
57    const pinnedTracks = this.ctx.timeline.tracks.filter(
58      (trackRef) => trackRef.isPinned,
59    );
60    const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((trackRef) => ({
61      groupName: trackRef.groupName,
62      trackName: trackRef.displayName,
63    }));
64    window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(tracksToSave));
65  }
66
67  private restoreTracks() {
68    const savedTracks = window.localStorage.getItem(SAVED_TRACKS_KEY);
69    if (!savedTracks) {
70      alert('No saved tracks. Use the Save command first');
71      return;
72    }
73    const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
74    const tracks: TrackRef[] = this.ctx.timeline.tracks;
75    tracksToRestore.forEach((trackToRestore) => {
76      // Check for an exact match
77      const exactMatch = tracks.find((track) => {
78        return (
79          track.key &&
80          trackToRestore.trackName === track.displayName &&
81          trackToRestore.groupName === track.groupName
82        );
83      });
84
85      if (exactMatch) {
86        this.ctx.timeline.pinTrack(exactMatch.key!);
87      } else {
88        // We attempt a match after removing numbers to potentially pin a
89        // "similar" track from a different trace. Removing numbers allows
90        // flexibility; for instance, with multiple 'sysui' processes (e.g.
91        // track group name: "com.android.systemui 123") without this approach,
92        // any could be mistakenly pinned. The goal is to restore specific
93        // tracks within the same trace, ensuring that a previously pinned track
94        // is pinned again.
95        // If the specific process with that PID is unavailable, pinning any
96        // other process matching the package name is attempted.
97        const fuzzyMatch = tracks.find((track) => {
98          return (
99            track.key &&
100            this.removeNumbers(trackToRestore.trackName) ===
101              this.removeNumbers(track.displayName) &&
102            this.removeNumbers(trackToRestore.groupName) ===
103              this.removeNumbers(track.groupName)
104          );
105        });
106
107        if (fuzzyMatch) {
108          this.ctx.timeline.pinTrack(fuzzyMatch.key!);
109        } else {
110          console.warn(
111            '[RestorePinnedTracks] No track found that matches',
112            trackToRestore,
113          );
114        }
115      }
116    });
117  }
118
119  private removeNumbers(inputString?: string): string | undefined {
120    return inputString?.replace(/\d+/g, '');
121  }
122}
123
124interface SavedPinnedTrack {
125  // Optional: group name for the track. Usually matches with process name.
126  groupName?: string;
127
128  // Track name to restore.
129  trackName: string;
130}
131
132export const plugin: PluginDescriptor = {
133  pluginId: PLUGIN_ID,
134  plugin: RestorePinnedTrack,
135};
136