ui: Refactor GpuCompute plugin data flow and slot query caching

Refactor GpuCompute plugin tab components for clean declarative data
flow. Summary rows and options are pre-fetched during trace load, and
KernelSummarySection synchronously renders presorted rows without
in-place mutations. KernelMetricsSection consumes mandatory data passed
from ComputeTab. QuerySlot caches raw metric groups keyed solely by
slice ID, enabling instant client-side terminology and unit updates.
diff --git a/ui/src/plugins/com.meta.GpuCompute/analysis.ts b/ui/src/plugins/com.meta.GpuCompute/analysis.ts
index bafe4e6..52d6912 100644
--- a/ui/src/plugins/com.meta.GpuCompute/analysis.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/analysis.ts
@@ -60,17 +60,17 @@
 export interface AnalysisProvider {
   // Renders the full Analysis tab body.
   renderAnalysisTab(attrs: {
-    engine: Engine;
-    sliceId: number;
-    analysisCache: AnalysisCache;
+    readonly engine: Engine;
+    readonly sliceId?: number;
+    readonly analysisCache: AnalysisCache;
   }): m.Children;
 
   // Renders a per-section inline analysis button.
   renderSectionAnalysis(attrs: {
-    section: MetricSection;
-    kernelData: KernelMetricData;
-    sliceId: number;
-    analysisCache: AnalysisCache;
+    readonly section: MetricSection;
+    readonly kernelData: KernelMetricData;
+    readonly sliceId?: number;
+    readonly analysisCache: AnalysisCache;
   }): m.Children;
 }
 
diff --git a/ui/src/plugins/com.meta.GpuCompute/details.ts b/ui/src/plugins/com.meta.GpuCompute/details.ts
index 8069f2a..11d7ee7 100644
--- a/ui/src/plugins/com.meta.GpuCompute/details.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/details.ts
@@ -297,7 +297,7 @@
 // =============================================================================
 
 // Intermediate grouping of a kernel's launch args and counter metrics.
-type KernelGroup = {
+export type KernelGroup = {
   kernelId: number;
   kernelName: string;
   launchTs: number;
@@ -510,29 +510,41 @@
   };
 }
 
+// Fetches raw kernel metric groups without applying terminology or unit humanization.
+// Keyed strictly by sliceId for QuerySlot caching.
+export async function fetchRawKernelMetricGroups(
+  ctx: GpuComputeContext,
+  engine: QueryCapable,
+  sliceId: number,
+): Promise<KernelGroup[]> {
+  const kernelQuery = buildKernelQuery(ctx, sliceId);
+  const kernelResult = await engine.query(kernelQuery);
+  const groups = reduceKernelRows(ctx, kernelResult.iter({}));
+  return Array.from(groups.values()).sort((a, b) => a.launchTs - b.launchTs);
+}
+
+// Materializes KernelMetricData from a raw KernelGroup using current context formatting.
+export function buildKernelMetricDataFromGroup(
+  ctx: GpuComputeContext,
+  group: KernelGroup,
+): KernelMetricData {
+  return buildMetricSectionData(
+    ctx,
+    group.kernelId,
+    group.kernelName,
+    group.metricsKV,
+    group.launchDur,
+  );
+}
+
 // Loads the full metric data for a single kernel slice.
-//
-// Executes the kernel query, reduces the rows, and builds
-// {@link KernelMetricData} for each kernel group (sorted by launch time).
 export async function fetchSelectedKernelMetricData(
   ctx: GpuComputeContext,
   engine: QueryCapable,
   sliceId: number,
 ): Promise<KernelMetricData[] | undefined> {
-  const kernelQuery = buildKernelQuery(ctx, sliceId);
-  const kernelResult = await engine.query(kernelQuery);
-  const groups = reduceKernelRows(ctx, kernelResult.iter({}));
-  return Array.from(groups.values())
-    .sort((a, b) => a.launchTs - b.launchTs)
-    .map((g) =>
-      buildMetricSectionData(
-        ctx,
-        g.kernelId,
-        g.kernelName,
-        g.metricsKV,
-        g.launchDur,
-      ),
-    );
+  const groups = await fetchRawKernelMetricGroups(ctx, engine, sliceId);
+  return groups.map((g) => buildKernelMetricDataFromGroup(ctx, g));
 }
 
 // =============================================================================
@@ -747,10 +759,11 @@
 // =============================================================================
 
 // Attrs accepted by the top-level {@link KernelMetricsSection} component.
-export interface KernelMetricsSectionSettings {
+export interface KernelMetricsSectionSettings extends m.Attributes {
   ctx: GpuComputeContext;
   engine: QueryCapable;
   sliceId?: number;
+  data: KernelMetricData[];
   renderKernel?: (
     kernel: KernelMetricData,
     renderCtx: {engine: QueryCapable},
@@ -759,90 +772,14 @@
   analysisCache?: AnalysisCache;
 }
 
-// Internal state for {@link KernelMetricsSection}.
-type DataState = {
-  kernelTableData?: KernelMetricData[];
-  loadedSliceId?: number;
-  loadedTerminologyId?: string;
-};
-
-function loadMetricData(
-  attrs: KernelMetricsSectionSettings,
-  state: DataState,
-): void {
-  state.loadedSliceId = attrs.sliceId;
-  state.loadedTerminologyId = attrs.ctx.terminologyId;
-
-  const findFirstWithMetrics = async (): Promise<KernelMetricData[]> => {
-    const sql = `
-      SELECT s.id
-      FROM gpu_slice s
-      INNER JOIN gpu_track tr ON tr.id = s.track_id
-      INNER JOIN counter c ON c.ts >= s.ts AND c.ts < s.ts + s.dur
-        AND c.track_id IN (
-          SELECT gc_tc.id FROM gpu_counter_track gc_tc
-          INNER JOIN gpu_counter_group gc ON gc.track_id = gc_tc.id
-            AND gc.group_id = ${COMPUTE_COUNTER_GROUP_ID}
-        )
-      WHERE s.render_stage_category = ${COMPUTE_RENDER_STAGE_CATEGORY}
-      LIMIT 1;
-    `;
-    const result = await attrs.engine.query(sql);
-    const iter = result.iter({});
-    if (!iter.valid()) return [];
-    const firstId = Number(iter.get('id'));
-    return (
-      (await fetchSelectedKernelMetricData(attrs.ctx, attrs.engine, firstId)) ??
-      []
-    );
-  };
-
-  const load = async () => {
-    let data: KernelMetricData[] = [];
-    if (attrs.sliceId != null) {
-      data =
-        (await fetchSelectedKernelMetricData(
-          attrs.ctx,
-          attrs.engine,
-          attrs.sliceId,
-        )) ?? [];
-      if (data.length === 0) {
-        data = await findFirstWithMetrics();
-      }
-    } else {
-      data = await findFirstWithMetrics();
-    }
-    state.kernelTableData = data;
-  };
-
-  load();
-}
-
 // Top-level Mithril component for the "Details" tab.
 //
-// Loads the metric data for the selected slice (or finds the first
+// Displays the metric data for the selected slice (or finds the first
 // kernel with metrics), builds section collapse state, and renders
-// the full metric table tree. Re-fetches when sliceId or terminology
-// changes.
-export const KernelMetricsSection: m.Component<
-  KernelMetricsSectionSettings,
-  DataState
-> = {
-  oninit: ({attrs, state}) => {
-    loadMetricData(attrs, state);
-  },
-
-  onbeforeupdate: ({attrs, state}) => {
-    if (
-      attrs.sliceId !== state.loadedSliceId ||
-      attrs.ctx.terminologyId !== state.loadedTerminologyId
-    ) {
-      loadMetricData(attrs, state);
-    }
-  },
-
-  view: ({attrs, state}) => {
-    if (!state.kernelTableData) return null;
+// the full metric table tree.
+export const KernelMetricsSection: m.Component<KernelMetricsSectionSettings> = {
+  view({attrs}) {
+    const kernelTableData = attrs.data;
 
     const baselineLookup:
       Map<string, {unit: string; value: number | string}> | undefined = (() => {
@@ -905,16 +842,13 @@
       );
     };
 
-    if (state.kernelTableData.length === 0) {
+    if (kernelTableData.length === 0) {
       return m(
         '.pf-gpu-compute__pad',
         m('p', 'No kernel compute metrics for this trace.'),
       );
     }
 
-    return m(
-      '.pf-gpu-compute__pad',
-      renderSingleTable(state.kernelTableData[0]),
-    );
+    return m('.pf-gpu-compute__pad', renderSingleTable(kernelTableData[0]));
   },
 };
diff --git a/ui/src/plugins/com.meta.GpuCompute/index.ts b/ui/src/plugins/com.meta.GpuCompute/index.ts
index 761ed49..8eeed00 100644
--- a/ui/src/plugins/com.meta.GpuCompute/index.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/index.ts
@@ -17,7 +17,12 @@
 import type {PerfettoPlugin} from '../../public/plugin';
 import type {Trace} from '../../public/trace';
 import type {Engine} from '../../trace_processor/engine';
-import {KernelMetricsSection, fetchSelectedKernelMetricData} from './details';
+import {
+  KernelMetricsSection,
+  type KernelGroup,
+  fetchRawKernelMetricGroups,
+  buildKernelMetricDataFromGroup,
+} from './details';
 import type {TrackEventSelection} from '../../public/selection';
 import {renderToolbar} from './toolbar';
 import type {InfoTab} from './toolbar';
@@ -42,7 +47,9 @@
   type AnalysisProvider,
   AnalysisProviderHolder,
 } from './analysis';
-import {AtomicTaskQueue, AsyncMemo} from '../../base/async_memo';
+import type {Tab} from '../../public/tab';
+import {maybeUndefined} from '../../base/utils';
+import {AsyncMemo} from '../../base/async_memo';
 
 export interface GpuComputeContext {
   humanizeMetrics: boolean;
@@ -53,47 +60,20 @@
   readonly analysisProviderHolder: AnalysisProviderHolder;
 }
 
-class Compute {
-  readonly ctx: GpuComputeContext;
-
-  constructor(
-    private readonly engine: Engine,
-    private readonly trace: Trace,
-    private readonly tabUri: string,
-    terminologyRegistry: TerminologyRegistry,
-    sectionRegistry: SectionRegistry,
-    analysisProviderHolder: AnalysisProviderHolder,
-  ) {
-    this.ctx = {
-      humanizeMetrics: true,
-      activeInfoTab: 'summary',
-      terminologyId: 'cuda',
-      terminologyRegistry,
-      sectionRegistry,
-      analysisProviderHolder,
-    };
-  }
-
-  private sliceId: number | undefined = -1;
-  private options: KernelLaunchOption[] = [];
-  private summaryRows: SummaryRow[] = [];
-  private knownKernelIds = new Set<number>();
+class ComputeTab implements Tab {
+  private readonly ctx: GpuComputeContext;
+  private readonly knownKernelIds = new Set<number>();
 
   // Selection-driven metric fetching via QuerySlot. Selection changes
   // trigger mithril redraws; render() reads the current selection and
   // polls the QuerySlot which handles deduplication, background
   // fetching, and race-condition prevention.
-  private readonly taskQueue = new AtomicTaskQueue();
-  private readonly selectionSlot = new AsyncMemo<{
-    hasMetrics: boolean;
-    toolbar?: ToolbarInfo;
-  }>(this.taskQueue);
-  private hadSelection = false;
-  private appliedSelectionSliceId: number | undefined;
+  private readonly selectionSlot = new AsyncMemo<KernelGroup[]>();
+  private readonly baselineSlot = new AsyncMemo<KernelGroup[]>();
 
-  private baselineSliceId: number | undefined = undefined;
-  private baselineToolbarInfo?: ToolbarInfo;
-  private baselineData?: KernelMetricData;
+  private selectedKernelId?: number;
+  private baselineKernelId?: number;
+  private prevTimelineSelectionId?: number;
 
   // Cache for storing analysis results by sliceId
   private readonly kernelAnalysisCache = new Map<
@@ -134,225 +114,195 @@
     },
   };
 
-  public getTitle() {
+  constructor(
+    private readonly engine: Engine,
+    private readonly trace: Trace,
+    private readonly tabUri: string,
+    terminologyRegistry: TerminologyRegistry,
+    sectionRegistry: SectionRegistry,
+    analysisProviderHolder: AnalysisProviderHolder,
+    private readonly summaryRows: readonly SummaryRow[],
+    private readonly options: readonly KernelLaunchOption[],
+  ) {
+    this.ctx = {
+      humanizeMetrics: true,
+      activeInfoTab: 'summary',
+      terminologyId: 'cuda',
+      terminologyRegistry,
+      sectionRegistry,
+      analysisProviderHolder,
+    };
+    this.knownKernelIds = new Set(options.map((o) => o.id));
+
+    // Initially select the first kernel (if we have one)
+    this.selectedKernelId = maybeUndefined(this.options[0])?.id;
+  }
+
+  // Fetch and process all kernel-related performance metric data from the trace
+  // If a slice is selected, we pass sliceId so the component shows only the
+  // selected kernel compute metrics if they are available
+  render(): m.Children {
+    this.maybeSyncTimelineSelection();
+
+    const {toolbar: toolbarInfo, data: selectionData} = this.getSelectionData();
+    const {toolbar: baselineToolbarInfo, data: baselineData} =
+      this.getBaselineData();
+
+    return m('', [
+      renderToolbar({
+        ctx: this.ctx,
+        options: this.options,
+        sliceId: this.selectedKernelId,
+        onChange: (id, suppress) => this.setSliceId(id, suppress),
+        toolbarInfo,
+        baselineId: this.baselineKernelId,
+        baselineInfo: baselineToolbarInfo,
+        baselineEnabled: this.baselineKernelId !== undefined,
+        onToggleBaseline: (enabled: boolean) => this.setBaselineId(enabled),
+      }),
+      this.renderBody(selectionData, baselineData),
+    ]);
+  }
+
+  getTitle() {
     return 'GPU Compute';
   }
 
-  // Updates which kernel is shown from the dropdown. Fetches toolbar
-  // metrics for the selected kernel and switches to the appropriate tab.
-  public async setSliceId(sliceId: number, suppressAutoDetails = false) {
-    const firstId = this.options[0]?.id ?? -1;
-    this.sliceId = sliceId !== -1 ? sliceId : firstId;
-    const requestedSliceId = this.sliceId;
-
-    if (this.sliceId === -1) {
-      this.setToolbarInfo(undefined);
-      this.ctx.activeInfoTab = 'summary';
-      return;
+  // Fetch and process all kernel-related performance metric data from the trace
+  private renderBody(
+    selectionData?: KernelMetricData[],
+    baselineData?: KernelMetricData,
+  ): m.Children {
+    if (this.ctx.activeInfoTab === 'summary') {
+      return m(KernelSummarySection, {
+        ctx: this.ctx,
+        engine: this.engine,
+        sliceId: this.selectedKernelId,
+        openSliceInDetail: (id: number) => this.setSliceId(id),
+        prefetchedRows: this.summaryRows,
+      });
     }
 
-    try {
-      const data = await fetchSelectedKernelMetricData(
-        this.ctx,
-        this.engine,
-        this.sliceId,
-      );
-      // Guard against stale async completions: if the sliceId changed
-      // while the query was in flight, discard the result.
-      if (this.sliceId !== requestedSliceId) return;
-      const hasMetrics = Array.isArray(data) && data.length > 0;
-      this.setToolbarInfo(hasMetrics ? data[0].toolbar : undefined);
-      if (!hasMetrics) {
-        this.sliceId = firstId;
-        this.ctx.activeInfoTab = 'summary';
-      } else if (!suppressAutoDetails) {
-        this.ctx.activeInfoTab = 'details';
+    if (selectionData === undefined) {
+      return null;
+    }
+
+    if (this.ctx.activeInfoTab === 'analysis') {
+      const provider = this.ctx.analysisProviderHolder.get();
+      if (provider) {
+        return provider.renderAnalysisTab({
+          engine: this.engine,
+          sliceId: this.selectedKernelId,
+          analysisCache: this.analysisCache,
+        });
       }
-    } catch (e) {
-      console.warn('GpuCompute: failed to fetch toolbar metrics:', e);
-      this.setToolbarInfo(undefined);
+      return m('.pf-gpu-compute__pad', 'Analysis plugin not enabled.');
+    }
+
+    return m(KernelMetricsSection, {
+      ctx: this.ctx,
+      engine: this.engine,
+      sliceId: this.selectedKernelId,
+      data: selectionData,
+      baseline: baselineData,
+      analysisCache: this.analysisCache,
+    });
+  }
+
+  // Updates which kernel is shown from the dropdown or summary section.
+  private setSliceId(sliceId: number, suppressAutoDetails = false) {
+    this.selectedKernelId = sliceId;
+    if (!suppressAutoDetails) {
+      this.ctx.activeInfoTab = 'details';
     }
   }
 
-  private async setBaselineId(useCurrent: boolean) {
-    // If not using current, clear baseline state
-    if (!useCurrent) {
-      this.baselineSliceId = undefined;
-      this.baselineToolbarInfo = undefined;
-      this.baselineData = undefined;
+  private setBaselineId(enabled: boolean) {
+    // If not enabled, clear baseline state
+    if (!enabled) {
+      this.baselineKernelId = undefined;
       return;
     }
 
     // Setting the baseline id to the current selection, otherwise fallback to the first kernel launch metrics
-    const id =
-      this.sliceId != null && this.sliceId !== -1
-        ? this.sliceId
-        : this.options[0]?.id;
-    if (id == null) return;
-    this.baselineSliceId = id;
-
-    // Fetching baseline metrics to populate the toolbar info with baselineData
-    try {
-      const data = await fetchSelectedKernelMetricData(
-        this.ctx,
-        this.engine,
-        id,
-      );
-      this.baselineToolbarInfo = data?.[0]?.toolbar;
-      this.baselineData = data?.[0];
-    } catch {
-      this.baselineToolbarInfo = undefined;
-      this.baselineData = undefined;
-    }
+    const id = this.selectedKernelId ?? this.options[0]?.id;
+    if (id === undefined) return;
+    this.baselineKernelId = id;
   }
 
-  private async refreshBaseline() {
-    if (this.baselineSliceId == null) return;
+  private getSelectionData(): {
+    toolbar?: ToolbarInfo;
+    data?: KernelMetricData[];
+  } {
+    const sliceId = this.selectedKernelId;
+    if (sliceId === undefined) return {};
 
-    try {
-      const data = await fetchSelectedKernelMetricData(
-        this.ctx,
-        this.engine,
-        this.baselineSliceId,
-      );
-      this.baselineToolbarInfo = data?.[0]?.toolbar;
-      this.baselineData = data?.[0];
-    } catch {
-      this.baselineToolbarInfo = undefined;
-      this.baselineData = undefined;
-    }
+    const selectionResult = this.selectionSlot.use({
+      key: {sliceId},
+      retainOn: ['sliceId'],
+      compute: async () => {
+        return fetchRawKernelMetricGroups(this.ctx, this.engine, sliceId);
+      },
+    });
+
+    const groups = selectionResult.data;
+    if (!groups) return {};
+
+    const data = groups.map((g) => buildKernelMetricDataFromGroup(this.ctx, g));
+    return {
+      toolbar: data[0]?.toolbar,
+      data,
+    };
   }
 
-  // Updates the "Results" dropdown with new launch options
-  public setOptions(opts: KernelLaunchOption[]) {
-    this.options = opts;
-    this.knownKernelIds = new Set(opts.map((o) => o.id));
-  }
-
-  public setSummaryRows(rows: SummaryRow[]) {
-    this.summaryRows = rows;
-  }
-
-  // Populating the toolbar with the correct kernel's launch info
-  private toolbarInfo?: ToolbarInfo;
-  public setToolbarInfo(info?: ToolbarInfo) {
-    this.toolbarInfo = info;
-  }
-
-  // Used to re-fetch the same items so both toolbar + tables reflect new mode
-  private async refresh() {
-    if (this.sliceId != null && this.sliceId !== -1) {
-      await this.setSliceId(this.sliceId);
+  private getBaselineData(): {
+    toolbar?: ToolbarInfo;
+    data?: KernelMetricData;
+  } {
+    if (this.baselineKernelId === undefined) {
+      return {};
     }
 
-    // If there's an active baseline, re-fetch it for the same sliceId (don't overwrite it with current!)
-    if (this.baselineSliceId != null) {
-      await this.refreshBaseline();
-    }
+    const baselineResult = this.baselineSlot.use({
+      key: {sliceId: this.baselineKernelId},
+      retainOn: ['sliceId'],
+      compute: async () => {
+        return fetchRawKernelMetricGroups(
+          this.ctx,
+          this.engine,
+          this.baselineKernelId!,
+        );
+      },
+    });
+
+    const groups = baselineResult.data;
+    if (!groups || groups.length === 0) return {};
+
+    const data = buildKernelMetricDataFromGroup(this.ctx, groups[0]);
+    return {
+      toolbar: data.toolbar,
+      data,
+    };
   }
 
-  // Fetch and process all kernel-related performance metric data from the trace
-  // If a slice is selected, we pass sliceId so the component shows only the selected kernel compute metrics if they are available
-  render(): m.Children {
+  private maybeSyncTimelineSelection(): void {
     const sel = this.trace.selection.selection;
     const selSliceId =
       sel.kind === 'track_event'
         ? (sel as TrackEventSelection).eventId
         : undefined;
 
-    if (selSliceId !== undefined) {
-      this.hadSelection = true;
-      const id = selSliceId;
-      const isKnownKernel = this.knownKernelIds.has(id);
-
-      // Show the tab immediately for known kernels so the user doesn't
-      // see a flicker to the "Current Selection" tab while the async
-      // metric query is in flight.
-      if (isKnownKernel && this.appliedSelectionSliceId !== selSliceId) {
-        this.sliceId = selSliceId;
+    if (
+      selSliceId !== undefined &&
+      selSliceId !== this.prevTimelineSelectionId
+    ) {
+      this.prevTimelineSelectionId = selSliceId;
+      if (this.knownKernelIds.has(selSliceId)) {
+        this.selectedKernelId = selSliceId;
         this.ctx.activeInfoTab = 'details';
         this.trace.tabs.showTab(this.tabUri);
       }
-
-      const result = this.selectionSlot.use({
-        key: {sliceId: id},
-        compute: async () => {
-          const data = await fetchSelectedKernelMetricData(
-            this.ctx,
-            this.engine,
-            id,
-          );
-          const hasMetrics = Array.isArray(data) && data.length > 0;
-          return {
-            hasMetrics,
-            toolbar: hasMetrics ? data[0].toolbar : undefined,
-          };
-        },
-      });
-
-      if (result.data && this.appliedSelectionSliceId !== selSliceId) {
-        this.appliedSelectionSliceId = selSliceId;
-        if (result.data.hasMetrics) {
-          this.sliceId = selSliceId;
-          this.setToolbarInfo(result.data.toolbar);
-        } else if (!isKnownKernel) {
-          this.sliceId = this.options[0]?.id ?? -1;
-          this.setToolbarInfo(undefined);
-          this.ctx.activeInfoTab = 'summary';
-        }
-      }
-    } else if (this.hadSelection) {
-      this.hadSelection = false;
-      this.appliedSelectionSliceId = undefined;
     }
-
-    const toolbar = renderToolbar({
-      ctx: this.ctx,
-      options: this.options,
-      sliceId: this.sliceId ?? undefined,
-      onChange: (id, suppress) => this.setSliceId(id ?? -1, suppress),
-      toolbarInfo: this.toolbarInfo,
-      baselineId: this.baselineSliceId,
-      baselineInfo: this.baselineToolbarInfo,
-      baselineEnabled: this.baselineSliceId != null,
-      onHumanizeChanged: () => this.refresh(),
-      onToggleBaseline: (enabled: boolean) => this.setBaselineId(enabled),
-      onTerminologyChanged: () => this.refresh(),
-    });
-
-    const effectiveSliceId = this.sliceId ?? -1;
-    let body: m.Children;
-
-    if (this.ctx.activeInfoTab === 'summary') {
-      body = m(KernelSummarySection, {
-        ctx: this.ctx,
-        engine: this.engine,
-        sliceId: effectiveSliceId,
-        openSliceInDetail: (id: number) => this.setSliceId(id),
-        prefetchedRows: this.summaryRows,
-      });
-    } else if (this.ctx.activeInfoTab === 'analysis') {
-      const provider = this.ctx.analysisProviderHolder.get();
-      if (provider) {
-        body = provider.renderAnalysisTab({
-          engine: this.engine,
-          sliceId: effectiveSliceId,
-          analysisCache: this.analysisCache,
-        });
-      } else {
-        body = m('.pf-gpu-compute__pad', 'Analysis plugin not enabled.');
-      }
-    } else {
-      body = m(KernelMetricsSection, {
-        ctx: this.ctx,
-        engine: this.engine,
-        sliceId: effectiveSliceId,
-        baseline: this.baselineData,
-        analysisCache: this.analysisCache,
-      });
-    }
-
-    return m('div', [toolbar, body]);
   }
 }
 
@@ -397,39 +347,54 @@
 
   async onTraceLoad(trace: Trace): Promise<void> {
     const tabUri = `${GpuComputePlugin.id}#Compute`;
-    trace.commands.registerCommand({
-      id: `${GpuComputePlugin.id}#ShowComputeTab`,
-      name: 'Show Compute Tab',
-      callback: () => trace.tabs.showTab(tabUri),
-    });
 
-    const content = new Compute(
+    const rows = await this.fetchSummaryRows(trace);
+    const options = rows.map((r) => ({id: r.id, label: r.demangledName}));
+
+    if (options.length === 0) {
+      // No kernels - don't show the tab
+      return;
+    }
+
+    const content = new ComputeTab(
       trace.engine,
       trace,
       tabUri,
       this.terminologyRegistry,
       this.sectionRegistry,
       this.analysisProviderHolder,
+      rows,
+      options,
     );
+
+    trace.commands.registerCommand({
+      id: `${GpuComputePlugin.id}#ShowComputeTab`,
+      name: 'Show Compute Tab',
+      callback: () => trace.tabs.showTab(tabUri),
+    });
+
     trace.tabs.registerTab({
       isEphemeral: false,
       uri: tabUri,
       content: content,
     });
 
+    if (options.length > 0) {
+      trace.tabs.showTab(tabUri);
+    }
+  }
+
+  private async fetchSummaryRows(trace: Trace) {
     try {
       const rows = await fetchKernelSummaryRows(
         this.getContext(),
         trace.engine,
       );
-      content.setSummaryRows(rows);
-      content.setOptions(rows.map((r) => ({id: r.id, label: r.demangledName})));
-      if (rows.length > 0) {
-        trace.tabs.showTab(tabUri);
-      }
+      rows.sort((a, b) => a.id - b.id);
+      return rows;
     } catch (e) {
       console.warn('GpuCompute: failed to fetch kernel launch list:', e);
-      content.setOptions([]);
+      return [];
     }
   }
 }
diff --git a/ui/src/plugins/com.meta.GpuCompute/summary.ts b/ui/src/plugins/com.meta.GpuCompute/summary.ts
index 461624e..e10c598 100644
--- a/ui/src/plugins/com.meta.GpuCompute/summary.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/summary.ts
@@ -31,51 +31,39 @@
 import {Icon} from '../../widgets/icon';
 import type {GpuComputeContext} from './index';
 import {adjustSeconds} from './humanize';
+import {NUM, NUM_NULL, STR_NULL} from '../../trace_processor/query_result';
+import {assertUnreachable} from '../../base/assert';
 
 // Per-kernel row returned by {@link fetchKernelSummaryRows}.
 export type SummaryRow = {
-  id: number;
-  demangledName: string;
-  durationNSecNum: number | string | null;
-  computePct: number | string | null;
-  memoryPct: number | string | null;
-  registersPerThread: number | string | null;
-  gridSize: number | string | null;
+  readonly id: number;
+  readonly demangledName: string;
+  readonly durationNSecNum: number;
+  readonly computePct: number;
+  readonly memoryPct: number;
+  readonly registersPerThread: number;
+  readonly gridSize: number;
 };
 
 const PAGE_SIZE = 100;
 
-// Component state holding the fetched rows and per-column max values.
-type SummaryState = {
-  rows?: SummaryRow[];
-  maxDurationNSec?: number;
-  maxComputePct?: number;
-  maxMemoryPct?: number;
-  maxRegisters?: number;
-  maxGridSize?: number;
-  launchIndexBySliceId: Map<number, number>;
-  sortKey: SortKey | null;
-  sortDescending: boolean;
-  pageOffset: number;
-};
-
 // Renders a bar whose width is proportional to `val / max`.
 // Falls back to `—` when the value is non-finite or missing.
-const renderRelPercentBar = (
-  val?: number,
-  max?: number,
+function renderRelPercentBar(
+  val: number,
+  max?: number, // Undefined means no max
   label?: string,
-): m.Children => {
-  const hasLabel = typeof label === 'string' && label.trim() !== '';
-  const curVal = Number(val);
-  const maxVal = Number(max);
+): m.Children {
+  const hasLabel = label !== undefined && label.trim() !== '';
+  const curVal = val;
+  const maxVal = Number(max); // NaN if undefined
   if (!Number.isFinite(curVal) || !Number.isFinite(maxVal) || maxVal <= 0) {
     return hasLabel ? renderPercentBar(0, null, false, label) : '—';
   }
   const clamped = Math.max(0, Math.min(curVal, maxVal));
   const pct = Math.max(0, Math.min(100, (clamped / maxVal) * 100));
   return renderPercentBar(pct, null, false, label ?? '');
-};
+}
 
 // =============================================================================
 // Data fetching
@@ -135,10 +123,10 @@
         EXTRACT_ARG(cs.arg_set_id, 'kernel_name'),
         cs.name
       ) AS demangledName,
-      CAST(EXTRACT_ARG(cs.arg_set_id, 'registers_per_thread') AS REAL) AS registers_per_thread,
+      CAST(EXTRACT_ARG(cs.arg_set_id, 'registers_per_thread') AS REAL) AS registersPerThread,
       CAST(EXTRACT_ARG(cs.arg_set_id, 'launch.grid_size.x') AS REAL)
         * CAST(COALESCE(EXTRACT_ARG(cs.arg_set_id, 'launch.grid_size.y'), 1) AS REAL)
-        * CAST(COALESCE(EXTRACT_ARG(cs.arg_set_id, 'launch.grid_size.z'), 1) AS REAL) AS grid_size,
+        * CAST(COALESCE(EXTRACT_ARG(cs.arg_set_id, 'launch.grid_size.z'), 1) AS REAL) AS gridSize,
       COALESCE(
         ${coalesceExpr(durationNames)},
         CAST(cs.dur AS REAL)
@@ -150,18 +138,27 @@
   `;
 
   const result = await engine.query(sql);
-  const iter = result.iter({});
+  const iter = result.iter({
+    id: NUM,
+    demangledName: STR_NULL,
+    durationNSecNum: NUM_NULL,
+    // The follwoing can be string | number | null - we don't have a type for
+    // this - just use unknown and cast later
+    computePct: NUM_NULL,
+    memoryPct: NUM_NULL,
+    registersPerThread: NUM_NULL,
+    gridSize: NUM_NULL,
+  });
   const list: SummaryRow[] = [];
   while (iter.valid()) {
     list.push({
-      id: Number(iter.get('id')),
-      demangledName: String(iter.get('demangledName') ?? ''),
-      durationNSecNum: (iter.get('durationNSecNum') as number | null) ?? null,
-      computePct: (iter.get('computePct') as number | string | null) ?? null,
-      memoryPct: (iter.get('memoryPct') as number | string | null) ?? null,
-      registersPerThread:
-        (iter.get('registers_per_thread') as number | string | null) ?? null,
-      gridSize: (iter.get('grid_size') as number | string | null) ?? null,
+      id: iter.id,
+      demangledName: iter.demangledName ?? '',
+      durationNSecNum: iter.durationNSecNum ?? 0,
+      computePct: iter.computePct ?? 0,
+      memoryPct: iter.memoryPct ?? 0,
+      registersPerThread: iter.registersPerThread ?? 0,
+      gridSize: iter.gridSize ?? 0,
     });
     iter.next();
   }
@@ -174,11 +171,11 @@
 
 // Attrs accepted by {@link KernelSummarySection}.
 export interface SummarySectionAttrs extends m.Attributes {
-  ctx: GpuComputeContext;
-  engine: Engine;
-  sliceId?: number;
-  openSliceInDetail?: (sliceId: number) => void;
-  prefetchedRows?: SummaryRow[];
+  readonly ctx: GpuComputeContext;
+  readonly engine: Engine;
+  readonly sliceId?: number;
+  readonly openSliceInDetail?: (sliceId: number) => void;
+  readonly prefetchedRows: readonly SummaryRow[];
 }
 
 // Column keys that the table can be sorted by.
@@ -189,23 +186,25 @@
 function getSortableValue(
   r: SummaryRow,
   key: SortKey,
-  launchIndex: Map<number, number>,
-): number | string | undefined {
+  launchIndex: ReadonlyMap<number, number>,
+): number | string {
   switch (key) {
     case 'id':
       return launchIndex.get(r.id) ?? r.id;
     case 'name':
-      return r.demangledName ?? '';
+      return r.demangledName;
     case 'duration':
-      return Number(r.durationNSecNum);
+      return r.durationNSecNum;
     case 'compute':
-      return Number(r.computePct);
+      return r.computePct;
     case 'memory':
-      return Number(r.memoryPct);
+      return r.memoryPct;
     case 'registers':
-      return Number(r.registersPerThread);
+      return r.registersPerThread;
     case 'grid_size':
-      return Number(r.gridSize);
+      return r.gridSize;
+    default:
+      assertUnreachable(key);
   }
 }
 
@@ -235,7 +234,7 @@
 
   // Numeric comparison
   if (isANum && isBNum) {
-    const delta = Number(aVal) - Number(bVal);
+    const delta = aVal - bVal;
     return descending ? -Math.sign(delta) : Math.sign(delta);
   }
 
@@ -247,175 +246,154 @@
 
 // Mithril component that renders the summary table.
 //
-// On init it fetches all kernel launches via {@link fetchKernelSummaryRows},
-// computes per-column max values for the relative bars, and renders a
-// sortable `<table>` whose rows can be double-clicked to navigate to
+// Renders a sortable `<table>` whose rows can be double-clicked to navigate to
 // the kernel's detail view.
-export const KernelSummarySection: m.Component<
-  SummarySectionAttrs,
-  SummaryState
-> = {
-  async oninit({attrs, state}) {
-    state.launchIndexBySliceId = new Map();
-    state.sortKey = 'id';
-    state.sortDescending = false;
-    state.pageOffset = 0;
+export function KernelSummarySection({
+  attrs,
+}: m.Vnode<SummarySectionAttrs>): m.Component<SummarySectionAttrs> {
+  const launchIndexBySliceId = new Map();
+  let sortKey: SortKey = 'id';
+  let sortDescending = false;
+  let pageOffset = 0;
 
-    const rows =
-      attrs.prefetchedRows ??
-      (await fetchKernelSummaryRows(attrs.ctx, attrs.engine));
+  const rows = attrs.prefetchedRows;
 
-    // Build launch-order map so the ID column shows 0, 1, 2, …
-    rows.forEach((opt, zeroBasedIndex) =>
-      state.launchIndexBySliceId.set(opt.id, zeroBasedIndex),
-    );
+  // Build launch-order map so the ID column shows 0, 1, 2, …
+  rows.forEach((opt, zeroBasedIndex) =>
+    launchIndexBySliceId.set(opt.id, zeroBasedIndex),
+  );
 
-    // Initial sort by launch order (ascending)
-    rows.sort((a, b) => a.id - b.id);
+  // Per-column max values drive the relative percent-bar widths
+  const finiteMax = (arr: number[]) => {
+    const nums = arr.filter((x) => Number.isFinite(x));
+    if (nums.length === 0) {
+      return undefined;
+    }
+    // Reduce, not `Math.max(...nums)`, which overflows the stack for large arrays.
+    return nums.reduce((a, b) => Math.max(a, b));
+  };
 
-    // Per-column max values drive the relative percent-bar widths
-    const finiteMax = (arr: Array<number | null | undefined>) => {
-      const nums = arr
-        .map(Number)
-        .filter((x) => Number.isFinite(x)) as number[];
-      if (nums.length === 0) {
-        return undefined;
-      }
-      // Reduce, not `Math.max(...nums)`, which overflows the stack for large arrays.
-      return nums.reduce((a, b) => Math.max(a, b));
-    };
+  const maxDurationNSec = finiteMax(rows.map((r) => r.durationNSecNum));
+  const maxComputePct = finiteMax(rows.map((r) => r.computePct));
+  const maxMemoryPct = finiteMax(rows.map((r) => r.memoryPct));
+  const maxRegisters = finiteMax(rows.map((r) => r.registersPerThread));
+  const maxGridSize = finiteMax(rows.map((r) => r.gridSize));
 
-    state.rows = rows;
-    state.maxDurationNSec = finiteMax(
-      rows.map((r) => Number(r.durationNSecNum)),
-    );
-    state.maxComputePct = finiteMax(rows.map((r) => Number(r.computePct)));
-    state.maxMemoryPct = finiteMax(rows.map((r) => Number(r.memoryPct)));
-    state.maxRegisters = finiteMax(
-      rows.map((r) => Number(r.registersPerThread)),
-    );
-    state.maxGridSize = finiteMax(rows.map((r) => Number(r.gridSize)));
-  },
+  return {
+    view({attrs}) {
+      const terminology = attrs.ctx.terminologyRegistry.get(
+        attrs.ctx.terminologyId,
+      );
 
-  view({state, attrs}) {
-    const terminology = attrs.ctx.terminologyRegistry.get(
-      attrs.ctx.terminologyId,
-    );
-
-    const rows = state.rows ?? [];
-
-    // Formats a raw metric value into a display label with optional unit.
-    const label = (
-      val: number | string | null | undefined,
-      unit?: string,
-    ): string => {
-      if (val == null || val === 'null' || val === 'undefined') {
-        return '—';
-      }
-
-      // Humanize seconds when enabled
-      if (unit === 'nsecond' && Number.isFinite(Number(val))) {
-        if (attrs.ctx.humanizeMetrics) {
-          const {value: v, unit: u} = adjustSeconds(Number(val) / 1e9);
-          return `${formatNumber(v)} ${u}`;
+      // Formats a raw metric value into a display label with optional unit.
+      const label = (
+        val: number | string | null | undefined,
+        unit?: string,
+      ): string => {
+        if (val == null || val === 'null' || val === 'undefined') {
+          return '—';
         }
-        return `${formatNumber(Number(val))} nsecond`;
-      }
 
-      const text = Number.isFinite(Number(val))
-        ? String(formatNumber(Number(val)))
-        : String(val);
-      return unit ? `${text} ${unit}` : text;
-    };
+        // Humanize seconds when enabled
+        if (unit === 'nsecond' && Number.isFinite(Number(val))) {
+          if (attrs.ctx.humanizeMetrics) {
+            const {value: v, unit: u} = adjustSeconds(Number(val) / 1e9);
+            return `${formatNumber(v)} ${u}`;
+          }
+          return `${formatNumber(Number(val))} nsecond`;
+        }
 
-    // Sort rows immutably for rendering
-    const {sortKey, sortDescending, launchIndexBySliceId} = state;
-    const sortedRows = (() => {
-      if (!sortKey) return rows;
-      const copy = rows.slice();
-      copy.sort((a, b) =>
-        compare(a, b, sortKey, sortDescending, launchIndexBySliceId),
-      );
-      return copy;
-    })();
+        const text = Number.isFinite(Number(val))
+          ? String(formatNumber(Number(val)))
+          : String(val);
+        return unit ? `${text} ${unit}` : text;
+      };
 
-    // Cycle sort direction on header click
-    const onSort = (key: SortKey) => {
-      if (state.sortKey === key) {
-        state.sortDescending = !state.sortDescending;
-      } else {
-        state.sortKey = key;
-        state.sortDescending = true;
-      }
-      state.pageOffset = 0;
-    };
+      // Sort rows immutably for rendering
+      const sortedRows = (() => {
+        if (!sortKey) return rows;
+        const copy = rows.slice();
+        copy.sort((a, b) =>
+          compare(a, b, sortKey, sortDescending, launchIndexBySliceId),
+        );
+        return copy;
+      })();
 
-    // Up/down arrow indicator for the active sort column
-    const arrowIconFor = (key: SortKey) => {
-      if (state.sortKey !== key) return null;
-      const icon = state.sortDescending ? 'expand_more' : 'expand_less';
+      // Cycle sort direction on header click
+      const onSort = (key: SortKey) => {
+        if (sortKey === key) {
+          sortDescending = !sortDescending;
+        } else {
+          sortKey = key;
+          sortDescending = true;
+        }
+        pageOffset = 0;
+      };
+
+      // Up/down arrow indicator for the active sort column
+      const arrowIconFor = (key: SortKey) => {
+        if (sortKey !== key) return null;
+        const icon = sortDescending ? 'expand_more' : 'expand_less';
+        return m(
+          'i',
+          {class: 'pf-icon pf-left-icon', style: 'margin-left:6px;'},
+          icon,
+        );
+      };
+
+      const headerCell = (text: string, key: SortKey) =>
+        m(
+          'th.pf-gpu-compute__summary-th',
+          {
+            onclick: () => onSort(key),
+            title: 'Sort',
+          },
+          [text, arrowIconFor(key)],
+        );
+
       return m(
-        'i',
-        {class: 'pf-icon pf-left-icon', style: 'margin-left:6px;'},
-        icon,
-      );
-    };
-
-    const headerCell = (text: string, key: SortKey) =>
-      m(
-        'th.pf-gpu-compute__summary-th',
-        {
-          onclick: () => onSort(key),
-          title: 'Sort',
-        },
-        [text, arrowIconFor(key)],
-      );
-
-    return m(
-      '.pf-gpu-compute',
-      m('table.pf-gpu-compute__summary-table', [
-        m('caption.pf-gpu-compute__summary-caption', [
-          m('.pf-gpu-compute__summary-caption-row', [
-            m(Icon, {
-              icon: Icons.Help,
-              title: 'About this table',
-              style: 'font-size:16px;',
-            }),
-            m(
-              'span',
-              'This table shows all results in the report. Use the column headers to sort the results in this report. Double-Click a result to see detailed metrics.',
-            ),
+        '.pf-gpu-compute',
+        m('table.pf-gpu-compute__summary-table', [
+          m('caption.pf-gpu-compute__summary-caption', [
+            m('.pf-gpu-compute__summary-caption-row', [
+              m(Icon, {
+                icon: Icons.Help,
+                title: 'About this table',
+                style: 'font-size:16px;',
+              }),
+              m(
+                'span',
+                'This table shows all results in the report. Use the column headers to sort the results in this report. Double-Click a result to see detailed metrics.',
+              ),
+            ]),
           ]),
-        ]),
 
-        m('colgroup', [
-          m('col', {style: 'width:5%'}),
-          m('col', {style: 'width:25%'}),
-          m('col', {style: 'width:14%'}),
-          m('col', {style: 'width:14%'}),
-          m('col', {style: 'width:14%'}),
-          m('col', {style: 'width:14%'}),
-          m('col', {style: 'width:14%'}),
-        ]),
-
-        m(
-          'thead',
-          m('tr.pf-gpu-compute__summary-thead-row', [
-            headerCell('ID', 'id'),
-            headerCell('Demangled Name', 'name'),
-            headerCell('Duration', 'duration'),
-            headerCell('Compute Throughput', 'compute'),
-            headerCell('Memory Throughput', 'memory'),
-            headerCell('# Registers', 'registers'),
-            headerCell(`${terminology.grid.title} Size`, 'grid_size'),
+          m('colgroup', [
+            m('col', {style: 'width:5%'}),
+            m('col', {style: 'width:25%'}),
+            m('col', {style: 'width:14%'}),
+            m('col', {style: 'width:14%'}),
+            m('col', {style: 'width:14%'}),
+            m('col', {style: 'width:14%'}),
+            m('col', {style: 'width:14%'}),
           ]),
-        ),
 
-        m(
-          'tbody',
-          sortedRows
-            .slice(state.pageOffset, state.pageOffset + PAGE_SIZE)
-            .map((r) =>
+          m(
+            'thead',
+            m('tr.pf-gpu-compute__summary-thead-row', [
+              headerCell('ID', 'id'),
+              headerCell('Demangled Name', 'name'),
+              headerCell('Duration', 'duration'),
+              headerCell('Compute Throughput', 'compute'),
+              headerCell('Memory Throughput', 'memory'),
+              headerCell('# Registers', 'registers'),
+              headerCell(`${terminology.grid.title} Size`, 'grid_size'),
+            ]),
+          ),
+
+          m(
+            'tbody',
+            sortedRows.slice(pageOffset, pageOffset + PAGE_SIZE).map((r) =>
               m(
                 'tr.pf-gpu-compute__summary-row',
                 {
@@ -434,72 +412,73 @@
                   m(
                     'td.pf-gpu-compute__summary-td',
                     renderRelPercentBar(
-                      Number(r.durationNSecNum),
-                      state.maxDurationNSec,
-                      label(Number(r.durationNSecNum), 'nsecond'),
+                      r.durationNSecNum,
+                      maxDurationNSec,
+                      label(r.durationNSecNum, 'nsecond'),
                     ),
                   ),
                   m(
                     'td.pf-gpu-compute__summary-td',
                     renderRelPercentBar(
-                      Number(r.computePct),
-                      state.maxComputePct,
+                      r.computePct,
+                      maxComputePct,
                       label(r.computePct),
                     ),
                   ),
                   m(
                     'td.pf-gpu-compute__summary-td',
                     renderRelPercentBar(
-                      Number(r.memoryPct),
-                      state.maxMemoryPct,
+                      r.memoryPct,
+                      maxMemoryPct,
                       label(r.memoryPct),
                     ),
                   ),
                   m(
                     'td.pf-gpu-compute__summary-td',
                     renderRelPercentBar(
-                      Number(r.registersPerThread),
-                      state.maxRegisters,
+                      r.registersPerThread,
+                      maxRegisters,
                       label(r.registersPerThread),
                     ),
                   ),
                   m(
                     'td.pf-gpu-compute__summary-td',
                     renderRelPercentBar(
-                      Number(r.gridSize),
-                      state.maxGridSize,
+                      r.gridSize,
+                      maxGridSize,
                       label(r.gridSize),
                     ),
                   ),
                 ],
               ),
             ),
-        ),
-      ]),
-      sortedRows.length > PAGE_SIZE &&
-        m('.pf-gpu-compute__summary-pagination', [
-          m(Button, {
-            icon: Icons.PrevPage,
-            disabled: state.pageOffset === 0,
-            onclick: () => {
-              state.pageOffset = Math.max(0, state.pageOffset - PAGE_SIZE);
-            },
-          }),
-          m(
-            'span',
-            `${state.pageOffset + 1}–${Math.min(state.pageOffset + PAGE_SIZE, sortedRows.length)} of ${sortedRows.length}`,
           ),
-          m(Button, {
-            icon: Icons.NextPage,
-            disabled: state.pageOffset + PAGE_SIZE >= sortedRows.length,
-            onclick: () => {
-              state.pageOffset = Math.min(
-                state.pageOffset + PAGE_SIZE,
-                sortedRows.length - PAGE_SIZE,
-              );
-            },
-          }),
         ]),
-    );
-  },
-};
+        sortedRows.length > PAGE_SIZE &&
+          m('.pf-gpu-compute__summary-pagination', [
+            m(Button, {
+              icon: Icons.PrevPage,
+              disabled: pageOffset === 0,
+              onclick: () => {
+                pageOffset = Math.max(0, pageOffset - PAGE_SIZE);
+              },
+            }),
+            m(
+              'span',
+              `${pageOffset + 1}–${Math.min(pageOffset + PAGE_SIZE, sortedRows.length)} of ${sortedRows.length}`,
+            ),
+            m(Button, {
+              icon: Icons.NextPage,
+              disabled: pageOffset + PAGE_SIZE >= sortedRows.length,
+              onclick: () => {
+                pageOffset = Math.min(
+                  pageOffset + PAGE_SIZE,
+                  sortedRows.length - PAGE_SIZE,
+                );
+              },
+            }),
+          ]),
+      );
+    },
+  };
+}
diff --git a/ui/src/plugins/com.meta.GpuCompute/toolbar.ts b/ui/src/plugins/com.meta.GpuCompute/toolbar.ts
index 049c6a5..8cd22a1 100644
--- a/ui/src/plugins/com.meta.GpuCompute/toolbar.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/toolbar.ts
@@ -27,6 +27,7 @@
 import {Switch} from '../../widgets/switch';
 import type {GpuComputeContext} from './index';
 import {Select} from '../../widgets/select';
+import {assertIsInstance} from '../../base/assert';
 
 // Memoized kernel selector that skips vdom diffing when the options
 // list and selected value haven't changed. Without this, mithril
@@ -34,7 +35,7 @@
 interface KernelSelectAttrs {
   readonly options: readonly KernelLaunchOption[];
   readonly value: string;
-  onChange: (id: number | undefined) => void;
+  onChange(id: number): void;
 }
 
 class KernelSelect implements m.ClassComponent<KernelSelectAttrs> {
@@ -54,12 +55,14 @@
         value: attrs.value,
         className: 'pf-gpu-compute__toolbar-kernel-select',
         onchange: (e: Event) => {
-          const v = (e.target as HTMLSelectElement).value;
-          attrs.onChange(v === '' ? undefined : Number(v));
+          assertIsInstance(e.currentTarget, HTMLSelectElement);
+          const rawValue = e.currentTarget.value;
+          const parsedValue = parseInt(rawValue);
+          attrs.onChange(parsedValue);
         },
       },
       attrs.options.map((o, i) =>
-        m('option', {value: String(o.id)}, `${i} - ${trunc(o.label)}`),
+        m('option', {value: o.id}, `${i} - ${trunc(o.label)}`),
       ),
     );
   }
@@ -116,11 +119,11 @@
 export function renderToolbar(opts: {
   ctx: GpuComputeContext;
   // Kernel launch entries for the Results dropdown.
-  options: KernelLaunchOption[];
+  options: readonly KernelLaunchOption[];
   // Currently selected kernel slice ID.
   sliceId?: number;
   // Called when the user picks a different kernel.
-  onChange: (id: number | undefined, suppressAutoDetails?: boolean) => void;
+  onChange: (id: number, suppressAutoDetails?: boolean) => void;
   // Metric summary for the selected kernel.
   toolbarInfo?: ToolbarInfo;
   // Slice ID of the baseline kernel (if active).
@@ -131,10 +134,6 @@
   baselineEnabled?: boolean;
   // Toggle baseline mode on/off.
   onToggleBaseline?: (enabled: boolean) => void;
-  // Called when the humanize-metrics toggle changes.
-  onHumanizeChanged?: () => void;
-  // Called when the terminology dropdown changes.
-  onTerminologyChanged?: () => void;
 }): m.Children {
   const ctx = opts.ctx;
   const firstId = opts.options[0]?.id;
@@ -277,7 +276,6 @@
               checked: ctx.humanizeMetrics,
               onchange: (e: Event) => {
                 ctx.humanizeMetrics = (e.target as HTMLInputElement).checked;
-                opts.onHumanizeChanged?.();
               },
             }),
           ),
@@ -292,7 +290,6 @@
                 value: ctx.terminologyId,
                 onchange: (e: Event) => {
                   ctx.terminologyId = (e.target as HTMLSelectElement).value;
-                  opts.onTerminologyChanged?.();
                 },
                 className: 'pf-gpu-compute__toolbar-terminology-select',
               },