Some more tweaks
diff --git a/docs/AGENTS-ui.md b/docs/AGENTS-ui.md
index 8b0c697..50788dc 100644
--- a/docs/AGENTS-ui.md
+++ b/docs/AGENTS-ui.md
@@ -147,6 +147,52 @@
 - Prefer using the existing widget library (`ui/src/widgets/`) over creating new components.
 - Use `readonly` for attrs properties to prevent accidental mutation. We like things to be immutable.
 
+### Async Data Fetching with AsyncMemo
+
+Use `AsyncMemo` (from `ui/src/base/async_memo.ts`) for async data fetching in synchronous render cycles. It handles caching, deduplication, cancellation, and race-condition prevention automatically.
+
+```typescript
+import {AsyncMemo} from '../../base/async_memo';
+
+class MyComponent implements m.ClassComponent<MyAttrs> {
+  // One memo per piece of async data. Create in the class body.
+  private readonly dataMemo = new AsyncMemo<MyData>();
+
+  view({attrs}: m.CVnode<MyAttrs>): m.Children {
+    // Call `use()` every render cycle with current parameters.
+    const result = this.dataMemo.use({
+      // Key identifies the request. Changes trigger a re-fetch.
+      key: {sliceId: attrs.sliceId, filters: attrs.filters},
+      // Async function that fetches the data.
+      compute: async () => fetchData(attrs.sliceId, attrs.filters),
+      // Optional: show stale data while fetching for these key fields.
+      // E.g., pagination changes show old data immediately; filter
+      // changes show loading state.
+      retainOn: ['pagination'],
+    });
+
+    if (result.data === undefined) {
+      return m('div', 'Loading...');
+    }
+    return renderData(result.data);
+  }
+
+  onremove() {
+    // Cancel pending tasks and cleanup resources.
+    this.dataMemo.dispose();
+  }
+}
+```
+
+**Key concepts:**
+
+- **key**: Object identifying the memo. Changes trigger a re-fetch.
+- **retainOn**: Key fields that allow showing previous data while fetching. Only fields listed here can differ while stale data is retained.
+- **enabled**: Truthy value required before the task runs. Use for dependencies like `enabled: otherMemo.data` to wait for another query to complete.
+- **dispose()**: Call in `onremove()` to cancel pending tasks and prevent orphaned work.
+
+**Prefer AsyncMemo over manual state tracking** for async data in components. It serializes tasks, prevents race conditions, and handles cancellation automatically.
+
 **Conditional Rendering with State Preservation:**
 Use the `Gate` component when you need to conditionally show/hide content while preserving component state:
 ```typescript
@@ -226,6 +272,47 @@
 }
 ```
 
+### Running Queries
+
+Always use the typed query extractor.
+
+```ts
+const iter = result.iter({ id: NUM, ts: LONG, dur: LONG, name: STR_NULL });
+iter.id; // number
+iter.ts; // bigint
+iter.dur; // bigint
+iter.name; // string | null
+```
+
+Types can be found in `ui/src/trace_processor/query_result.ts`.
+
+Avoid pulling out columns by name if you know the fields up front.
+
+```ts
+// !!! AVOID !!!
+iter.get("id") as number;
+iter.get("ts") as bigint;
+iter.get("dur") as bigint;
+iter.get("name") as string | null;
+```
+
+Timestamps and durations have special tagged types — e.g. `time` and `duration`.
+They also have helper classes loaded with static functions `Time` and
+`Duration`, see `ui/src/base/time.ts`.
+
+Use the dedicated `.fromRaw()` converters to convert from bigints from queries
+to times and duration types instead of type assertions.
+
+```ts
+// DO THIS!
+Time.fromRaw(iter.ts);
+Duration.fromRaw(iter.dur);
+
+// !!! AVOID !!!
+iter.ts as time;
+iter.dur as duration;
+```
+
 ## Track creation
 
 Rarely you need to create a new Track from scratch.
@@ -276,6 +363,23 @@
 function process(items: readonly string[]): void { ... }
 ```
 
+**Avoid `!` non-null assertions by binding to local variables before closures:**
+TypeScript doesn't carry control-flow narrowing across closure boundaries. Instead of using `!`, extract the value to a local `const` in the guarded scope:
+```typescript
+// Bad - TypeScript can't narrow `this` properties or object fields across closures
+if (this.baselineKernelId === undefined) return;
+const result = memo.use({
+  compute: async () => fetch(this.baselineKernelId!), // needs `!`
+});
+
+// Good - local const is narrowed by the guard and visible inside the closure
+if (this.baselineKernelId === undefined) return;
+const baselineKernelId = this.baselineKernelId;
+const result = memo.use({
+  compute: async () => fetch(baselineKernelId), // no `!` needed
+});
+```
+
 **Use `classNames()` utility for building CSS class strings:**
 ```typescript
 import {classNames} from '../base/classnames';
diff --git a/ui/src/plugins/com.meta.GpuCompute/analysis.ts b/ui/src/plugins/com.meta.GpuCompute/analysis.ts
index 52d6912..223d0cd 100644
--- a/ui/src/plugins/com.meta.GpuCompute/analysis.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/analysis.ts
@@ -29,15 +29,15 @@
 // Result of a performance analysis.
 export interface PerformanceAnalysisResult {
   // The section that was analyzed
-  sectionName: string;
+  readonly sectionName: string;
   // The generated analysis
-  analysis: string;
+  readonly analysis: string;
   // Whether the analysis completed successfully
-  success: boolean;
+  readonly success: boolean;
   // Error message if analysis failed
-  error?: string;
+  readonly error?: string;
   // The name of the provider used to generate the analysis
-  providerName?: string;
+  readonly providerName?: string;
 }
 
 // Interface for analysis cache operations passed from parent component.
diff --git a/ui/src/plugins/com.meta.GpuCompute/details.ts b/ui/src/plugins/com.meta.GpuCompute/details.ts
index 11d7ee7..9d986c8 100644
--- a/ui/src/plugins/com.meta.GpuCompute/details.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/details.ts
@@ -151,40 +151,40 @@
 
 // A single row of metric data (label + unit + value).
 export type MetricRow = {
-  metric_id: string;
-  metric_label: string;
-  metric_unit: string;
-  metric_value: number | string | null;
+  readonly metric_id: string;
+  readonly metric_label: string;
+  readonly metric_unit: string;
+  readonly metric_value: number | string | null;
 };
 
 // A table within a section (description + rows).
 export type MetricTable = {
-  table_desc: string | null;
-  data: MetricRow[];
+  readonly table_desc: string | null;
+  readonly data: MetricRow[];
 };
 
 // A titled group of metric tables (one per registered Section).
 export type MetricSection = {
-  section: string;
-  tables: MetricTable[];
+  readonly section: string;
+  readonly tables: MetricTable[];
 };
 
 // Summary text shown in the toolbar card for the selected kernel.
 export type ToolbarInfo = {
-  sizeText: string;
-  timeText: string;
-  cyclesText: string;
-  archText: string;
-  smFrequencyText: string;
-  processText: string;
+  readonly sizeText: string;
+  readonly timeText: string;
+  readonly cyclesText: string;
+  readonly archText: string;
+  readonly smFrequencyText: string;
+  readonly processText: string;
 };
 
 // Full metric payload for a single kernel launch.
 export type KernelMetricData = {
-  id: number;
-  kernelName: string;
-  sections: MetricSection[];
-  toolbar?: ToolbarInfo;
+  readonly id: number;
+  readonly kernelName: string;
+  readonly sections: MetricSection[];
+  readonly toolbar?: ToolbarInfo;
 };
 
 // Callback signature for rendering a percent-bar cell.
@@ -298,11 +298,11 @@
 
 // Intermediate grouping of a kernel's launch args and counter metrics.
 export type KernelGroup = {
-  kernelId: number;
-  kernelName: string;
-  launchTs: number;
-  launchDur: number;
-  metricsKV: Record<string, number | string>;
+  readonly kernelId: number;
+  readonly kernelName: string;
+  readonly launchTs: number;
+  readonly launchDur: number;
+  readonly metricsKV: Record<string, number | string>;
 };
 
 // Reduces the SQL result iterator into a `kernelId → KernelGroup` map.
@@ -819,14 +819,15 @@
           attrs.renderKernel(kernel, {engine: attrs.engine})) ??
         (() => {
           const analysisProvider = attrs.ctx.analysisProviderHolder.get();
+          const analysisCache = attrs.analysisCache;
           const renderFooter =
-            attrs.analysisCache && analysisProvider
+            analysisCache && analysisProvider
               ? (sec: MetricSection) =>
                   analysisProvider.renderSectionAnalysis({
                     section: sec,
                     kernelData: kernel,
                     sliceId: kernel.id,
-                    analysisCache: attrs.analysisCache!,
+                    analysisCache,
                   })
               : undefined;
           const sections = attrs.ctx.sectionRegistry.getSections();
diff --git a/ui/src/plugins/com.meta.GpuCompute/humanize.ts b/ui/src/plugins/com.meta.GpuCompute/humanize.ts
index e30d5ba..338d26f 100644
--- a/ui/src/plugins/com.meta.GpuCompute/humanize.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/humanize.ts
@@ -26,16 +26,16 @@
 
 // Shape of a single metric row fed into the humanization pipeline.
 type RowShape = {
-  metric_id: string | null;
-  metric_label: string;
-  metric_unit: string;
-  metric_value: number | string | null;
+  readonly metric_id: string | null;
+  readonly metric_label: string;
+  readonly metric_unit: string;
+  readonly metric_value: number | string | null;
 };
 // Shape of a metric table (description + array of rows).
-type TableShape = {table_desc: string | null; data: RowShape[]};
+type TableShape = {readonly table_desc: string | null; readonly data: readonly RowShape[]};
 
 // Shape of a metric section (title + array of tables).
-type SectionShape = {section: string; tables: TableShape[]};
+type SectionShape = {readonly section: string; readonly tables: readonly TableShape[]};
 
 // Type guard: returns `true` when `v` is a finite number.
 function isNumber(v: unknown): v is number {
diff --git a/ui/src/plugins/com.meta.GpuCompute/index.ts b/ui/src/plugins/com.meta.GpuCompute/index.ts
index 8eeed00..2b3d505 100644
--- a/ui/src/plugins/com.meta.GpuCompute/index.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/index.ts
@@ -23,7 +23,6 @@
   fetchRawKernelMetricGroups,
   buildKernelMetricDataFromGroup,
 } from './details';
-import type {TrackEventSelection} from '../../public/selection';
 import {renderToolbar} from './toolbar';
 import type {InfoTab} from './toolbar';
 import type {
@@ -68,8 +67,8 @@
   // trigger mithril redraws; render() reads the current selection and
   // polls the QuerySlot which handles deduplication, background
   // fetching, and race-condition prevention.
-  private readonly selectionSlot = new AsyncMemo<KernelGroup[]>();
-  private readonly baselineSlot = new AsyncMemo<KernelGroup[]>();
+  private readonly selectionMemo = new AsyncMemo<KernelGroup[]>();
+  private readonly baselineMemo = new AsyncMemo<KernelGroup[]>();
 
   private selectedKernelId?: number;
   private baselineKernelId?: number;
@@ -231,13 +230,13 @@
   }
 
   private getSelectionData(): {
-    toolbar?: ToolbarInfo;
-    data?: KernelMetricData[];
+    readonly toolbar?: ToolbarInfo;
+    readonly data?: KernelMetricData[];
   } {
     const sliceId = this.selectedKernelId;
     if (sliceId === undefined) return {};
 
-    const selectionResult = this.selectionSlot.use({
+    const selectionResult = this.selectionMemo.use({
       key: {sliceId},
       retainOn: ['sliceId'],
       compute: async () => {
@@ -256,21 +255,22 @@
   }
 
   private getBaselineData(): {
-    toolbar?: ToolbarInfo;
-    data?: KernelMetricData;
+    readonly toolbar?: ToolbarInfo;
+    readonly data?: KernelMetricData;
   } {
     if (this.baselineKernelId === undefined) {
       return {};
     }
 
-    const baselineResult = this.baselineSlot.use({
-      key: {sliceId: this.baselineKernelId},
+    const baselineKernelId = this.baselineKernelId;
+    const baselineResult = this.baselineMemo.use({
+      key: {sliceId: baselineKernelId},
       retainOn: ['sliceId'],
       compute: async () => {
         return fetchRawKernelMetricGroups(
           this.ctx,
           this.engine,
-          this.baselineKernelId!,
+          baselineKernelId,
         );
       },
     });
@@ -285,12 +285,12 @@
     };
   }
 
+  // If the current selection has changed, and it looks like the user has
+  // selected a kernel slice id, set the current selected kernel slice id to
+  // this one.
   private maybeSyncTimelineSelection(): void {
     const sel = this.trace.selection.selection;
-    const selSliceId =
-      sel.kind === 'track_event'
-        ? (sel as TrackEventSelection).eventId
-        : undefined;
+    const selSliceId = sel.kind === 'track_event' ? sel.eventId : undefined;
 
     if (
       selSliceId !== undefined &&
diff --git a/ui/src/plugins/com.meta.GpuCompute/terminology/index.ts b/ui/src/plugins/com.meta.GpuCompute/terminology/index.ts
index 836c03e..c47236d 100644
--- a/ui/src/plugins/com.meta.GpuCompute/terminology/index.ts
+++ b/ui/src/plugins/com.meta.GpuCompute/terminology/index.ts
@@ -77,15 +77,15 @@
 export function createTerminology(
   name: string,
   terms: {
-    gpu: Term;
-    thread: Term;
-    warp: Term;
-    block: Term;
-    grid: Term;
-    sm: Term;
-    streamingMultiprocessor: Term;
-    sharedMem: Term;
-    tensor: Term;
+    readonly gpu: Term;
+    readonly thread: Term;
+    readonly warp: Term;
+    readonly block: Term;
+    readonly grid: Term;
+    readonly sm: Term;
+    readonly streamingMultiprocessor: Term;
+    readonly sharedMem: Term;
+    readonly tensor: Term;
   },
 ): Terminology {
   return {name, ...terms};