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};