blob: 049c6a5facadff33da1d2fd05fb4d15f5c5cd830 [file]
// Copyright (C) 2026 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Toolbar and tab-strip for the GPU Compute tab.
//
// Renders the kernel-selection dropdown, current/baseline metric cards,
// the Details/Summary/Analysis tab strip, and a View popup with
// unit-humanization and terminology toggles.
import m from 'mithril';
import type {KernelLaunchOption, ToolbarInfo} from './details';
import {Popup, PopupPosition} from '../../widgets/popup';
import {Button, ButtonVariant} from '../../widgets/button';
import {Card} from '../../widgets/card';
import {Icons} from '../../base/semantic_icons';
import {Switch} from '../../widgets/switch';
import type {GpuComputeContext} from './index';
import {Select} from '../../widgets/select';
// Memoized kernel selector that skips vdom diffing when the options
// list and selected value haven't changed. Without this, mithril
// rebuilds and diffs thousands of <option> vnodes on every redraw.
interface KernelSelectAttrs {
readonly options: readonly KernelLaunchOption[];
readonly value: string;
onChange: (id: number | undefined) => void;
}
class KernelSelect implements m.ClassComponent<KernelSelectAttrs> {
onbeforeupdate(
{attrs}: m.CVnode<KernelSelectAttrs>,
old: m.CVnode<KernelSelectAttrs>,
): boolean {
return (
attrs.value !== old.attrs.value || attrs.options !== old.attrs.options
);
}
view({attrs}: m.CVnode<KernelSelectAttrs>): m.Children {
return m(
Select,
{
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));
},
},
attrs.options.map((o, i) =>
m('option', {value: String(o.id)}, `${i} - ${trunc(o.label)}`),
),
);
}
}
// Maximum label length before truncation.
const MAX_LABEL_LENGTH = 50;
// Truncates a string to `n` characters, appending '...' when shortened.
function trunc(s: string, n = MAX_LABEL_LENGTH): string {
return s.length > n ? s.slice(0, n - 3) + '...' : s;
}
interface TabOption {
readonly key: string;
readonly title: string;
}
interface TabStripAttrs {
readonly className?: string;
readonly tabs: ReadonlyArray<TabOption>;
readonly currentTabKey: string;
onTabChange(key: string): void;
}
class TabStrip implements m.ClassComponent<TabStripAttrs> {
view({attrs}: m.CVnode<TabStripAttrs>) {
const {tabs, currentTabKey, onTabChange, className} = attrs;
return m(
'.pf-gpu-compute__toolbar-tab-strip',
{className},
m(
'.pf-gpu-compute__toolbar-tab-strip__tabs',
tabs.map((tab) => {
const {key, title} = tab;
return m(
'.pf-gpu-compute__toolbar-tab-strip__tab',
{
active: currentTabKey === key,
key,
onclick: () => {
onTabChange(key);
},
},
m('span.pf-gpu-compute__toolbar-tab-strip__tab-title', title),
);
}),
),
);
}
}
// Renders the full toolbar (kernel card + tab strip + view popup).
export function renderToolbar(opts: {
ctx: GpuComputeContext;
// Kernel launch entries for the Results dropdown.
options: KernelLaunchOption[];
// Currently selected kernel slice ID.
sliceId?: number;
// Called when the user picks a different kernel.
onChange: (id: number | undefined, suppressAutoDetails?: boolean) => void;
// Metric summary for the selected kernel.
toolbarInfo?: ToolbarInfo;
// Slice ID of the baseline kernel (if active).
baselineId?: number;
// Metric summary for the baseline kernel.
baselineInfo?: ToolbarInfo;
// Whether baseline comparison is enabled.
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;
const value = (opts.sliceId ?? firstId ?? '').toString();
// Resolve baseline label from options list
const baselineLabel = (() => {
if (!opts.baselineEnabled || opts.baselineId == null) {
return '—';
}
const i = opts.options.findIndex((o) => o.id === opts.baselineId);
if (i < 0) return '—';
return `${i} - ${trunc(opts.options[i].label)}`;
})();
// Available terminology providers for the View popup dropdown
const terminologyOptions = ctx.terminologyRegistry.getOptions();
return [
m(
Card,
{
className: 'pf-gpu-compute__toolbar-card',
},
[
// Row 0: Headers
m('div'),
m('h1.pf-gpu-compute__toolbar-header', 'Result'),
m('h1.pf-gpu-compute__toolbar-header', 'Size'),
m('h1.pf-gpu-compute__toolbar-header', 'Time'),
m('h1.pf-gpu-compute__toolbar-header', 'Cycles'),
m('h1.pf-gpu-compute__toolbar-header', 'Arch'),
m(
'h1.pf-gpu-compute__toolbar-header',
`${ctx.terminologyRegistry.get(ctx.terminologyId).sm.title} Frequency`,
),
m('h1.pf-gpu-compute__toolbar-header', 'Process'),
// Row 1: Current kernel — includes the Results dropdown selector
m('div.pf-gpu-compute__toolbar-row-label', [
m(
'span.pf-gpu-compute__toolbar-swatch.pf-gpu-compute__toolbar-swatch--current',
),
m('span', 'Current'),
]),
m(KernelSelect, {
options: opts.options,
value,
onChange: opts.onChange,
}),
m(
'span.pf-gpu-compute__toolbar-size',
opts.toolbarInfo?.sizeText ?? '—',
),
m('span', opts.toolbarInfo?.timeText ?? '—'),
m('span', opts.toolbarInfo?.cyclesText ?? '—'),
m('span', opts.toolbarInfo?.archText ?? '—'),
m('span', opts.toolbarInfo?.smFrequencyText ?? '—'),
m('span', opts.toolbarInfo?.processText ?? '—'),
// Row 2: Baseline kernel row (only rendered when enabled)
...(opts.baselineEnabled
? [
m('div.pf-gpu-compute__toolbar-row-label', [
m(
'span.pf-gpu-compute__toolbar-swatch.pf-gpu-compute__toolbar-swatch--baseline',
),
m('span', 'Baseline'),
]),
m('div.pf-gpu-compute__toolbar-baseline-value', baselineLabel),
m(
'span.pf-gpu-compute__toolbar-size',
opts.baselineInfo?.sizeText ?? '—',
),
m('span', opts.baselineInfo?.timeText ?? '—'),
m('span', opts.baselineInfo?.cyclesText ?? '—'),
m('span', opts.baselineInfo?.archText ?? '—'),
m('span', opts.baselineInfo?.smFrequencyText ?? '—'),
m('span', opts.baselineInfo?.processText ?? '—'),
]
: []),
],
),
// Secondary toolbar: tab strip + baseline toggle + View popup
m('div.pf-gpu-compute__toolbar-secondary', [
// Tabs
m(TabStrip, {
tabs: [
{key: 'summary', title: 'Summary'},
{key: 'details', title: 'Details'},
...(ctx.analysisProviderHolder.isAvailable()
? [{key: 'analysis', title: 'Analysis'}]
: []),
],
currentTabKey: ctx.activeInfoTab,
onTabChange: (key: string) => {
ctx.activeInfoTab = key as InfoTab;
},
}),
m(Button, {
className: 'pf-gpu-compute__toolbar-btn',
icon: Icons.Change,
label: opts.baselineEnabled ? 'Clear Baseline' : 'Add Baseline',
variant: ButtonVariant.Outlined,
onclick: () => {
// Toggles baseline mode: when enabled, the caller will treat the current selection as baseline
const enable = !opts.baselineEnabled;
opts.onToggleBaseline?.(enable);
// Auto focusing to 'Details' tab
if (enable) {
ctx.activeInfoTab = 'details';
}
},
}),
m(
Popup,
{
position: PopupPosition.BottomStart,
offset: 4,
matchWidth: false,
fitContent: true,
trigger: m(Button, {
icon: 'visibility',
label: 'View',
className: 'pf-gpu-compute__toolbar-btn',
variant: ButtonVariant.Outlined,
}),
},
[
// Auto-Convert Metric Units
m(
'div.pf-gpu-compute__toolbar-popup-row',
m('div', 'Auto-Convert Metric Units'),
m(Switch, {
checked: ctx.humanizeMetrics,
onchange: (e: Event) => {
ctx.humanizeMetrics = (e.target as HTMLInputElement).checked;
opts.onHumanizeChanged?.();
},
}),
),
// Terminology
m('div.pf-gpu-compute__toolbar-popup-spacer'),
m(
'div.pf-gpu-compute__toolbar-popup-row',
m('div', 'Terminology'),
m(
'select.pf-select',
{
value: ctx.terminologyId,
onchange: (e: Event) => {
ctx.terminologyId = (e.target as HTMLSelectElement).value;
opts.onTerminologyChanged?.();
},
className: 'pf-gpu-compute__toolbar-terminology-select',
},
terminologyOptions.map((opt) =>
m('option', {value: opt.id}, opt.name),
),
),
),
],
),
]),
// Toolbar divider
m('hr.pf-gpu-compute__toolbar-divider'),
];
}
// Available sub-tabs in the GPU Compute tab.
export type InfoTab = 'details' | 'summary' | 'analysis';