blob: 4bbc3a803409bdf3a1c8604f85cf4c1884fb5154 [file] [edit]
// 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.
import './styles.scss';
import m from 'mithril';
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 type {TrackEventSelection} from '../../public/selection';
import {renderToolbar} from './toolbar';
import type {InfoTab} from './toolbar';
import type {
KernelLaunchOption,
KernelMetricData,
ToolbarInfo,
} from './details';
import {KernelSummarySection, fetchKernelSummaryRows} from './summary';
import type {SummaryRow} from './summary';
import {registerSpeedOfLightSection} from './section/speed_of_light';
import {registerLaunchStatisticsSection} from './section/launch_statistics';
import {registerOccupancySection} from './section/occupancy';
import {registerWorkloadAnalysisSection} from './section/workload_analysis';
import {cudaTerminology} from './terminology/cuda';
import {openclTerminology} from './terminology/opencl';
import {TerminologyRegistry} from './terminology';
import {SectionRegistry} from './section';
import {
type PerformanceAnalysisResult,
type AnalysisCache,
type AnalysisProvider,
AnalysisProviderHolder,
} from './analysis';
import {SerialTaskQueue, QuerySlot} from '../../base/query_slot';
export interface GpuComputeContext {
humanizeMetrics: boolean;
activeInfoTab: InfoTab;
terminologyId: string;
readonly terminologyRegistry: TerminologyRegistry;
readonly sectionRegistry: SectionRegistry;
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>();
// 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 SerialTaskQueue();
private readonly selectionSlot = new QuerySlot<{
hasMetrics: boolean;
toolbar?: ToolbarInfo;
}>(this.taskQueue);
private hadSelection = false;
private appliedSelectionSliceId: number | undefined;
private baselineSliceId: number | undefined = undefined;
private baselineToolbarInfo?: ToolbarInfo;
private baselineData?: KernelMetricData;
// Cache for storing analysis results by sliceId
private readonly kernelAnalysisCache = new Map<
number,
PerformanceAnalysisResult
>();
// Cache for storing section analysis results by sliceId and section name
private readonly sectionAnalysisCache = new Map<
string,
PerformanceAnalysisResult
>();
// Helper to create cache key for section analysis
private getSectionCacheKey(sliceId: number, sectionName: string): string {
return `${sliceId}:${sectionName}`;
}
// Analysis cache interface passed to the KernelAnalysisSection component
private readonly analysisCache: AnalysisCache = {
getKernelAnalysis: (sliceId: number) =>
this.kernelAnalysisCache.get(sliceId),
setKernelAnalysis: (sliceId: number, result: PerformanceAnalysisResult) => {
this.kernelAnalysisCache.set(sliceId, result);
},
getSectionAnalysis: (sliceId: number, sectionName: string) =>
this.sectionAnalysisCache.get(
this.getSectionCacheKey(sliceId, sectionName),
),
setSectionAnalysis: (
sliceId: number,
sectionName: string,
result: PerformanceAnalysisResult,
) => {
this.sectionAnalysisCache.set(
this.getSectionCacheKey(sliceId, sectionName),
result,
);
},
};
public 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;
}
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';
}
} catch (e) {
console.warn('GpuCompute: failed to fetch toolbar metrics:', e);
this.setToolbarInfo(undefined);
}
}
private async setBaselineId(useCurrent: boolean) {
// If not using current, clear baseline state
if (!useCurrent) {
this.baselineSliceId = undefined;
this.baselineToolbarInfo = undefined;
this.baselineData = 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;
}
}
private async refreshBaseline() {
if (this.baselineSliceId == null) 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;
}
}
// 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);
}
// 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();
}
}
// 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 {
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;
this.ctx.activeInfoTab = 'details';
this.trace.tabs.showTab(this.tabUri);
}
const result = this.selectionSlot.use({
key: {sliceId: id},
queryFn: 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]);
}
}
// Plugin entry point, registers the tab and menu command
export default class GpuComputePlugin implements PerfettoPlugin {
static readonly id = 'com.meta.GpuCompute';
static readonly description =
'Analyzes GPU compute kernel performance using hardware counters ' +
'and launch parameters found in the trace.';
private readonly terminologyRegistry = new TerminologyRegistry();
private readonly sectionRegistry = new SectionRegistry();
private readonly analysisProviderHolder = new AnalysisProviderHolder();
constructor() {
this.terminologyRegistry.register('cuda', cudaTerminology);
this.terminologyRegistry.register('opencl', openclTerminology);
registerSpeedOfLightSection(this.sectionRegistry);
registerLaunchStatisticsSection(this.sectionRegistry);
registerOccupancySection(this.sectionRegistry);
registerWorkloadAnalysisSection(this.sectionRegistry);
}
registerAnalysisProvider(provider: AnalysisProvider): void {
this.analysisProviderHolder.register(provider);
}
getSectionRegistry(): SectionRegistry {
return this.sectionRegistry;
}
getContext(): GpuComputeContext {
return {
humanizeMetrics: true,
activeInfoTab: 'details',
terminologyId: 'cuda',
terminologyRegistry: this.terminologyRegistry,
sectionRegistry: this.sectionRegistry,
analysisProviderHolder: this.analysisProviderHolder,
};
}
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(
trace.engine,
trace,
tabUri,
this.terminologyRegistry,
this.sectionRegistry,
this.analysisProviderHolder,
);
trace.tabs.registerTab({
isEphemeral: false,
uri: tabUri,
content: content,
});
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);
}
} catch (e) {
console.warn('GpuCompute: failed to fetch kernel launch list:', e);
content.setOptions([]);
}
}
}