Merge "Add plugin to save and restore pinned track on a best effort" into main
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index ba60270..0750e2e 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -299,11 +299,20 @@
},
get tracks(): TrackRef[] {
- return Object.values(globals.state.tracks).map((trackState) => {
+ const tracks = Object.values(globals.state.tracks);
+ const pinnedTracks = globals.state.pinnedTracks;
+ const groups = globals.state.trackGroups;
+ return tracks.map((trackState) => {
+ const group = trackState.trackGroup
+ ? groups[trackState.trackGroup]
+ : undefined;
return {
displayName: trackState.name,
uri: trackState.uri,
params: trackState.params,
+ key: trackState.key,
+ groupName: group?.name,
+ isPinned: pinnedTracks.includes(trackState.key),
};
});
},
diff --git a/ui/src/core/default_plugins.ts b/ui/src/core/default_plugins.ts
index 44634a3..a9f8395 100644
--- a/ui/src/core/default_plugins.ts
+++ b/ui/src/core/default_plugins.ts
@@ -32,6 +32,7 @@
'dev.perfetto.BookmarkletApi',
'dev.perfetto.CoreCommands',
'dev.perfetto.LargeScreensPerf',
+ 'dev.perfetto.RestorePinnedTrack',
'perfetto.AndroidLog',
'perfetto.Annotation',
'perfetto.AsyncSlices',
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/OWNERS b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/OWNERS
new file mode 100644
index 0000000..987684d
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/OWNERS
@@ -0,0 +1,2 @@
+nicomazz@google.com
+nickchameyev@google.com
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
new file mode 100644
index 0000000..81036db
--- /dev/null
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -0,0 +1,135 @@
+// Copyright (C) 2024 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 {
+ Plugin,
+ PluginContext,
+ PluginContextTrace,
+ PluginDescriptor,
+ TrackRef,
+} from '../../public';
+
+const PLUGIN_ID = 'dev.perfetto.RestorePinnedTrack';
+const SAVED_TRACKS_KEY = `${PLUGIN_ID}#savedPerfettoTracks`;
+
+/**
+ * Fuzzy save and restore of pinned tracks.
+ *
+ * Tries to persist pinned tracks. Uses full string matching between track name
+ * and group name. When no match is found for a saved track, it tries again
+ * without numbers.
+ */
+class RestorePinnedTrack implements Plugin {
+ onActivate(_ctx: PluginContext): void {}
+
+ private ctx!: PluginContextTrace;
+
+ async onTraceLoad(ctx: PluginContextTrace): Promise<void> {
+ this.ctx = ctx;
+ ctx.registerCommand({
+ id: `${PLUGIN_ID}#save`,
+ name: 'Save: Pinned tracks',
+ callback: () => {
+ this.saveTracks();
+ },
+ });
+ ctx.registerCommand({
+ id: `${PLUGIN_ID}#restore`,
+ name: 'Restore: Pinned tracks',
+ callback: () => {
+ this.restoreTracks();
+ },
+ });
+ }
+
+ private saveTracks() {
+ const pinnedTracks = this.ctx.timeline.tracks.filter(
+ (trackRef) => trackRef.isPinned,
+ );
+ const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((trackRef) => ({
+ groupName: trackRef.groupName,
+ trackName: trackRef.displayName,
+ }));
+ window.localStorage.setItem(SAVED_TRACKS_KEY, JSON.stringify(tracksToSave));
+ }
+
+ private restoreTracks() {
+ const savedTracks = window.localStorage.getItem(SAVED_TRACKS_KEY);
+ if (!savedTracks) {
+ alert('No saved tracks. Use the Save command first');
+ return;
+ }
+ const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
+ const tracks: TrackRef[] = this.ctx.timeline.tracks;
+ tracksToRestore.forEach((trackToRestore) => {
+ // Check for an exact match
+ const exactMatch = tracks.find((track) => {
+ return (
+ track.key &&
+ trackToRestore.trackName === track.displayName &&
+ trackToRestore.groupName === track.groupName
+ );
+ });
+
+ if (exactMatch) {
+ this.ctx.timeline.pinTrack(exactMatch.key!);
+ } else {
+ // We attempt a match after removing numbers to potentially pin a
+ // "similar" track from a different trace. Removing numbers allows
+ // flexibility; for instance, with multiple 'sysui' processes (e.g.
+ // track group name: "com.android.systemui 123") without this approach,
+ // any could be mistakenly pinned. The goal is to restore specific
+ // tracks within the same trace, ensuring that a previously pinned track
+ // is pinned again.
+ // If the specific process with that PID is unavailable, pinning any
+ // other process matching the package name is attempted.
+ const fuzzyMatch = tracks.find((track) => {
+ return (
+ track.key &&
+ this.removeNumbers(trackToRestore.trackName) ===
+ this.removeNumbers(track.displayName) &&
+ this.removeNumbers(trackToRestore.groupName) ===
+ this.removeNumbers(track.groupName)
+ );
+ });
+
+ if (fuzzyMatch) {
+ this.ctx.timeline.pinTrack(fuzzyMatch.key!);
+ } else {
+ console.warn(
+ '[RestorePinnedTracks] No track found that matches',
+ trackToRestore,
+ );
+ }
+ }
+ });
+ }
+
+ private removeNumbers(inputString?: string): string | undefined {
+ return inputString?.replace(/\d+/g, '');
+ }
+}
+
+interface SavedPinnedTrack {
+ // Optional: group name for the track. Usually matches with process name.
+ groupName?: string;
+
+ // Track name to restore.
+ trackName: string;
+}
+
+export const plugin: PluginDescriptor = {
+ pluginId: PLUGIN_ID,
+ plugin: RestorePinnedTrack,
+};
diff --git a/ui/src/public/index.ts b/ui/src/public/index.ts
index ece6ea0..f3e4338 100644
--- a/ui/src/public/index.ts
+++ b/ui/src/public/index.ts
@@ -482,6 +482,12 @@
// Optional: Add tracks to a group with this name.
groupName?: string;
+
+ // Optional: Track key
+ key?: string;
+
+ // Optional: Whether the track is pinned
+ isPinned?: boolean;
}
// A predicate for selecting a subset of tracks.