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', },