Merge changes Ic1c3e077,I69b8ec0d into main

* changes:
  ui: Use plugin deps to add tracks to process groups
  ui: Don't render tracks while the trace is loading
diff --git a/ui/src/frontend/viewer_page.ts b/ui/src/frontend/viewer_page.ts
index 9509c1d..0e5668f 100644
--- a/ui/src/frontend/viewer_page.ts
+++ b/ui/src/frontend/viewer_page.ts
@@ -323,28 +323,30 @@
         m(PanelContainer, {
           trace: attrs.trace,
           className: 'pinned-panel-container',
-          panels: attrs.trace.workspace.pinnedTracks.map((trackNode) => {
-            if (trackNode.uri) {
-              const tr = attrs.trace.tracks.getTrackRenderer(trackNode.uri);
-              return new TrackPanel({
-                trace: attrs.trace,
-                reorderable: true,
-                node: trackNode,
-                trackRenderer: tr,
-                revealOnCreate: true,
-                indentationLevel: 0,
-                topOffsetPx: 0,
-              });
-            } else {
-              return new TrackPanel({
-                trace: attrs.trace,
-                node: trackNode,
-                revealOnCreate: true,
-                indentationLevel: 0,
-                topOffsetPx: 0,
-              });
-            }
-          }),
+          panels: AppImpl.instance.isLoadingTrace
+            ? []
+            : attrs.trace.workspace.pinnedTracks.map((trackNode) => {
+                if (trackNode.uri) {
+                  const tr = attrs.trace.tracks.getTrackRenderer(trackNode.uri);
+                  return new TrackPanel({
+                    trace: attrs.trace,
+                    reorderable: true,
+                    node: trackNode,
+                    trackRenderer: tr,
+                    revealOnCreate: true,
+                    indentationLevel: 0,
+                    topOffsetPx: 0,
+                  });
+                } else {
+                  return new TrackPanel({
+                    trace: attrs.trace,
+                    node: trackNode,
+                    revealOnCreate: true,
+                    indentationLevel: 0,
+                    topOffsetPx: 0,
+                  });
+                }
+              }),
           renderUnderlay: (ctx, size) => renderUnderlay(attrs.trace, ctx, size),
           renderOverlay: (ctx, size, panels) =>
             renderOverlay(attrs.trace, ctx, size, panels),
@@ -353,7 +355,7 @@
         m(PanelContainer, {
           trace: attrs.trace,
           className: 'scrolling-panel-container',
-          panels: scrollingPanels,
+          panels: AppImpl.instance.isLoadingTrace ? [] : scrollingPanels,
           onPanelStackResize: (width) => {
             const timelineWidth = width - TRACK_SHELL_WIDTH;
             this.timelineWidthPx = timelineWidth;
diff --git a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
index a946cc2..b9d0919 100644
--- a/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
+++ b/ui/src/plugins/dev.perfetto.AndroidDmabuf/index.ts
@@ -17,13 +17,10 @@
   SqlDataSource,
 } from '../../public/lib/tracks/query_counter_track';
 import {PerfettoPlugin} from '../../public/plugin';
-import {
-  getOrCreateGroupForProcess,
-  getOrCreateGroupForThread,
-} from '../../public/standard_groups';
 import {Trace} from '../../public/trace';
 import {TrackNode} from '../../public/workspace';
 import {NUM_NULL} from '../../trace_processor/query_result';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 async function registerAllocsTrack(
   ctx: Trace,
@@ -44,6 +41,8 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.AndroidDmabuf';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const e = ctx.engine;
     await e.query(`INCLUDE PERFETTO MODULE android.memory.dmabuf`);
@@ -66,9 +65,10 @@
                  WHERE upid = ${it.upid}`,
         };
         await registerAllocsTrack(ctx, uri, config);
-        getOrCreateGroupForProcess(ctx.workspace, it.upid).addChildInOrder(
-          new TrackNode({uri, title: 'dmabuf allocs'}),
-        );
+        ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForProcess(it.upid)
+          ?.addChildInOrder(new TrackNode({uri, title: 'dmabuf allocs'}));
       } else if (it.utid != null) {
         const uri = `/android_process_dmabuf_utid_${it.utid}`;
         const config: SqlDataSource = {
@@ -76,9 +76,10 @@
                  WHERE utid = ${it.utid}`,
         };
         await registerAllocsTrack(ctx, uri, config);
-        getOrCreateGroupForThread(ctx.workspace, it.utid).addChildInOrder(
-          new TrackNode({uri, title: 'dmabuf allocs'}),
-        );
+        ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForThread(it.utid)
+          ?.addChildInOrder(new TrackNode({uri, title: 'dmabuf allocs'}));
       }
     }
   }
diff --git a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
index ecd8dab..e5940cd 100644
--- a/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
+++ b/ui/src/plugins/dev.perfetto.AsyncSlices/index.ts
@@ -20,19 +20,18 @@
 import {getThreadUriPrefix, getTrackName} from '../../public/utils';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 import {AsyncSliceTrack} from './async_slice_track';
-import {
-  getOrCreateGroupForProcess,
-  getOrCreateGroupForThread,
-} from '../../public/standard_groups';
 import {exists} from '../../base/utils';
 import {assertExists, assertTrue} from '../../base/logging';
 import {SliceSelectionAggregator} from './slice_selection_aggregator';
 import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
 import {getSliceTable} from './table';
 import {extensions} from '../../public/lib/extensions';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.AsyncSlices';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const trackIdsToUris = new Map<number, string>();
 
@@ -298,8 +297,10 @@
       if (parent !== false && parent !== undefined) {
         parent.trackNode.addChildInOrder(t.trackNode);
       } else {
-        const processGroup = getOrCreateGroupForProcess(ctx.workspace, t.upid);
-        processGroup.addChildInOrder(t.trackNode);
+        const processGroup = ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForProcess(t.upid);
+        processGroup?.addChildInOrder(t.trackNode);
       }
     });
   }
@@ -399,8 +400,10 @@
       if (parent !== false && parent !== undefined) {
         parent.trackNode.addChildInOrder(t.trackNode);
       } else {
-        const group = getOrCreateGroupForThread(ctx.workspace, t.utid);
-        group.addChildInOrder(t.trackNode);
+        const group = ctx.plugins
+          .getPlugin(ProcessThreadGroupsPlugin)
+          .getGroupForThread(t.utid);
+        group?.addChildInOrder(t.trackNode);
       }
     });
   }
diff --git a/ui/src/plugins/dev.perfetto.Counter/index.ts b/ui/src/plugins/dev.perfetto.Counter/index.ts
index ef063ca..47a8f61 100644
--- a/ui/src/plugins/dev.perfetto.Counter/index.ts
+++ b/ui/src/plugins/dev.perfetto.Counter/index.ts
@@ -27,11 +27,8 @@
 import {TraceProcessorCounterTrack} from './trace_processor_counter_track';
 import {exists} from '../../base/utils';
 import {TrackNode} from '../../public/workspace';
-import {
-  getOrCreateGroupForProcess,
-  getOrCreateGroupForThread,
-} from '../../public/standard_groups';
 import {CounterSelectionAggregator} from './counter_selection_aggregator';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 const NETWORK_TRACK_REGEX = new RegExp('^.* (Received|Transmitted)( KB)?$');
 const ENTITY_RESIDENCY_REGEX = new RegExp('^Entity residency:');
@@ -108,6 +105,8 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.Counter';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     await this.addCounterTracks(ctx);
     await this.addGpuFrequencyTracks(ctx);
@@ -313,9 +312,11 @@
           name,
         ),
       });
-      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForThread(utid);
       const track = new TrackNode({uri, title: name, sortOrder: 30});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
   }
 
@@ -371,9 +372,11 @@
           name,
         ),
       });
-      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
       const track = new TrackNode({uri, title: name, sortOrder: 20});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
   }
 
diff --git a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
index e33e341..05718df 100644
--- a/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.CpuProfile/index.ts
@@ -19,11 +19,13 @@
 import {CpuProfileTrack} from './cpu_profile_track';
 import {getThreadUriPrefix} from '../../public/utils';
 import {exists} from '../../base/utils';
-import {getOrCreateGroupForThread} from '../../public/standard_groups';
 import {TrackNode} from '../../public/workspace';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.CpuProfile';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const result = await ctx.engine.query(`
       with thread_cpu_sample as (
@@ -62,9 +64,11 @@
         },
         track: new CpuProfileTrack(ctx, uri, utid),
       });
-      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForThread(utid);
       const track = new TrackNode({uri, title, sortOrder: -40});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.Frames/index.ts b/ui/src/plugins/dev.perfetto.Frames/index.ts
index 8d3abec..d9df86c 100644
--- a/ui/src/plugins/dev.perfetto.Frames/index.ts
+++ b/ui/src/plugins/dev.perfetto.Frames/index.ts
@@ -18,16 +18,18 @@
 } from '../../public/track_kinds';
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
-import {getOrCreateGroupForProcess} from '../../public/standard_groups';
 import {getTrackName} from '../../public/utils';
 import {TrackNode} from '../../public/workspace';
 import {NUM, NUM_NULL, STR, STR_NULL} from '../../trace_processor/query_result';
 import {ActualFramesTrack} from './actual_frames_track';
 import {ExpectedFramesTrack} from './expected_frames_track';
 import {FrameSelectionAggregator} from './frame_selection_aggregator';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.Frames';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     this.addExpectedFrames(ctx);
     this.addActualFrames(ctx);
@@ -88,9 +90,11 @@
           kind: EXPECTED_FRAMES_SLICE_TRACK_KIND,
         },
       });
-      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
       const track = new TrackNode({uri, title, sortOrder: -50});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
   }
 
@@ -151,9 +155,11 @@
           kind: ACTUAL_FRAMES_SLICE_TRACK_KIND,
         },
       });
-      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
       const track = new TrackNode({uri, title, sortOrder: -50});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.HeapProfile/index.ts b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
index 2e0591f..fcd79a3 100644
--- a/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.HeapProfile/index.ts
@@ -17,9 +17,9 @@
 import {PerfettoPlugin} from '../../public/plugin';
 import {LONG, NUM, STR} from '../../trace_processor/query_result';
 import {HeapProfileTrack} from './heap_profile_track';
-import {getOrCreateGroupForProcess} from '../../public/standard_groups';
 import {TrackNode} from '../../public/workspace';
 import {createPerfettoTable} from '../../trace_processor/sql_utils';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 function getUriForTrack(upid: number): string {
   return `/process_${upid}/heap_profile`;
@@ -27,6 +27,8 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.HeapProfile';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const it = await ctx.engine.query(`
       select value from stats
@@ -94,9 +96,11 @@
         },
         track: new HeapProfileTrack(ctx, uri, tableName, upid, incomplete),
       });
-      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
       const track = new TrackNode({uri, title, sortOrder: -30});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
 
     ctx.addEventListener('traceready', async () => {
diff --git a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
index 04e8b13..1488376 100644
--- a/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
+++ b/ui/src/plugins/dev.perfetto.PerfSamplesProfile/index.ts
@@ -23,11 +23,8 @@
   ThreadPerfSamplesProfileTrack,
 } from './perf_samples_profile_track';
 import {getThreadUriPrefix} from '../../public/utils';
-import {
-  getOrCreateGroupForProcess,
-  getOrCreateGroupForThread,
-} from '../../public/standard_groups';
 import {TrackNode} from '../../public/workspace';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 export interface Data extends TrackData {
   tsStarts: BigInt64Array;
@@ -39,6 +36,8 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.PerfSamplesProfile';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const pResult = await ctx.engine.query(`
       select distinct upid
@@ -59,9 +58,11 @@
         },
         track: new ProcessPerfSamplesProfileTrack(ctx, uri, upid),
       });
-      const group = getOrCreateGroupForProcess(ctx.workspace, upid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForProcess(upid);
       const track = new TrackNode({uri, title, sortOrder: -40});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
     const tResult = await ctx.engine.query(`
       select distinct
@@ -99,9 +100,11 @@
         },
         track: new ThreadPerfSamplesProfileTrack(ctx, uri, utid),
       });
-      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForThread(utid);
       const track = new TrackNode({uri, title, sortOrder: -50});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
 
     ctx.addEventListener('traceready', async () => {
diff --git a/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts b/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts
index 60aa11c..eb720c9 100644
--- a/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts
+++ b/ui/src/plugins/dev.perfetto.ProcessThreadGroups/index.ts
@@ -14,10 +14,6 @@
 
 import {Trace} from '../../public/trace';
 import {PerfettoPlugin} from '../../public/plugin';
-import {
-  getOrCreateGroupForProcess,
-  getOrCreateGroupForThread,
-} from '../../public/standard_groups';
 import {TrackNode} from '../../public/workspace';
 import {NUM, STR, STR_NULL} from '../../trace_processor/query_result';
 
@@ -41,24 +37,35 @@
 // including the kernel groups, sorting, and adding summary tracks.
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.ProcessThreadGroups';
-  async onTraceLoad(ctx: Trace): Promise<void> {
-    const processGroups = new Map<number, TrackNode>();
-    const threadGroups = new Map<number, TrackNode>();
 
+  private readonly processGroups = new Map<number, TrackNode>();
+  private readonly threadGroups = new Map<number, TrackNode>();
+
+  constructor(private readonly ctx: Trace) {}
+
+  getGroupForProcess(upid: number): TrackNode | undefined {
+    return this.processGroups.get(upid);
+  }
+
+  getGroupForThread(utid: number): TrackNode | undefined {
+    return this.threadGroups.get(utid);
+  }
+
+  async onTraceLoad(ctx: Trace): Promise<void> {
     // Pre-group all kernel "threads" (actually processes) if this is a linux
     // system trace. Below, addProcessTrackGroups will skip them due to an
     // existing group uuid, and addThreadStateTracks will fill in the
     // per-thread tracks. Quirk: since all threads will appear to be
     // TrackKindPriority.MAIN_THREAD, any process-level tracks will end up
     // pushed to the bottom of the group in the UI.
-    await this.addKernelThreadGrouping(ctx, threadGroups);
+    await this.addKernelThreadGrouping();
 
     // Create the per-process track groups. Note that this won't necessarily
     // create a track per process. If a process has been completely idle and has
     // no sched events, no track group will be emitted.
     // Will populate this.addTrackGroupActions
-    await this.addProcessGroups(ctx, processGroups, threadGroups);
-    await this.addThreadGroups(ctx, processGroups, threadGroups);
+    await this.addProcessGroups();
+    await this.addThreadGroups();
 
     ctx.addEventListener('traceready', () => {
       // If, by the time the trace has finished loading, some of the process or
@@ -68,15 +75,12 @@
           g.remove();
         }
       };
-      processGroups.forEach(removeIfEmpty);
-      threadGroups.forEach(removeIfEmpty);
+      this.processGroups.forEach(removeIfEmpty);
+      this.threadGroups.forEach(removeIfEmpty);
     });
   }
 
-  private async addKernelThreadGrouping(
-    ctx: Trace,
-    threadGroups: Map<number, TrackNode>,
-  ): Promise<void> {
+  private async addKernelThreadGrouping(): Promise<void> {
     // Identify kernel threads if this is a linux system trace, and sufficient
     // process information is available. Kernel threads are identified by being
     // children of kthreadd (always pid 2).
@@ -86,7 +90,7 @@
     // which has pid 0 but appears as a distinct process (with its own comm) on
     // each cpu. It'd make sense to exclude its thread state track, but still
     // put process-scoped tracks in this group.
-    const result = await ctx.engine.query(`
+    const result = await this.ctx.engine.query(`
       select
         t.utid, p.upid, (case p.pid when 2 then 1 else 0 end) isKthreadd
       from
@@ -123,28 +127,27 @@
       sortOrder: 50,
       isSummary: true,
     });
-    ctx.workspace.addChildInOrder(kernelThreadsGroup);
+    this.ctx.workspace.addChildInOrder(kernelThreadsGroup);
 
     // Set the group for all kernel threads (including kthreadd itself).
     for (; it.valid(); it.next()) {
       const {utid} = it;
 
-      const threadGroup = getOrCreateGroupForThread(ctx.workspace, utid);
-      threadGroup.headless = true;
+      const threadGroup = new TrackNode({
+        uri: `thread${utid}`,
+        title: `Thread ${utid}`,
+        isSummary: true,
+        headless: true,
+      });
       kernelThreadsGroup.addChildInOrder(threadGroup);
-
-      threadGroups.set(utid, threadGroup);
+      this.threadGroups.set(utid, threadGroup);
     }
   }
 
   // Adds top level groups for processes and thread that don't belong to a
   // process.
-  private async addProcessGroups(
-    ctx: Trace,
-    processGroups: Map<number, TrackNode>,
-    threadGroups: Map<number, TrackNode>,
-  ): Promise<void> {
-    const result = await ctx.engine.query(`
+  private async addProcessGroups(): Promise<void> {
+    const result = await this.ctx.engine.query(`
       with processGroups as (
         select
           upid,
@@ -231,7 +234,7 @@
 
       if (kind === 'process') {
         // Ignore kernel process groups
-        if (processGroups.has(uid)) {
+        if (this.processGroups.has(uid)) {
           continue;
         }
 
@@ -247,41 +250,41 @@
         }
 
         const displayName = getProcessDisplayName(name ?? undefined, id);
-        const group = getOrCreateGroupForProcess(ctx.workspace, uid);
-        group.title = displayName;
-        group.uri = `/process_${uid}`; // Summary track URI
-        group.sortOrder = 50;
+        const group = new TrackNode({
+          uri: `/process_${uid}`,
+          title: displayName,
+          isSummary: true,
+          sortOrder: 50,
+        });
 
         // Re-insert the child node to sort it
-        ctx.workspace.addChildInOrder(group);
-        processGroups.set(uid, group);
+        this.ctx.workspace.addChildInOrder(group);
+        this.processGroups.set(uid, group);
       } else {
         // Ignore kernel process groups
-        if (threadGroups.has(uid)) {
+        if (this.threadGroups.has(uid)) {
           continue;
         }
 
         const displayName = getThreadDisplayName(name ?? undefined, id);
-        const group = getOrCreateGroupForThread(ctx.workspace, uid);
-        group.title = displayName;
-        group.uri = `/thread_${uid}`; // Summary track URI
-        group.sortOrder = 50;
+        const group = new TrackNode({
+          uri: `/thread_${uid}`,
+          title: displayName,
+          isSummary: true,
+          sortOrder: 50,
+        });
 
         // Re-insert the child node to sort it
-        ctx.workspace.addChildInOrder(group);
-        threadGroups.set(uid, group);
+        this.ctx.workspace.addChildInOrder(group);
+        this.threadGroups.set(uid, group);
       }
     }
   }
 
   // Create all the nested & headless thread groups that live inside existing
   // process groups.
-  private async addThreadGroups(
-    ctx: Trace,
-    processGroups: Map<number, TrackNode>,
-    threadGroups: Map<number, TrackNode>,
-  ): Promise<void> {
-    const result = await ctx.engine.query(`
+  private async addThreadGroups(): Promise<void> {
+    const result = await this.ctx.engine.query(`
       with threadGroups as (
         select
           utid,
@@ -329,15 +332,18 @@
       const {utid, tid, upid, threadName} = it;
 
       // Ignore kernel thread groups
-      if (threadGroups.has(utid)) {
+      if (this.threadGroups.has(utid)) {
         continue;
       }
 
-      const group = getOrCreateGroupForThread(ctx.workspace, utid);
-      group.title = getThreadDisplayName(threadName ?? undefined, tid);
-      threadGroups.set(utid, group);
-      group.headless = true;
-      processGroups.get(upid)?.addChildInOrder(group);
+      const group = new TrackNode({
+        uri: `/thread_${utid}`,
+        title: getThreadDisplayName(threadName ?? undefined, tid),
+        isSummary: true,
+        headless: true,
+      });
+      this.threadGroups.set(utid, group);
+      this.processGroups.get(upid)?.addChildInOrder(group);
     }
   }
 }
diff --git a/ui/src/plugins/dev.perfetto.ThreadState/index.ts b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
index 155b35f..d7c2363 100644
--- a/ui/src/plugins/dev.perfetto.ThreadState/index.ts
+++ b/ui/src/plugins/dev.perfetto.ThreadState/index.ts
@@ -22,9 +22,9 @@
 import {getThreadStateTable} from './table';
 import {sqlTableRegistry} from '../../frontend/widgets/sql/table/sql_table_registry';
 import {TrackNode} from '../../public/workspace';
-import {getOrCreateGroupForThread} from '../../public/standard_groups';
 import {ThreadStateSelectionAggregator} from './thread_state_selection_aggregator';
 import {extensions} from '../../public/lib/extensions';
+import ProcessThreadGroupsPlugin from '../dev.perfetto.ProcessThreadGroups';
 
 function uriForThreadStateTrack(upid: number | null, utid: number): string {
   return `${getThreadUriPrefix(upid, utid)}_state`;
@@ -32,6 +32,8 @@
 
 export default class implements PerfettoPlugin {
   static readonly id = 'dev.perfetto.ThreadState';
+  static readonly dependencies = [ProcessThreadGroupsPlugin];
+
   async onTraceLoad(ctx: Trace): Promise<void> {
     const {engine} = ctx;
 
@@ -87,9 +89,11 @@
         track: new ThreadStateTrack(ctx, uri, utid),
       });
 
-      const group = getOrCreateGroupForThread(ctx.workspace, utid);
+      const group = ctx.plugins
+        .getPlugin(ProcessThreadGroupsPlugin)
+        .getGroupForThread(utid);
       const track = new TrackNode({uri, title, sortOrder: 10});
-      group.addChildInOrder(track);
+      group?.addChildInOrder(track);
     }
 
     sqlTableRegistry['thread_state'] = getThreadStateTable();
diff --git a/ui/src/public/standard_groups.ts b/ui/src/public/standard_groups.ts
index 61b844a..2bc7570 100644
--- a/ui/src/public/standard_groups.ts
+++ b/ui/src/public/standard_groups.ts
@@ -15,40 +15,6 @@
 import {TrackNode, TrackNodeArgs, Workspace} from './workspace';
 
 /**
- * Gets or creates a group for a given process given the normal grouping
- * conventions.
- *
- * @param workspace - The workspace to search for the group on.
- * @param upid - The upid of teh process to find.
- */
-export function getOrCreateGroupForProcess(
-  workspace: Workspace,
-  upid: number,
-): TrackNode {
-  return getOrCreateGroup(workspace, `process${upid}`, {
-    title: `Process ${upid}`,
-    isSummary: true,
-  });
-}
-
-/**
- * Gets or creates a group for a given thread given the normal grouping
- * conventions.
- *
- * @param workspace - The workspace to search for the group on.
- * @param utid - The utid of the thread to find.
- */
-export function getOrCreateGroupForThread(
-  workspace: Workspace,
-  utid: number,
-): TrackNode {
-  return getOrCreateGroup(workspace, `thread${utid}`, {
-    title: `Thread ${utid}`,
-    isSummary: true,
-  });
-}
-
-/**
  * Gets or creates a group for user interaction
  *
  * @param workspace - The workspace on which to create the group.