| // 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'; |