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