ui: fix crash with flamegraph on remount of area select (#7109)
diff --git a/ui/src/components/flamegraph_panel.ts b/ui/src/components/flamegraph_panel.ts
index 03451b2..43e58a0 100644
--- a/ui/src/components/flamegraph_panel.ts
+++ b/ui/src/components/flamegraph_panel.ts
@@ -14,7 +14,11 @@
 
 import type m from 'mithril';
 import type {Trace} from '../public/trace';
-import {QueryFlamegraph, type QueryFlamegraphMetric} from './query_flamegraph';
+import {
+  QueryFlamegraph,
+  type QueryFlamegraphDependency,
+  type QueryFlamegraphMetric,
+} from './query_flamegraph';
 import type {
   FlamegraphAddableMetric,
   FlamegraphState,
@@ -36,10 +40,10 @@
   readonly addableMetrics?: ReadonlyArray<FlamegraphAddableMetric>;
   readonly onAddMetric?: (metric: FlamegraphAddableMetric) => void;
 
-  // Perfetto tables / indices the metric SQL depends on. The panel forwards
-  // them to the inner `QueryFlamegraph`, which disposes them along with
-  // itself on unmount or when the array reference changes.
-  readonly dependencies?: ReadonlyArray<AsyncDisposable>;
+  // Shared Perfetto tables / indices the metric SQL depends on. The caller
+  // retains ownership; the panel keeps a clone alive until its inner
+  // `QueryFlamegraph` is disposed.
+  readonly dependencies?: ReadonlyArray<QueryFlamegraphDependency>;
 }
 
 // Mithril wrapper around `QueryFlamegraph` that owns the inner instance's
diff --git a/ui/src/components/query_flamegraph.ts b/ui/src/components/query_flamegraph.ts
index 019935c..a53898d 100644
--- a/ui/src/components/query_flamegraph.ts
+++ b/ui/src/components/query_flamegraph.ts
@@ -40,7 +40,7 @@
 import type {Trace} from '../public/trace';
 import {sqliteString} from '../base/string_utils';
 import {parseUserFilterRegex} from '../widgets/flamegraph_regex';
-import {SharedAsyncDisposable} from '../base/shared_disposable';
+import type {SharedAsyncDisposable} from '../base/shared_disposable';
 import {Monitor} from '../base/monitor';
 
 export interface QueryFlamegraphColumn {
@@ -195,6 +195,8 @@
   readonly unfilteredCumulativeValue: number;
 }
 
+export type QueryFlamegraphDependency = SharedAsyncDisposable<AsyncDisposable>;
+
 // A Perfetto UI component which wraps the `Flamegraph` widget and fetches the
 // data for the widget by querying an `Engine`.
 export class QueryFlamegraph implements AsyncDisposable {
@@ -212,9 +214,9 @@
 
   constructor(
     private readonly trace: Trace,
-    dependencies: ReadonlyArray<AsyncDisposable> = [],
+    dependencies: ReadonlyArray<QueryFlamegraphDependency> = [],
   ) {
-    this.dependencies = dependencies.map((d) => SharedAsyncDisposable.wrap(d));
+    this.dependencies = dependencies.map((d) => d.clone());
   }
 
   async [Symbol.asyncDispose](): Promise<void> {
@@ -260,11 +262,10 @@
     const engine = this.trace.engine;
     this.queryLimiter.schedule(async () => {
       this.data = undefined;
-      // Clone all the dependencies to make sure the the are not dropped while
-      // this function is running, adding them to the trash to make sure they
-      // are disposed after this function returns, but note this won't
-      // actually drop the tables unless this class instances have also been
-      // disposed due to the SharedAsyncDisposable logic.
+      // Clone all dependencies so they cannot be dropped while this function
+      // is running. Disposing these clones after the function returns does not
+      // drop the tables while either this instance or the caller still owns a
+      // clone.
       await using trash = new AsyncDisposableStack();
       for (const dependency of this.dependencies ?? []) {
         // If the dependency is disposed, it means that we have already ended
diff --git a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
index 7666caa..cc1cefd 100644
--- a/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
+++ b/ui/src/plugins/dev.perfetto.TraceProcessorTrack/index.ts
@@ -16,6 +16,8 @@
 import {removeFalsyValues} from '../../base/array_utils';
 import {AsyncLimiter} from '../../base/async_limiter';
 import {ensureExists} from '../../base/assert';
+import {AsyncDisposableStack} from '../../base/disposable_stack';
+import {SharedAsyncDisposable} from '../../base/shared_disposable';
 import {Time} from '../../base/time';
 import {
   createAggregationTab,
@@ -25,6 +27,7 @@
 import {openDistributionTab} from '../../components/distribution_panel';
 import {
   metricsFromTableOrSubquery,
+  type QueryFlamegraphDependency,
   type QueryFlamegraphMetric,
 } from '../../components/query_flamegraph';
 import {FlamegraphPanel} from '../../components/flamegraph_panel';
@@ -74,6 +77,11 @@
   typeof TRACE_PROCESSOR_TRACK_PLUGIN_STATE_SCHEMA
 >;
 
+interface SliceFlamegraphData extends AsyncDisposable {
+  readonly metrics: ReadonlyArray<QueryFlamegraphMetric>;
+  readonly dependencies: ReadonlyArray<QueryFlamegraphDependency>;
+}
+
 function createDetailsPanel(trace: Trace, utid: number | null) {
   if (utid === null) {
     return undefined;
@@ -605,12 +613,7 @@
 
   private createSliceFlameGraphPanel(trace: Trace) {
     let previousSelection: AreaSelection | undefined;
-    let computed:
-      | {
-          metrics: ReadonlyArray<QueryFlamegraphMetric>;
-          dependencies: ReadonlyArray<AsyncDisposable>;
-        }
-      | undefined;
+    let computed: SliceFlamegraphData | undefined;
     let isLoading = false;
     const limiter = new AsyncLimiter();
 
@@ -624,8 +627,10 @@
         previousSelection = selection;
         if (selectionChanged) {
           limiter.schedule(async () => {
+            const previousComputed = computed;
             computed = undefined;
             isLoading = true;
+            await previousComputed?.[Symbol.asyncDispose]();
             computed = await this.computeSliceFlamegraph(trace, selection);
             isLoading = false;
           });
@@ -657,13 +662,7 @@
   private async computeSliceFlamegraph(
     trace: Trace,
     currentSelection: AreaSelection,
-  ): Promise<
-    | {
-        metrics: ReadonlyArray<QueryFlamegraphMetric>;
-        dependencies: ReadonlyArray<AsyncDisposable>;
-      }
-    | undefined
-  > {
+  ): Promise<SliceFlamegraphData | undefined> {
     const trackIds = [];
     for (const trackInfo of currentSelection.tracks) {
       if (!trackInfo?.tags?.kinds?.includes(SLICE_TRACK_KIND)) {
@@ -698,11 +697,14 @@
       },
     });
 
-    const iiTable = await createIITable(
-      trace.engine,
-      dataset,
-      currentSelection.start,
-      currentSelection.end,
+    await using disposables = new AsyncDisposableStack();
+    const iiTable = disposables.use(
+      await createIITable(
+        trace.engine,
+        dataset,
+        currentSelection.start,
+        currentSelection.end,
+      ),
     );
     // Will be automatically cleaned up when `iiTable` is dropped.
     await createPerfettoIndex({
@@ -787,7 +789,14 @@
         metrics,
       );
     });
-    return {metrics, dependencies: [iiTable]};
+    const dependency: QueryFlamegraphDependency = SharedAsyncDisposable.wrap(
+      disposables.move(),
+    );
+    return {
+      metrics,
+      dependencies: [dependency],
+      [Symbol.asyncDispose]: () => dependency[Symbol.asyncDispose](),
+    };
   }
 
   private addMinimapContentProvider(ctx: Trace) {