Merge "ui: relocate registerXXX methods under App{tracks,tabs}" into main
diff --git a/ui/src/common/plugins.ts b/ui/src/common/plugins.ts
index 3a3ac40..19ad6a9 100644
--- a/ui/src/common/plugins.ts
+++ b/ui/src/common/plugins.ts
@@ -31,7 +31,7 @@
 import {defaultPlugins} from '../core/default_plugins';
 import {PromptOption} from '../public/omnibox';
 import {DisposableStack} from '../base/disposable_stack';
-import {TraceContext} from '../frontend/trace_context';
+import {TraceInfo} from '../public/trace_info';
 import {Workspace} from '../public/workspace';
 import {Migrate, Store} from '../base/store';
 import {LegacyDetailsPanel} from '../public/details_panel';
@@ -43,30 +43,36 @@
 export class PluginContextImpl implements App, Disposable {
   private trash = new DisposableStack();
   private alive = true;
+  readonly commands;
+  readonly sidebar;
 
-  registerCommand(cmd: Command): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
+  constructor(readonly pluginId: string) {
+    const thiz = this;
+    this.commands = {
+      registerCommand(cmd: Command): void {
+        // Silently ignore if context is dead.
+        if (!thiz.alive) return;
 
-    const disposable = globals.commandManager.registerCommand(cmd);
-    this.trash.use(disposable);
+        const disposable = globals.commandManager.registerCommand(cmd);
+        thiz.trash.use(disposable);
+      },
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      runCommand(id: string, ...args: any[]): any {
+        return globals.commandManager.runCommand(id, ...args);
+      },
+    };
+
+    this.sidebar = {
+      addSidebarMenuItem(menuItem: SidebarMenuItem): void {
+        thiz.trash.use(globals.sidebarMenuItems.register(menuItem));
+      },
+    };
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
-    return globals.commandManager.runCommand(id, ...args);
-  }
-
-  constructor(readonly pluginId: string) {}
-
   [Symbol.dispose]() {
     this.trash.dispose();
     this.alive = false;
   }
-
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void {
-    this.trash.use(globals.sidebarMenuItems.register(menuItem));
-  }
 }
 
 // This PluginContextTrace implementation provides the plugin access to trace
@@ -77,6 +83,10 @@
   private trash = new DisposableStack();
   private alive = true;
   readonly engine: Engine;
+  readonly commands;
+  readonly tracks;
+  readonly tabs;
+  readonly sidebar;
 
   constructor(
     private ctx: App,
@@ -85,49 +95,70 @@
     const engineProxy = engine.getProxy(ctx.pluginId);
     this.trash.use(engineProxy);
     this.engine = engineProxy;
-  }
+    const thiz = this;
 
-  registerCommand(cmd: Command): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
+    this.commands = {
+      registerCommand(cmd: Command): void {
+        // Silently ignore if context is dead.
+        if (!thiz.alive) return;
 
-    const dispose = globals.commandManager.registerCommand(cmd);
-    this.trash.use(dispose);
-  }
+        const dispose = globals.commandManager.registerCommand(cmd);
+        thiz.trash.use(dispose);
+      },
 
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
+      // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      runCommand(id: string, ...args: any[]): any {
+        return ctx.commands.runCommand(id, ...args);
+      },
+    };
 
-    this.trash.use(globals.sidebarMenuItems.register(menuItem));
-  }
+    this.tracks = {
+      registerTrack(trackDesc: TrackDescriptor): void {
+        // Silently ignore if context is dead.
+        if (!thiz.alive) return;
 
-  registerTrack(trackDesc: TrackDescriptor): void {
-    // Silently ignore if context is dead.
-    if (!this.alive) return;
+        const dispose = globals.trackManager.registerTrack({
+          ...trackDesc,
+          pluginId: thiz.pluginId,
+        });
+        thiz.trash.use(dispose);
+      },
+    };
 
-    const dispose = globals.trackManager.registerTrack({
-      ...trackDesc,
-      pluginId: this.pluginId,
-    });
-    this.trash.use(dispose);
-  }
+    this.tabs = {
+      registerTab(desc: TabDescriptor): void {
+        if (!thiz.alive) return;
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any {
-    return this.ctx.runCommand(id, ...args);
-  }
+        const unregister = globals.tabManager.registerTab(desc);
+        thiz.trash.use(unregister);
+      },
 
-  registerTab(desc: TabDescriptor): void {
-    if (!this.alive) return;
+      addDefaultTab(uri: string): void {
+        const remove = globals.tabManager.addDefaultTab(uri);
+        thiz.trash.use(remove);
+      },
 
-    const unregister = globals.tabManager.registerTab(desc);
-    this.trash.use(unregister);
-  }
+      openQuery: (query: string, title: string) => {
+        addQueryResultsTab({query, title});
+      },
 
-  addDefaultTab(uri: string): void {
-    const remove = globals.tabManager.addDefaultTab(uri);
-    this.trash.use(remove);
+      showTab(uri: string): void {
+        globals.tabManager.showTab(uri);
+      },
+
+      hideTab(uri: string): void {
+        globals.tabManager.hideTab(uri);
+      },
+    };
+
+    this.sidebar = {
+      addSidebarMenuItem(menuItem: SidebarMenuItem): void {
+        // Silently ignore if context is dead.
+        if (!thiz.alive) return;
+
+        thiz.trash.use(globals.sidebarMenuItems.register(menuItem));
+      },
+    };
   }
 
   registerDetailsPanel(detailsPanel: LegacyDetailsPanel): void {
@@ -138,20 +169,6 @@
     this.trash.use(unregister);
   }
 
-  readonly tabs = {
-    openQuery: (query: string, title: string) => {
-      addQueryResultsTab({query, title});
-    },
-
-    showTab(uri: string): void {
-      globals.tabManager.showTab(uri);
-    },
-
-    hideTab(uri: string): void {
-      globals.tabManager.hideTab(uri);
-    },
-  };
-
   get pluginId(): string {
     return this.ctx.pluginId;
   }
@@ -168,12 +185,12 @@
     get viewport(): TimeSpan {
       return globals.timeline.visibleWindow.toTimeSpan();
     },
-
-    get workspace(): Workspace {
-      return globals.workspace;
-    },
   };
 
+  get workspace(): Workspace {
+    return globals.workspace;
+  }
+
   [Symbol.dispose]() {
     this.trash.dispose();
     this.alive = false;
@@ -183,7 +200,7 @@
     return globals.store.createSubStore(['plugins', this.pluginId], migrate);
   }
 
-  get trace(): TraceContext {
+  get trace(): TraceInfo {
     return globals.traceContext;
   }
 
diff --git a/ui/src/controller/trace_controller.ts b/ui/src/controller/trace_controller.ts
index 44317aa..9a77279 100644
--- a/ui/src/controller/trace_controller.ts
+++ b/ui/src/controller/trace_controller.ts
@@ -84,7 +84,7 @@
   deserializeAppStatePhase1,
   deserializeAppStatePhase2,
 } from '../common/state_serialization';
-import {TraceContext} from '../frontend/trace_context';
+import {TraceInfo} from '../public/trace_info';
 import {ProfileType, profileType} from '../public/selection';
 
 type States = 'init' | 'loading_trace' | 'ready';
@@ -1141,7 +1141,7 @@
 async function getTraceTimeDetails(
   engine: Engine,
   engineCfg: EngineConfig,
-): Promise<TraceContext> {
+): Promise<TraceInfo> {
   const traceTime = await getTraceTimeBounds(engine);
 
   // Find the first REALTIME or REALTIME_COARSE clock snapshot.
diff --git a/ui/src/core/search_data.ts b/ui/src/core/search_data.ts
new file mode 100644
index 0000000..cd5856b
--- /dev/null
+++ b/ui/src/core/search_data.ts
@@ -0,0 +1,30 @@
+// Copyright (C) 2019 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.
+
+export type SearchSource = 'cpu' | 'log' | 'slice' | 'track';
+
+export interface SearchSummary {
+  tsStarts: BigInt64Array;
+  tsEnds: BigInt64Array;
+  count: Uint8Array;
+}
+
+export interface CurrentSearchResults {
+  eventIds: Float64Array;
+  tses: BigInt64Array;
+  utids: Float64Array;
+  trackUris: string[];
+  sources: SearchSource[];
+  totalResults: number;
+}
diff --git a/ui/src/core_plugins/android_log/index.ts b/ui/src/core_plugins/android_log/index.ts
index b4375b0..d263f6f 100644
--- a/ui/src/core_plugins/android_log/index.ts
+++ b/ui/src/core_plugins/android_log/index.ts
@@ -55,13 +55,13 @@
     const uri = 'perfetto.AndroidLog';
     const title = 'Android logs';
     if (logCount > 0) {
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title,
         tags: {kind: ANDROID_LOGS_TRACK_KIND},
         track: new AndroidLogTrack(ctx.engine),
       });
-      ctx.timeline.workspace.insertChildInOrder(new TrackNode(uri, title));
+      ctx.workspace.insertChildInOrder(new TrackNode(uri, title));
     }
 
     const androidLogsTabUri = 'perfetto.AndroidLog#tab';
@@ -72,7 +72,7 @@
       (x) => x as LogFilteringCriteria,
     );
 
-    ctx.registerTab({
+    ctx.tabs.registerTab({
       isEphemeral: false,
       uri: androidLogsTabUri,
       content: {
@@ -83,10 +83,10 @@
     });
 
     if (logCount > 0) {
-      ctx.addDefaultTab(androidLogsTabUri);
+      ctx.tabs.addDefaultTab(androidLogsTabUri);
     }
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.AndroidLog#ShowLogsTab',
       name: 'Show android logs tab',
       callback: () => {
diff --git a/ui/src/core_plugins/annotation/index.ts b/ui/src/core_plugins/annotation/index.ts
index 6d11848..9bd76fa 100644
--- a/ui/src/core_plugins/annotation/index.ts
+++ b/ui/src/core_plugins/annotation/index.ts
@@ -54,7 +54,7 @@
       const {id, name, upid, groupName} = it;
 
       const uri = `/annotation_${id}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -87,15 +87,15 @@
           group.headerTrackUri = uri;
           container = group;
           groups.set(groupName, group);
-          ctx.timeline.workspace.insertChildInOrder(group);
+          ctx.workspace.insertChildInOrder(group);
         } else {
           container = existingGroup;
         }
       } else {
         if (upid !== 0) {
-          container = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+          container = getOrCreateGroupForProcess(ctx.workspace, upid);
         } else {
-          container = ctx.timeline.workspace;
+          container = ctx.workspace;
         }
       }
 
@@ -126,7 +126,7 @@
       const {id: trackId, name, upid} = counterIt;
 
       const uri = `/annotation_counter_${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -143,7 +143,7 @@
         }),
       });
 
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       group.insertChildInOrder(new TrackNode(uri, name));
     }
   }
diff --git a/ui/src/core_plugins/async_slices/index.ts b/ui/src/core_plugins/async_slices/index.ts
index 0729c79..5170bcc 100644
--- a/ui/src/core_plugins/async_slices/index.ts
+++ b/ui/src/core_plugins/async_slices/index.ts
@@ -73,7 +73,7 @@
       const maxDepth = it.maxDepth;
 
       const uri = `/async_slices_${rawName}_${it.parentId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -85,7 +85,7 @@
       });
       const trackNode = new TrackNode(uri, displayName);
       trackNode.sortOrder = -25;
-      ctx.timeline.workspace.insertChildInOrder(trackNode);
+      ctx.workspace.insertChildInOrder(trackNode);
     }
   }
 
@@ -130,7 +130,7 @@
       });
 
       const uri = `/process_${upid}/async_slices_${rawTrackIds}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -145,7 +145,7 @@
           trackIds,
         ),
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = 30;
       group.insertChildInOrder(track);
@@ -207,7 +207,7 @@
       });
 
       const uri = `/${getThreadUriPrefix(upid, utid)}_slice_${rawTrackIds}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -226,7 +226,7 @@
           trackIds,
         ),
       });
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = 20;
       group.insertChildInOrder(track);
@@ -283,7 +283,7 @@
       });
 
       const uri = `/async_slices_${name}_${uid}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
diff --git a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
index 3617d2b..63f549d 100644
--- a/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
+++ b/ui/src/core_plugins/chrome_critical_user_interactions/index.ts
@@ -36,7 +36,7 @@
 
 class CriticalUserInteractionPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri: CriticalUserInteractionTrack.kind,
       tags: {
         kind: CriticalUserInteractionTrack.kind,
@@ -108,7 +108,7 @@
   }
 
   onActivate(ctx: App): void {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CriticalUserInteraction.AddInteractionTrack',
       name: 'Add track: Chrome interactions',
       callback: () => addCriticalUserInteractionTrack(),
diff --git a/ui/src/core_plugins/chrome_scroll_jank/index.ts b/ui/src/core_plugins/chrome_scroll_jank/index.ts
index 41e85e7..e66e3f2 100644
--- a/ui/src/core_plugins/chrome_scroll_jank/index.ts
+++ b/ui/src/core_plugins/chrome_scroll_jank/index.ts
@@ -88,7 +88,7 @@
       await this.addEventLatencyTrack(ctx, group);
       await this.addScrollJankV3ScrollTrack(ctx, group);
       await ScrollJankCauseMap.initialize(ctx.engine);
-      ctx.timeline.workspace.insertChildInOrder(group);
+      ctx.workspace.insertChildInOrder(group);
       group.expand();
     }
   }
@@ -114,7 +114,7 @@
     const {upid, utid} = it;
     const uri = 'perfetto.ChromeScrollJank';
     const displayName = 'Scroll Jank causes - long tasks';
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: displayName,
       tags: {
@@ -127,7 +127,7 @@
         uri,
       }),
     });
-    const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+    const group = getOrCreateGroupForThread(ctx.workspace, utid);
     const track = new TrackNode(uri, displayName);
     group.insertChildInOrder(track);
   }
@@ -144,7 +144,7 @@
     const uri = 'perfetto.ChromeScrollJank#toplevelScrolls';
     const title = 'Chrome Scrolls';
 
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title,
       tags: {
@@ -281,7 +281,7 @@
     const uri = 'perfetto.ChromeScrollJank#eventLatency';
     const title = 'Chrome Scroll Input Latencies';
 
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title,
       tags: {
@@ -324,7 +324,7 @@
     const uri = 'perfetto.ChromeScrollJank#scrollJankV3';
     const title = 'Chrome Scroll Janks';
 
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title,
       tags: {
diff --git a/ui/src/core_plugins/chrome_tasks/index.ts b/ui/src/core_plugins/chrome_tasks/index.ts
index 336b09a..f86fd0f 100644
--- a/ui/src/core_plugins/chrome_tasks/index.ts
+++ b/ui/src/core_plugins/chrome_tasks/index.ts
@@ -31,7 +31,7 @@
   async onTraceLoad(ctx: Trace) {
     await this.createTracks(ctx);
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'org.chromium.ChromeTasks.ShowChromeTasksTable',
       name: 'Show chrome_tasks table',
       callback: () =>
@@ -99,13 +99,13 @@
       const utid = it.utid;
       const uri = `org.chromium.ChromeTasks#thread.${utid}`;
       const title = `${it.threadName} ${it.tid}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         track: new ChromeTasksThreadTrack(ctx.engine, uri, asUtid(utid)),
         title,
       });
       group.insertChildInOrder(new TrackNode(uri, title));
-      ctx.timeline.workspace.insertChildInOrder(group);
+      ctx.workspace.insertChildInOrder(group);
     }
 
     ctx.registerDetailsPanel(
diff --git a/ui/src/core_plugins/commands/index.ts b/ui/src/core_plugins/commands/index.ts
index 4b780d2..f01c992 100644
--- a/ui/src/core_plugins/commands/index.ts
+++ b/ui/src/core_plugins/commands/index.ts
@@ -102,7 +102,7 @@
   private readonly disposable = new DisposableStack();
 
   onActivate(ctx: App) {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#ToggleLeftSidebar',
       name: 'Toggle left sidebar',
       callback: () => {
@@ -134,7 +134,7 @@
     });
 
     const OPEN_TRACE_COMMAND_ID = 'perfetto.CoreCommands#openTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_TRACE_COMMAND_ID,
       name: 'Open trace file',
       callback: () => {
@@ -143,7 +143,7 @@
       },
       defaultHotkey: '!Mod+O',
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addSidebarMenuItem({
       commandId: OPEN_TRACE_COMMAND_ID,
       group: 'navigation',
       icon: 'folder_open',
@@ -151,7 +151,7 @@
 
     const OPEN_LEGACY_TRACE_COMMAND_ID =
       'perfetto.CoreCommands#openTraceInLegacyUi';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_LEGACY_TRACE_COMMAND_ID,
       name: 'Open with legacy UI',
       callback: () => {
@@ -159,7 +159,7 @@
         input.click();
       },
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addSidebarMenuItem({
       commandId: OPEN_LEGACY_TRACE_COMMAND_ID,
       group: 'navigation',
       icon: 'filter_none',
@@ -167,7 +167,7 @@
   }
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryAllProcesses',
       name: 'Run query: All processes',
       callback: () => {
@@ -175,7 +175,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCpuTimeByProcess',
       name: 'Run query: CPU time by process',
       callback: () => {
@@ -183,7 +183,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCyclesByStateByCpu',
       name: 'Run query: cycles by p-state by CPU',
       callback: () => {
@@ -194,7 +194,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryCyclesByCpuByProcess',
       name: 'Run query: CPU Time by CPU by process',
       callback: () => {
@@ -205,7 +205,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#RunQueryHeapGraphBytesPerType',
       name: 'Run query: heap graph bytes per type',
       callback: () => {
@@ -216,7 +216,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#DebugSqlPerformance',
       name: 'Debug SQL performance',
       callback: () => {
@@ -224,32 +224,32 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#UnpinAllTracks',
       name: 'Unpin all pinned tracks',
       callback: () => {
-        const workspace = ctx.timeline.workspace;
+        const workspace = ctx.workspace;
         workspace.pinnedTracks.forEach((t) => workspace.unpinTrack(t));
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#ExpandAllGroups',
       name: 'Expand all track groups',
       callback: () => {
-        ctx.timeline.workspace.flatGroups.forEach((g) => g.expand());
+        ctx.workspace.flatGroups.forEach((g) => g.expand());
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#CollapseAllGroups',
       name: 'Collapse all track groups',
       callback: () => {
-        ctx.timeline.workspace.flatGroups.forEach((g) => g.collapse());
+        ctx.workspace.flatGroups.forEach((g) => g.collapse());
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#PanToTimestamp',
       name: 'Pan to timestamp',
       callback: (tsRaw: unknown) => {
@@ -268,7 +268,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CoreCommands#ShowCurrentSelectionTab',
       name: 'Show current selection tab',
       callback: () => {
@@ -276,7 +276,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: ADD_SQL_TABLE_TAB_COMMAND_ID,
       name: 'Open SQL table viewer',
       callback: (args: unknown) => {
@@ -292,7 +292,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'createNewEmptyWorkspace',
       name: 'Create new empty workspace',
       callback: async () => {
@@ -306,7 +306,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'switchWorkspace',
       name: 'Switch workspace',
       callback: async () => {
diff --git a/ui/src/core_plugins/counter/index.ts b/ui/src/core_plugins/counter/index.ts
index 4742ecd..458e2bf 100644
--- a/ui/src/core_plugins/counter/index.ts
+++ b/ui/src/core_plugins/counter/index.ts
@@ -181,7 +181,7 @@
       const unit = it.unit ?? undefined;
 
       const uri = `/counter_${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -202,9 +202,7 @@
           return await getCounterEventBounds(ctx.engine, trackId, id);
         },
       });
-      ctx.timeline.workspace.insertChildInOrder(
-        new TrackNode(uri, displayName),
-      );
+      ctx.workspace.insertChildInOrder(new TrackNode(uri, displayName));
     }
   }
 
@@ -252,7 +250,7 @@
       const name = it.name;
       const trackId = it.id;
       const uri = `counter.cpu.${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -273,7 +271,7 @@
       });
       const trackNode = new TrackNode(uri, name);
       trackNode.sortOrder = -20;
-      ctx.timeline.workspace.insertChildInOrder(trackNode);
+      ctx.workspace.insertChildInOrder(trackNode);
     }
   }
 
@@ -321,7 +319,7 @@
         threadTrack: true,
       });
       const uri = `${getThreadUriPrefix(upid, utid)}_counter_${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -342,7 +340,7 @@
           return await getCounterEventBounds(ctx.engine, trackId, id);
         },
       });
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, name);
       track.sortOrder = 30;
       group.insertChildInOrder(track);
@@ -384,7 +382,7 @@
         ...(exists(trackName) && {trackName}),
       });
       const uri = `/process_${upid}/counter_${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -404,7 +402,7 @@
           return await getCounterEventBounds(ctx.engine, trackId, id);
         },
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, name);
       track.sortOrder = 20;
       group.insertChildInOrder(track);
@@ -429,7 +427,7 @@
         const trackId = freqExistsResult.firstRow({id: NUM}).id;
         const uri = `/gpu_frequency_${gpu}`;
         const name = `Gpu ${gpu} Frequency`;
-        ctx.registerTrack({
+        ctx.tracks.registerTrack({
           uri,
           title: name,
           tags: {
@@ -450,7 +448,7 @@
         });
         const trackNode = new TrackNode(uri, name);
         trackNode.sortOrder = -20;
-        ctx.timeline.workspace.insertChildInOrder(trackNode);
+        ctx.workspace.insertChildInOrder(trackNode);
       }
     }
   }
diff --git a/ui/src/core_plugins/cpu_freq/index.ts b/ui/src/core_plugins/cpu_freq/index.ts
index 5924c16..6646135 100644
--- a/ui/src/core_plugins/cpu_freq/index.ts
+++ b/ui/src/core_plugins/cpu_freq/index.ts
@@ -69,7 +69,7 @@
 
         const uri = `/cpu_freq_cpu${cpu}`;
         const title = `Cpu ${cpu} Frequency`;
-        ctx.registerTrack({
+        ctx.tracks.registerTrack({
           uri,
           title,
           tags: {
@@ -80,7 +80,7 @@
         });
         const trackNode = new TrackNode(uri, title);
         trackNode.sortOrder = -40;
-        ctx.timeline.workspace.insertChildInOrder(trackNode);
+        ctx.workspace.insertChildInOrder(trackNode);
       }
     }
   }
diff --git a/ui/src/core_plugins/cpu_profile/index.ts b/ui/src/core_plugins/cpu_profile/index.ts
index 752b7f3..8a55533 100644
--- a/ui/src/core_plugins/cpu_profile/index.ts
+++ b/ui/src/core_plugins/cpu_profile/index.ts
@@ -67,7 +67,7 @@
       const threadName = it.threadName;
       const uri = `${getThreadUriPrefix(upid, utid)}_cpu_samples`;
       const displayName = `${threadName} (CPU Stack Samples)`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -83,7 +83,7 @@
           utid,
         ),
       });
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = -40;
       group.insertChildInOrder(track);
diff --git a/ui/src/core_plugins/cpu_slices/index.ts b/ui/src/core_plugins/cpu_slices/index.ts
index 9be74f74..04a8ff5 100644
--- a/ui/src/core_plugins/cpu_slices/index.ts
+++ b/ui/src/core_plugins/cpu_slices/index.ts
@@ -32,7 +32,7 @@
       const uri = `/sched_cpu${cpu}`;
 
       const name = size === undefined ? `Cpu ${cpu}` : `Cpu ${cpu} (${size})`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: name,
         tags: {
@@ -43,7 +43,7 @@
       });
       const trackNode = new TrackNode(uri, name);
       trackNode.sortOrder = -50;
-      ctx.timeline.workspace.insertChildInOrder(trackNode);
+      ctx.workspace.insertChildInOrder(trackNode);
     }
 
     ctx.registerDetailsPanel({
diff --git a/ui/src/core_plugins/critical_path/index.ts b/ui/src/core_plugins/critical_path/index.ts
index 5205a54..b6ffd4e 100644
--- a/ui/src/core_plugins/critical_path/index.ts
+++ b/ui/src/core_plugins/critical_path/index.ts
@@ -144,7 +144,7 @@
     // 2. Invoked via runCommand(...) by thread_state_tab.ts when the user
     //    clicks on the buttons in the details panel. In this case the details
     //    panel passes the utid explicitly.
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: CRITICAL_PATH_LITE_CMD,
       name: 'Critical path lite (selected thread state slice)',
       callback: async (utid?: Utid) => {
@@ -186,7 +186,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: CRITICAL_PATH_CMD,
       name: 'Critical path (selected thread state slice)',
       callback: async (utid?: Utid) => {
@@ -221,7 +221,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CriticalPathLite_AreaSelection',
       name: 'Critical path lite (over area selection)',
       callback: async () => {
@@ -263,7 +263,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CriticalPath_AreaSelection',
       name: 'Critical path  (over area selection)',
       callback: async () => {
@@ -297,7 +297,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.CriticalPathPprof_AreaSelection',
       name: 'Critical path pprof (over area selection)',
       callback: async () => {
diff --git a/ui/src/core_plugins/debug/index.ts b/ui/src/core_plugins/debug/index.ts
index c64980e..5fd247f 100644
--- a/ui/src/core_plugins/debug/index.ts
+++ b/ui/src/core_plugins/debug/index.ts
@@ -26,7 +26,7 @@
 
 class DebugTracksPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.DebugTracks#addDebugSliceTrack',
       name: 'Add debug slice track',
       callback: async (arg: unknown) => {
@@ -48,7 +48,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.DebugTracks#addDebugCounterTrack',
       name: 'Add debug counter track',
       callback: async (arg: unknown) => {
diff --git a/ui/src/core_plugins/example_traces/index.ts b/ui/src/core_plugins/example_traces/index.ts
index bfad0c8..e277187 100644
--- a/ui/src/core_plugins/example_traces/index.ts
+++ b/ui/src/core_plugins/example_traces/index.ts
@@ -32,14 +32,14 @@
   onActivate(ctx: App) {
     const OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID =
       'perfetto.CoreCommands#openExampleAndroidTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
       name: 'Open Android example',
       callback: () => {
         openTraceUrl(EXAMPLE_ANDROID_TRACE_URL);
       },
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addSidebarMenuItem({
       commandId: OPEN_EXAMPLE_ANDROID_TRACE_COMMAND_ID,
       group: 'example_traces',
       icon: 'description',
@@ -47,14 +47,14 @@
 
     const OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID =
       'perfetto.CoreCommands#openExampleChromeTrace';
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
       name: 'Open Chrome example',
       callback: () => {
         openTraceUrl(EXAMPLE_CHROME_TRACE_URL);
       },
     });
-    ctx.addSidebarMenuItem({
+    ctx.sidebar.addSidebarMenuItem({
       commandId: OPEN_EXAMPLE_CHROME_TRACE_COMMAND_ID,
       group: 'example_traces',
       icon: 'description',
diff --git a/ui/src/core_plugins/frames/index.ts b/ui/src/core_plugins/frames/index.ts
index 7c4d580..0c5f64f 100644
--- a/ui/src/core_plugins/frames/index.ts
+++ b/ui/src/core_plugins/frames/index.ts
@@ -73,7 +73,7 @@
       });
 
       const uri = `/process_${upid}/expected_frames`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         track: new ExpectedFramesTrack(engine, maxDepth, uri, trackIds),
@@ -83,7 +83,7 @@
           kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
         },
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = -50;
       group.insertChildInOrder(track);
@@ -137,7 +137,7 @@
       });
 
       const uri = `/process_${upid}/actual_frames`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         track: new ActualFramesTrack(engine, maxDepth, uri, trackIds),
@@ -147,7 +147,7 @@
           kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
         },
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = -50;
       group.insertChildInOrder(track);
diff --git a/ui/src/core_plugins/ftrace/index.ts b/ui/src/core_plugins/ftrace/index.ts
index ded23e5..c9e258f 100644
--- a/ui/src/core_plugins/ftrace/index.ts
+++ b/ui/src/core_plugins/ftrace/index.ts
@@ -65,7 +65,7 @@
       const uri = `/ftrace/cpu${cpuNum}`;
       const displayName = `Ftrace Track for CPU ${cpuNum}`;
 
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -89,7 +89,7 @@
 
     const ftraceTabUri = 'perfetto.FtraceRaw#FtraceEventsTab';
 
-    ctx.registerTab({
+    ctx.tabs.registerTab({
       uri: ftraceTabUri,
       isEphemeral: false,
       content: {
@@ -103,7 +103,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.FtraceRaw#ShowFtraceTab',
       name: 'Show ftrace tab',
       callback: () => {
diff --git a/ui/src/core_plugins/heap_profile/index.ts b/ui/src/core_plugins/heap_profile/index.ts
index 37ec4fa..b05dfba 100644
--- a/ui/src/core_plugins/heap_profile/index.ts
+++ b/ui/src/core_plugins/heap_profile/index.ts
@@ -57,7 +57,7 @@
       const upid = it.upid;
       const uri = `/process_${upid}/heap_profile`;
       const displayName = 'Heap Profile';
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -72,7 +72,7 @@
           upid,
         ),
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = -30;
       group.insertChildInOrder(track);
diff --git a/ui/src/core_plugins/perf_samples_profile/index.ts b/ui/src/core_plugins/perf_samples_profile/index.ts
index 4e965f9..4201a48 100644
--- a/ui/src/core_plugins/perf_samples_profile/index.ts
+++ b/ui/src/core_plugins/perf_samples_profile/index.ts
@@ -57,7 +57,7 @@
       const upid = it.upid;
       const uri = `/process_${upid}/perf_samples_profile`;
       const title = `Process Callstacks`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title,
         tags: {
@@ -72,7 +72,7 @@
           upid,
         ),
       });
-      const group = getOrCreateGroupForProcess(ctx.timeline.workspace, upid);
+      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
       const track = new TrackNode(uri, title);
       track.sortOrder = -40;
       group.insertChildInOrder(track);
@@ -103,7 +103,7 @@
           ? `Thread Callstacks ${tid}`
           : `${threadName} Callstacks ${tid}`;
       const uri = `${getThreadUriPrefix(upid, utid)}_perf_samples_profile`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -119,7 +119,7 @@
           utid,
         ),
       });
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = -50;
       group.insertChildInOrder(track);
diff --git a/ui/src/core_plugins/process/index.ts b/ui/src/core_plugins/process/index.ts
index ef94f37..462518e 100644
--- a/ui/src/core_plugins/process/index.ts
+++ b/ui/src/core_plugins/process/index.ts
@@ -21,7 +21,7 @@
 class ProcessPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace) {
     sqlTableRegistry['process'] = getProcessTable();
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.ShowTable.process',
       name: 'Open table: process',
       callback: () => {
diff --git a/ui/src/core_plugins/process_summary/index.ts b/ui/src/core_plugins/process_summary/index.ts
index 5194a13..3b12776 100644
--- a/ui/src/core_plugins/process_summary/index.ts
+++ b/ui/src/core_plugins/process_summary/index.ts
@@ -115,7 +115,7 @@
           utid,
         };
 
-        ctx.registerTrack({
+        ctx.tracks.registerTrack({
           uri,
           title: `${upid === null ? tid : pid} schedule`,
           tags: {
@@ -132,7 +132,7 @@
           utid,
         };
 
-        ctx.registerTrack({
+        ctx.tracks.registerTrack({
           uri,
           title: `${upid === null ? tid : pid} summary`,
           tags: {
@@ -191,7 +191,7 @@
       utid: it.utid,
     };
 
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri: '/kernel',
       title: `Kernel thread summary`,
       tags: {
diff --git a/ui/src/core_plugins/process_thread_groups/index.ts b/ui/src/core_plugins/process_thread_groups/index.ts
index 3ea523b..ec1a1ae 100644
--- a/ui/src/core_plugins/process_thread_groups/index.ts
+++ b/ui/src/core_plugins/process_thread_groups/index.ts
@@ -110,16 +110,13 @@
     const kernelThreadsGroup = new GroupNode('Kernel threads');
     kernelThreadsGroup.headerTrackUri = '/kernel'; // Summary track
     kernelThreadsGroup.sortOrder = 50;
-    ctx.timeline.workspace.insertChildInOrder(kernelThreadsGroup);
+    ctx.workspace.insertChildInOrder(kernelThreadsGroup);
 
     // Set the group for all kernel threads (including kthreadd itself).
     for (; it.valid(); it.next()) {
       const {utid} = it;
 
-      const threadGroup = getOrCreateGroupForThread(
-        ctx.timeline.workspace,
-        utid,
-      );
+      const threadGroup = getOrCreateGroupForThread(ctx.workspace, utid);
       threadGroup.headless = true;
       kernelThreadsGroup.insertChildInOrder(threadGroup);
 
@@ -233,13 +230,13 @@
         }
 
         const displayName = getProcessDisplayName(name ?? undefined, id);
-        const group = getOrCreateGroupForProcess(ctx.timeline.workspace, uid);
+        const group = getOrCreateGroupForProcess(ctx.workspace, uid);
         group.displayName = displayName;
         group.headerTrackUri = `/process_${uid}`; // Summary track URI
         group.sortOrder = 50;
 
         // Re-insert the child node to sort it
-        ctx.timeline.workspace.insertChildInOrder(group);
+        ctx.workspace.insertChildInOrder(group);
         cache.processGroups.set(uid, group);
       } else {
         // Ignore kernel process groups
@@ -259,13 +256,13 @@
         }
 
         const displayName = getThreadDisplayName(name ?? undefined, id);
-        const group = getOrCreateGroupForThread(ctx.timeline.workspace, uid);
+        const group = getOrCreateGroupForThread(ctx.workspace, uid);
         group.displayName = displayName;
         group.headerTrackUri = `/thread_${uid}`; // Summary track URI
         group.sortOrder = 50;
 
         // Re-insert the child node to sort it
-        ctx.timeline.workspace.insertChildInOrder(group);
+        ctx.workspace.insertChildInOrder(group);
         cache.threadGroups.set(uid, group);
       }
     }
@@ -326,7 +323,7 @@
         continue;
       }
 
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       group.displayName = threadName ?? `Thread ${tid}`;
       cache.threadGroups.set(utid, group);
       group.headless = true;
diff --git a/ui/src/core_plugins/sched/index.ts b/ui/src/core_plugins/sched/index.ts
index 7e73061..a229176 100644
--- a/ui/src/core_plugins/sched/index.ts
+++ b/ui/src/core_plugins/sched/index.ts
@@ -24,7 +24,7 @@
 class SchedPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace) {
     const runnableThreadCountUri = `/runnable_thread_count`;
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri: runnableThreadCountUri,
       title: 'Runnable thread count',
       track: new RunnableThreadCountTrack({
@@ -32,7 +32,7 @@
         uri: runnableThreadCountUri,
       }),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Sched.AddRunnableThreadCountTrackCommand',
       name: 'Add track: runnable thread count',
       callback: () =>
@@ -41,12 +41,12 @@
 
     const uri = uriForActiveCPUCountTrack();
     const title = 'Active CPU count';
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: title,
       track: new ActiveCPUCountTrack({trackUri: uri}, ctx.engine),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Sched.AddActiveCPUCountTrackCommand',
       name: 'Add track: active CPU count',
       callback: () => addPinnedTrack(ctx, uri, title),
@@ -55,13 +55,13 @@
     for (const cpuType of Object.values(CPUType)) {
       const uri = uriForActiveCPUCountTrack(cpuType);
       const title = `Active ${cpuType} CPU count`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: title,
         track: new ActiveCPUCountTrack({trackUri: uri}, ctx.engine, cpuType),
       });
 
-      ctx.registerCommand({
+      ctx.commands.registerCommand({
         id: `dev.perfetto.Sched.AddActiveCPUCountTrackCommand.${cpuType}`,
         name: `Add track: active ${cpuType} CPU count`,
         callback: () => addPinnedTrack(ctx, uri, title),
@@ -69,7 +69,7 @@
     }
 
     sqlTableRegistry['sched'] = getSchedTable();
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.ShowTable.sched',
       name: 'Open table: sched',
       callback: () => {
@@ -93,7 +93,7 @@
 function addPinnedTrack(ctx: Trace, uri: string, title: string) {
   const track = new TrackNode(uri, title);
   // Add track to the top of the stack
-  ctx.timeline.workspace.prependChild(track);
+  ctx.workspace.prependChild(track);
   track.pin();
 }
 
diff --git a/ui/src/core_plugins/screenshots/index.ts b/ui/src/core_plugins/screenshots/index.ts
index 074deeb..b6fadb4 100644
--- a/ui/src/core_plugins/screenshots/index.ts
+++ b/ui/src/core_plugins/screenshots/index.ts
@@ -35,7 +35,7 @@
     if (count > 0) {
       const displayName = 'Screenshots';
       const uri = '/screenshots';
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         track: new ScreenshotsTrack({
@@ -48,7 +48,7 @@
       });
       const trackNode = new TrackNode(uri, displayName);
       trackNode.sortOrder = -60;
-      ctx.timeline.workspace.insertChildInOrder(trackNode);
+      ctx.workspace.insertChildInOrder(trackNode);
 
       ctx.registerDetailsPanel(
         new BottomTabToSCSAdapter({
diff --git a/ui/src/core_plugins/slice/index.ts b/ui/src/core_plugins/slice/index.ts
index f995934..cf00fba 100644
--- a/ui/src/core_plugins/slice/index.ts
+++ b/ui/src/core_plugins/slice/index.ts
@@ -21,7 +21,7 @@
 class SlicePlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace) {
     sqlTableRegistry['slice'] = getSliceTable();
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.ShowTable.slice',
       name: 'Open table: slice',
       callback: () => {
diff --git a/ui/src/core_plugins/thread/index.ts b/ui/src/core_plugins/thread/index.ts
index e70203c..8abd7c4 100644
--- a/ui/src/core_plugins/thread/index.ts
+++ b/ui/src/core_plugins/thread/index.ts
@@ -21,7 +21,7 @@
 class ThreadPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace) {
     sqlTableRegistry['thread'] = getThreadTable();
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.ShowTable.thread',
       name: 'Open table: thread',
       callback: () => {
diff --git a/ui/src/core_plugins/thread_slice/index.ts b/ui/src/core_plugins/thread_slice/index.ts
index 221ab01..64bd5fd 100644
--- a/ui/src/core_plugins/thread_slice/index.ts
+++ b/ui/src/core_plugins/thread_slice/index.ts
@@ -87,7 +87,7 @@
       });
 
       const uri = `${getThreadUriPrefix(upid, utid)}_slice_${trackId}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -110,7 +110,7 @@
           maxDepth,
         ),
       });
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = 20;
       group.insertChildInOrder(track);
diff --git a/ui/src/core_plugins/thread_state/index.ts b/ui/src/core_plugins/thread_state/index.ts
index 5ce73fc..d5e1e25 100644
--- a/ui/src/core_plugins/thread_state/index.ts
+++ b/ui/src/core_plugins/thread_state/index.ts
@@ -66,7 +66,7 @@
       });
 
       const uri = `${getThreadUriPrefix(upid, utid)}_state`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         tags: {
@@ -87,7 +87,7 @@
         ),
       });
 
-      const group = getOrCreateGroupForThread(ctx.timeline.workspace, utid);
+      const group = getOrCreateGroupForThread(ctx.workspace, utid);
       const track = new TrackNode(uri, displayName);
       track.sortOrder = 10;
       group.insertChildInOrder(track);
@@ -111,7 +111,7 @@
     );
 
     sqlTableRegistry['thread_state'] = getThreadStateTable();
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.ShowTable.thread_state',
       name: 'Open table: thread_state',
       callback: () => {
diff --git a/ui/src/core_plugins/track_utils/index.ts b/ui/src/core_plugins/track_utils/index.ts
index 046cd28..9cb1e17 100644
--- a/ui/src/core_plugins/track_utils/index.ts
+++ b/ui/src/core_plugins/track_utils/index.ts
@@ -24,7 +24,7 @@
 
 class TrackUtilsPlugin implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'perfetto.RunQueryInSelectedTimeWindow',
       name: `Run query in selected time window`,
       callback: async () => {
@@ -37,7 +37,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       // Selects & reveals the first track on the timeline with a given URI.
       id: 'perfetto.FindTrack',
       name: 'Find track by URI',
diff --git a/ui/src/core_plugins/wattson/index.ts b/ui/src/core_plugins/wattson/index.ts
index 66773f9..8bc8fcd 100644
--- a/ui/src/core_plugins/wattson/index.ts
+++ b/ui/src/core_plugins/wattson/index.ts
@@ -32,7 +32,7 @@
     ctx.engine.query(`INCLUDE PERFETTO MODULE wattson.curves.ungrouped;`);
 
     const group = new GroupNode('Wattson');
-    ctx.timeline.workspace.insertChildInOrder(group);
+    ctx.workspace.insertChildInOrder(group);
 
     // CPUs estimate as part of CPU subsystem
     const cpus = globals.traceContext.cpus;
@@ -40,7 +40,7 @@
       const queryKey = `cpu${cpu}_curve`;
       const uri = `/wattson/cpu_subsystem_estimate_cpu${cpu}`;
       const displayName = `Cpu${cpu} Estimate`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         track: new CpuSubsystemEstimateTrack(ctx.engine, uri, queryKey),
@@ -55,7 +55,7 @@
 
     const uri = `/wattson/cpu_subsystem_estimate_dsu_scu`;
     const displayName = `DSU/SCU Estimate`;
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: displayName,
       track: new CpuSubsystemEstimateTrack(ctx.engine, uri, `dsu_scu`),
diff --git a/ui/src/frontend/app_context.ts b/ui/src/frontend/app_context.ts
index 54b8763..36902e2 100644
--- a/ui/src/frontend/app_context.ts
+++ b/ui/src/frontend/app_context.ts
@@ -16,12 +16,12 @@
 import {CurrentSearchResults} from '../common/search_data';
 import {State} from '../common/state';
 import {TimelineImpl} from '../core/timeline';
-import {TraceContext} from './trace_context';
+import {TraceInfo} from '../public/trace_info';
 
 export interface AppContext {
   readonly store: Store<State>;
   readonly state: State;
-  readonly traceContext: TraceContext;
+  readonly traceContext: TraceInfo;
 
   // TODO(stevegolton): This could probably be moved into TraceContext.
   readonly timeline: TimelineImpl;
diff --git a/ui/src/frontend/debug_tracks/add_debug_track_menu.ts b/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
index dce3f51..445ecab 100644
--- a/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
+++ b/ui/src/frontend/debug_tracks/add_debug_track_menu.ts
@@ -192,7 +192,10 @@
                 addPivotedTracks(
                   {
                     engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
+                    tracks: {
+                      registerTrack: (x) =>
+                        globals.trackManager.registerTrack(x),
+                    },
                   },
                   vnode.attrs.dataSource,
                   this.name,
@@ -213,7 +216,10 @@
                   // point we can just use the plugin's context object.
                   {
                     engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
+                    tracks: {
+                      registerTrack: (x) =>
+                        globals.trackManager.registerTrack(x),
+                    },
                   },
                   vnode.attrs.dataSource,
                   this.name,
@@ -232,7 +238,10 @@
                 addPivotedTracks(
                   {
                     engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
+                    tracks: {
+                      registerTrack: (x) =>
+                        globals.trackManager.registerTrack(x),
+                    },
                   },
                   vnode.attrs.dataSource,
                   this.name,
@@ -247,7 +256,10 @@
                   // point we can just use the plugin's context object.
                   {
                     engine: vnode.attrs.engine,
-                    registerTrack: (x) => globals.trackManager.registerTrack(x),
+                    tracks: {
+                      registerTrack: (x) =>
+                        globals.trackManager.registerTrack(x),
+                    },
                   },
                   vnode.attrs.dataSource,
                   this.name,
diff --git a/ui/src/frontend/debug_tracks/debug_tracks.ts b/ui/src/frontend/debug_tracks/debug_tracks.ts
index 10b4ba3..88ae56e 100644
--- a/ui/src/frontend/debug_tracks/debug_tracks.ts
+++ b/ui/src/frontend/debug_tracks/debug_tracks.ts
@@ -39,7 +39,9 @@
 // this to work.
 interface Context {
   engine: Engine;
-  registerTrack(track: TrackDescriptor): unknown;
+  tracks: {
+    registerTrack(track: TrackDescriptor): unknown;
+  };
 }
 
 // Names of the columns of the underlying view to be used as
@@ -136,7 +138,7 @@
   );
 
   const uri = `debug.slice.${uuidv4()}`;
-  ctx.registerTrack({
+  ctx.tracks.registerTrack({
     uri,
     title: trackName,
     track: new DebugSliceTrack(ctx.engine, {trackUri: uri}, tableName),
@@ -218,7 +220,7 @@
   );
 
   const uri = `debug.counter.${uuidv4()}`;
-  ctx.registerTrack({
+  ctx.tracks.registerTrack({
     uri,
     title: trackName,
     track: new DebugCounterTrack(ctx.engine, {trackUri: uri}, tableName),
diff --git a/ui/src/frontend/globals.ts b/ui/src/frontend/globals.ts
index ceb5a22..8f89a18 100644
--- a/ui/src/frontend/globals.ts
+++ b/ui/src/frontend/globals.ts
@@ -48,7 +48,7 @@
   SearchOverviewTrack,
 } from './search_overview_track';
 import {AppContext} from './app_context';
-import {TraceContext} from './trace_context';
+import {TraceInfo} from '../public/trace_info';
 import {Registry} from '../base/registry';
 import {SidebarMenuItem} from '../public/sidebar';
 import {Workspace} from '../public/workspace';
@@ -150,7 +150,7 @@
 }
 type ThreadMap = Map<number, ThreadDesc>;
 
-export const defaultTraceContext: TraceContext = {
+export const defaultTraceContext: TraceInfo = {
   traceTitle: '',
   traceUrl: '',
   start: Time.ZERO,
@@ -248,7 +248,7 @@
   // TODO(stevegolton): Eventually initialization that should be done on trace
   // load should be moved into here, and then we can remove TraceController
   // entirely
-  async onTraceLoad(engine: Engine, traceCtx: TraceContext): Promise<void> {
+  async onTraceLoad(engine: Engine, traceCtx: TraceInfo): Promise<void> {
     this.traceContext = traceCtx;
 
     const {start, end} = traceCtx;
diff --git a/ui/src/frontend/notes_panel.ts b/ui/src/frontend/notes_panel.ts
index 6443718..98f6972 100644
--- a/ui/src/frontend/notes_panel.ts
+++ b/ui/src/frontend/notes_panel.ts
@@ -18,7 +18,7 @@
 import {Time} from '../base/time';
 import {Actions} from '../common/actions';
 import {randomColor} from '../core/colorizer';
-import {SpanNote, Note} from 'src/public/note';
+import {SpanNote, Note} from '../public/note';
 import {raf} from '../core/raf_scheduler';
 import {Button, ButtonBar} from '../widgets/button';
 import {TextInput} from '../widgets/text_input';
diff --git a/ui/src/frontend/slice_args.ts b/ui/src/frontend/slice_args.ts
index 2a29bd8..8a48cd2 100644
--- a/ui/src/frontend/slice_args.ts
+++ b/ui/src/frontend/slice_args.ts
@@ -114,7 +114,9 @@
           addVisualisedArgTracks(
             {
               engine,
-              registerTrack: (t) => globals.trackManager.registerTrack(t),
+              tracks: {
+                registerTrack: (t) => globals.trackManager.registerTrack(t),
+              },
             },
             fullKey,
           );
diff --git a/ui/src/frontend/thread_slice_details_tab.ts b/ui/src/frontend/thread_slice_details_tab.ts
index 69c8900..f013cca 100644
--- a/ui/src/frontend/thread_slice_details_tab.ts
+++ b/ui/src/frontend/thread_slice_details_tab.ts
@@ -154,7 +154,9 @@
             // plugin's context object.
             {
               engine,
-              registerTrack: (x) => globals.trackManager.registerTrack(x),
+              tracks: {
+                registerTrack: (x) => globals.trackManager.registerTrack(x),
+              },
             },
             {
               sqlSource: `
diff --git a/ui/src/frontend/visualized_args_tracks.ts b/ui/src/frontend/visualized_args_tracks.ts
index 776105f..ee32a59 100644
--- a/ui/src/frontend/visualized_args_tracks.ts
+++ b/ui/src/frontend/visualized_args_tracks.ts
@@ -34,7 +34,7 @@
 // work.
 interface Context {
   engine: Engine;
-  registerTrack(track: TrackDescriptor): unknown;
+  tracks: {registerTrack(track: TrackDescriptor): unknown};
 }
 
 export async function addVisualisedArgTracks(ctx: Context, argName: string) {
@@ -80,7 +80,7 @@
     const maxDepth = it.maxDepth;
 
     const uri = `${VISUALISED_ARGS_SLICE_TRACK_URI_PREFIX}#${uuidv4()}`;
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: argName,
       chips: ['metric'],
diff --git a/ui/src/plugins/com.google.PixelMemory/index.ts b/ui/src/plugins/com.google.PixelMemory/index.ts
index 95f391a..bda2c76 100644
--- a/ui/src/plugins/com.google.PixelMemory/index.ts
+++ b/ui/src/plugins/com.google.PixelMemory/index.ts
@@ -18,7 +18,7 @@
 
 class PixelMemory implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PixelMemory#ShowTotalMemory',
       name: 'Add tracks: show a process total memory',
       callback: async (pid) => {
diff --git a/ui/src/plugins/com.google.android.GoogleCamera/index.ts b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
index 51ee688..7662669 100644
--- a/ui/src/plugins/com.google.android.GoogleCamera/index.ts
+++ b/ui/src/plugins/com.google.android.GoogleCamera/index.ts
@@ -22,7 +22,7 @@
   async onTraceLoad(ctx: Trace): Promise<void> {
     this.ctx = ctx;
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'com.google.android.GoogleCamera#LoadGoogleCameraStartupView',
       name: 'Load google camera startup view',
       callback: () => {
@@ -30,7 +30,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'com.google.android.GoogleCamera#PinCameraRelatedTracks',
       name: 'Pin camera related tracks',
       callback: (trackNames) => {
@@ -54,7 +54,7 @@
   }
 
   private pinTracks(trackNames: ReadonlyArray<string>) {
-    this.ctx.timeline.workspace.flatTracks.forEach((track) => {
+    this.ctx.workspace.flatTracks.forEach((track) => {
       if (trackNames.includes(track.displayName)) {
         track.pin();
       }
diff --git a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
index ab2c1ea..13000c2 100644
--- a/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidClientServer/index.ts
@@ -19,7 +19,7 @@
 
 class AndroidClientServer implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidClientServer#ThreadRuntimeIPC',
       name: 'Show dependencies in client server model',
       callback: async (sliceId) => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
index 96143a6..20b2699 100644
--- a/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidCujs/index.ts
@@ -216,7 +216,7 @@
 
 class AndroidCujs implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinJankCUJs',
       name: 'Add track: Android jank CUJs',
       callback: () => {
@@ -226,7 +226,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#ListJankCUJs',
       name: 'Run query: Android jank CUJs',
       callback: () => {
@@ -236,7 +236,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinLatencyCUJs',
       name: 'Add track: Android latency CUJs',
       callback: () => {
@@ -253,14 +253,14 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#ListLatencyCUJs',
       name: 'Run query: Android Latency CUJs',
       callback: () =>
         ctx.tabs.openQuery(LATENCY_CUJ_QUERY, 'Android Latency CUJs'),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidCujs#PinBlockingCalls',
       name: 'Add track: Android Blocking calls during CUJs',
       callback: () => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
index 3f84d87..1ac6c72 100644
--- a/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidLongBatteryTracing/index.ts
@@ -1161,10 +1161,10 @@
         const group = new GroupNode(groupName);
         group.insertChildInOrder(track);
         this.groups.set(groupName, group);
-        ctx.timeline.workspace.insertChildInOrder(group);
+        ctx.workspace.insertChildInOrder(group);
       }
     } else {
-      ctx.timeline.workspace.insertChildInOrder(track);
+      ctx.workspace.insertChildInOrder(track);
     }
   }
 
@@ -1185,7 +1185,7 @@
     };
 
     const uri = `/long_battery_tracing_${name}`;
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: name,
       track: new SimpleSliceTrack(ctx.engine, {trackUri: uri}, config),
@@ -1211,7 +1211,7 @@
     };
 
     const uri = `/long_battery_tracing_${name}`;
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: name,
       track: new SimpleCounterTrack(ctx.engine, {trackUri: uri}, config),
diff --git a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
index 7cef03c..5159f97 100644
--- a/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidNetwork/index.ts
@@ -39,7 +39,7 @@
   }
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#batteryEvents',
       name: 'Add track: battery events',
       callback: async (track) => {
@@ -60,7 +60,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidNetwork#activityTrack',
       name: 'Add track: network activity',
       callback: async (groupby, filter, trackName) => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
index 84475c4..3173ad0 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerf/index.ts
@@ -55,7 +55,7 @@
   }
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderSystemServerIncoming',
       name: 'Run query: system_server incoming binder graph',
       callback: () =>
@@ -66,7 +66,7 @@
         ),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderSystemServerOutgoing',
       name: 'Run query: system_server outgoing binder graph',
       callback: () =>
@@ -77,7 +77,7 @@
         ),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#MonitorContentionSystemServer',
       name: 'Run query: system_server monitor_contention graph',
       callback: () =>
@@ -88,7 +88,7 @@
         ),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#BinderAll',
       name: 'Run query: all process binder graph',
       callback: () =>
@@ -99,7 +99,7 @@
         ),
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#ThreadClusterDistribution',
       name: 'Run query: runtime cluster distribution for a thread',
       callback: async (tid) => {
@@ -134,7 +134,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#SchedLatency',
       name: 'Run query: top 50 sched latency for a thread',
       callback: async (tid) => {
@@ -158,7 +158,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#AppProcessStarts',
       name: 'Add tracks: app process starts',
       callback: async () => {
@@ -173,7 +173,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerf#AppIntentStarts',
       name: 'Add tracks: app intent starts',
       callback: async () => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
index 1a7898d..b9f4be7 100644
--- a/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidPerfTraceCounters/index.ts
@@ -29,7 +29,7 @@
   async onTraceLoad(ctx: Trace): Promise<void> {
     const resp = await ctx.engine.query(PERF_TRACE_COUNTERS_PRECONDITION);
     if (resp.numRows() === 0) return;
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.AndroidPerfTraceCounters#ThreadRuntimeIPC',
       name: 'Add a track to show a thread runtime ipc',
       callback: async (tid) => {
diff --git a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
index 4623621..44e1379 100644
--- a/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidStartup/index.ts
@@ -43,12 +43,12 @@
     };
     const uri = `/android_startups`;
     const title = 'Android App Startups';
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title: 'Android App Startups',
       track: new SimpleSliceTrack(ctx.engine, {trackUri: uri}, config),
     });
-    ctx.timeline.workspace.insertChildInOrder(new TrackNode(uri, title));
+    ctx.workspace.insertChildInOrder(new TrackNode(uri, title));
   }
 }
 
diff --git a/ui/src/plugins/dev.perfetto.Chaos/index.ts b/ui/src/plugins/dev.perfetto.Chaos/index.ts
index 07e2441..8800907 100644
--- a/ui/src/plugins/dev.perfetto.Chaos/index.ts
+++ b/ui/src/plugins/dev.perfetto.Chaos/index.ts
@@ -19,7 +19,7 @@
 
 class Chaos implements PerfettoPlugin {
   onActivate(ctx: App): void {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#CrashNow',
       name: 'Chaos: crash now',
       callback: () => {
@@ -29,7 +29,7 @@
   }
 
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#CrashNowQuery',
       name: 'Chaos: run crashing query',
       callback: () => {
@@ -44,7 +44,7 @@
       },
     });
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.Chaos#AddCrashingDebugTrack',
       name: 'Chaos: add crashing debug track',
       callback: () => {
diff --git a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
index 9f0628e..ad3a399 100644
--- a/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleSimpleCommand/index.ts
@@ -18,7 +18,7 @@
 // This is just an example plugin, used to prove that the plugin system works.
 class ExampleSimpleCommand implements PerfettoPlugin {
   onActivate(ctx: App): void {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.ExampleSimpleCommand#LogHelloWorld',
       name: 'Log "Hello, world!"',
       callback: () => console.log('Hello, world!'),
diff --git a/ui/src/plugins/dev.perfetto.ExampleState/index.ts b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
index 337e020..03b5d2e 100644
--- a/ui/src/plugins/dev.perfetto.ExampleState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ExampleState/index.ts
@@ -42,7 +42,7 @@
   async onTraceLoad(ctx: Trace): Promise<void> {
     this.store = ctx.mountStore((init: unknown) => this.migrate(init));
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.ExampleState#ShowCounter',
       name: 'Show ExampleState counter',
       callback: () => {
diff --git a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
index a81b40f..cec43a0 100644
--- a/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
+++ b/ui/src/plugins/dev.perfetto.GpuByProcess/index.ts
@@ -80,12 +80,12 @@
 
       const uri = `dev.perfetto.GpuByProcess#${upid}`;
       const title = `GPU ${processName}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title,
         track: new GpuPidTrack({engine: ctx.engine, uri}, upid),
       });
-      ctx.timeline.workspace.insertChildInOrder(new TrackNode(uri, title));
+      ctx.workspace.insertChildInOrder(new TrackNode(uri, title));
     }
   }
 
diff --git a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
index 277ce14..71fcbe1 100644
--- a/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
+++ b/ui/src/plugins/dev.perfetto.LargeScreensPerf/index.ts
@@ -17,11 +17,11 @@
 
 class LargeScreensPerf implements PerfettoPlugin {
   async onTraceLoad(ctx: Trace): Promise<void> {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.LargeScreensPerf#PinUnfoldLatencyTracks',
       name: 'Pin: Unfold latency tracks',
       callback: () => {
-        ctx.timeline.workspace.flatTracks.forEach((track) => {
+        ctx.workspace.flatTracks.forEach((track) => {
           if (
             !!track.displayName.includes('UnfoldTransition') ||
             track.displayName.includes('Screen on blocked') ||
diff --git a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
index 549bdba..53c6964 100644
--- a/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
+++ b/ui/src/plugins/dev.perfetto.PinAndroidPerfMetrics/index.ts
@@ -40,7 +40,7 @@
   }
 
   async onTraceReady(ctx: Trace) {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PinAndroidPerfMetrics#PinAndroidPerfMetrics',
       name: 'Add and Pin: Jank Metric Slice',
       callback: async (metric) => {
diff --git a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
index dba04f3..1b00182 100644
--- a/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.PinSysUITracks/index.ts
@@ -47,11 +47,11 @@
       upid: NUM,
     }).upid;
 
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: 'dev.perfetto.PinSysUITracks#PinSysUITracks',
       name: 'Pin: System UI Related Tracks',
       callback: () => {
-        ctx.timeline.workspace.flatTracks.forEach((track) => {
+        ctx.workspace.flatTracks.forEach((track) => {
           // Ensure we only grab tracks that are in the SysUI process group
           if (!track.uri.startsWith(`/process_${sysuiUpid}`)) return;
           if (
@@ -65,7 +65,7 @@
         });
 
         // expand the sysui process tracks group
-        ctx.timeline.workspace.flatGroups.forEach((group) => {
+        ctx.workspace.flatGroups.forEach((group) => {
           if (group.displayName.startsWith(SYSTEM_UI_PROCESS)) {
             group.expand();
           }
diff --git a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
index f4dea90..4f87e82 100644
--- a/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
+++ b/ui/src/plugins/dev.perfetto.RestorePinnedTracks/index.ts
@@ -35,14 +35,14 @@
 
   async onTraceLoad(ctx: Trace): Promise<void> {
     this.ctx = ctx;
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `${PLUGIN_ID}#save`,
       name: 'Save: Pinned tracks',
       callback: () => {
         this.saveTracks();
       },
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `${PLUGIN_ID}#restore`,
       name: 'Restore: Pinned tracks',
       callback: () => {
@@ -52,7 +52,7 @@
   }
 
   private saveTracks() {
-    const workspace = this.ctx.timeline.workspace;
+    const workspace = this.ctx.workspace;
     const pinnedTracks = workspace.pinnedTracks;
     const tracksToSave: SavedPinnedTrack[] = pinnedTracks.map((track) => ({
       groupName: groupName(track),
@@ -68,7 +68,7 @@
       return;
     }
     const tracksToRestore: SavedPinnedTrack[] = JSON.parse(savedTracks);
-    const workspace = this.ctx.timeline.workspace;
+    const workspace = this.ctx.workspace;
     const tracks = workspace.flatTracks;
     tracksToRestore.forEach((trackToRestore) => {
       // Check for an exact match
diff --git a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
index ab2cef1..e0904ac 100644
--- a/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
+++ b/ui/src/plugins/dev.perfetto.TimelineSync/index.ts
@@ -65,17 +65,17 @@
   >();
 
   onActivate(ctx: App): void {
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#enableTimelineSync`,
       name: 'Enable timeline sync with other Perfetto UI tabs',
       callback: () => this.showTimelineSyncDialog(),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#disableTimelineSync`,
       name: 'Disable timeline sync',
       callback: () => this.disableTimelineSync(this._sessionId),
     });
-    ctx.registerCommand({
+    ctx.commands.registerCommand({
       id: `dev.perfetto.SplitScreen#toggleTimelineSync`,
       name: 'Toggle timeline sync with other PerfettoUI tabs',
       callback: () => this.toggleTimelineSync(),
diff --git a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
index 68c7186..650ebbb 100644
--- a/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceMetadata/index.ts
@@ -28,7 +28,7 @@
     }
     const uri = `/clock_snapshots`;
     const title = 'Clock Snapshots';
-    ctx.registerTrack({
+    ctx.tracks.registerTrack({
       uri,
       title,
       track: new SimpleSliceTrack(
@@ -47,7 +47,7 @@
         },
       ),
     });
-    ctx.timeline.workspace.insertChildInOrder(new TrackNode(uri, title));
+    ctx.workspace.insertChildInOrder(new TrackNode(uri, title));
   }
 }
 
diff --git a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
index b3b9314..0a82296 100644
--- a/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
+++ b/ui/src/plugins/org.kernel.LinuxKernelDevices/index.ts
@@ -42,7 +42,7 @@
       const displayName = it.name ?? `${trackId}`;
 
       const uri = `/kernel_devices/${displayName}`;
-      ctx.registerTrack({
+      ctx.tracks.registerTrack({
         uri,
         title: displayName,
         track: new AsyncSliceTrack(
@@ -62,7 +62,7 @@
       const group = new GroupNode('Linux Kernel Devices');
       const track = new TrackNode(uri, displayName);
       group.insertChildInOrder(track);
-      ctx.timeline.workspace.insertChildInOrder(group);
+      ctx.workspace.insertChildInOrder(group);
     }
   }
 }
diff --git a/ui/src/public/app.ts b/ui/src/public/app.ts
index be000fd..6381bc5 100644
--- a/ui/src/public/app.ts
+++ b/ui/src/public/app.ts
@@ -26,15 +26,19 @@
    */
   readonly pluginId: string;
 
-  registerCommand(command: Command): void;
+  commands: {
+    registerCommand(command: Command): void;
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  runCommand(id: string, ...args: any[]): any;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    runCommand(id: string, ...args: any[]): any;
+  };
 
-  /**
-   * Adds a new menu item to the sidebar.
-   * All entries must map to a command. This will allow the shortcut and
-   * optional shortcut to be displayed on the UI.
-   */
-  addSidebarMenuItem(menuItem: SidebarMenuItem): void;
+  sidebar: {
+    /**
+     * Adds a new menu item to the sidebar.
+     * All entries must map to a command. This will allow the shortcut and
+     * optional shortcut to be displayed on the UI.
+     */
+    addSidebarMenuItem(menuItem: SidebarMenuItem): void;
+  };
 }
diff --git a/ui/src/public/trace.ts b/ui/src/public/trace.ts
index 66f22fd..480fb7d 100644
--- a/ui/src/public/trace.ts
+++ b/ui/src/public/trace.ts
@@ -14,7 +14,7 @@
 
 import {Migrate, Store} from '../base/store';
 import {time, TimeSpan} from '../base/time';
-import {TraceContext} from '../frontend/trace_context';
+import {TraceInfo as TraceInfo} from './trace_info';
 import {Engine} from '../trace_processor/engine';
 import {App} from './app';
 import {PromptOption} from './omnibox';
@@ -34,6 +34,10 @@
 export interface Trace extends App {
   readonly engine: Engine;
 
+  // Access the default workspace - used for adding, removing and reorganizing
+  // tracks
+  readonly workspace: Workspace;
+
   // Control over the main timeline.
   timeline: {
     // Bring a timestamp into view.
@@ -44,14 +48,17 @@
 
     // A span representing the current viewport location
     readonly viewport: TimeSpan;
-
-    // Access the default workspace - used for adding, removing and reorganizing
-    // tracks
-    readonly workspace: Workspace;
   };
 
   // Control over the bottom details pane.
   tabs: {
+    // Register a new tab for this plugin. Will be unregistered when the plugin
+    // is deactivated or when the trace is unloaded.
+    registerTab(tab: TabDescriptor): void;
+
+    // Suggest that a tab should be shown immediately.
+    addDefaultTab(uri: string): void;
+
     // Creates a new tab running the provided query.
     openQuery(query: string, title: string): void;
 
@@ -62,17 +69,12 @@
     hideTab(uri: string): void;
   };
 
-  // Register a new track against a unique key known as a URI. The track is not
-  // shown by default and callers need to either manually add it to a
-  // Workspace or use registerTrackAndShowOnTraceLoad() below.
-  registerTrack(trackDesc: TrackDescriptor): void;
-
-  // Register a new tab for this plugin. Will be unregistered when the plugin
-  // is deactivated or when the trace is unloaded.
-  registerTab(tab: TabDescriptor): void;
-
-  // Suggest that a tab should be shown immediately.
-  addDefaultTab(uri: string): void;
+  tracks: {
+    // Register a new track against a unique key known as a URI. The track is not
+    // shown by default and callers need to either manually add it to a
+    // Workspace or use registerTrackAndShowOnTraceLoad() below.
+    registerTrack(trackDesc: TrackDescriptor): void;
+  };
 
   // Register a hook into the current selection tab rendering logic that allows
   // customization of the current selection tab content.
@@ -81,7 +83,7 @@
   // Create a store mounted over the top of this plugin's persistent state.
   mountStore<T>(migrate: Migrate<T>): Store<T>;
 
-  readonly trace: TraceContext;
+  readonly trace: TraceInfo;
 
   // When the trace is opened via postMessage deep-linking, returns the sub-set
   // of postMessageData.pluginArgs[pluginId] for the current plugin. If not
diff --git a/ui/src/frontend/trace_context.ts b/ui/src/public/trace_info.ts
similarity index 97%
rename from ui/src/frontend/trace_context.ts
rename to ui/src/public/trace_info.ts
index fbd84d6..0a75fef 100644
--- a/ui/src/frontend/trace_context.ts
+++ b/ui/src/public/trace_info.ts
@@ -14,7 +14,7 @@
 
 import {time} from '../base/time';
 
-export interface TraceContext {
+export interface TraceInfo {
   traceTitle: string; // File name and size of the current trace.
   traceUrl: string; // URL of the Trace.
   readonly start: time;