blob: b6ec1b0feb98748b919e3b6288e934d219c2dd32 [file]
// Copyright (C) 2025 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 {v4} from 'uuid';
import {assertTrue} from '../../base/assert';
import type {App} from '../../public/app';
import {Button, type ButtonAttrs} from '../../widgets/button';
import {Checkbox} from '../../widgets/checkbox';
import {Intent} from '../../widgets/common';
import {
type MultiSelectDiff,
type MultiSelectOption,
PopupMultiSelect,
} from '../../widgets/multiselect';
import {Popup, type PopupAttrs, PopupPosition} from '../../widgets/popup';
import {Select} from '../../widgets/select';
import {Tabs} from '../../widgets/tabs';
import {TextInput} from '../../widgets/text_input';
import {Tree, TreeNode} from '../../widgets/tree';
import {Slider} from '../dev.perfetto.RecordTraceV2/pages/widgets/slider';
import {Toggle} from '../dev.perfetto.RecordTraceV2/pages/widgets/toggle';
import {
type ArmTelemetryCpuSpec,
type ArmTelemetryPmuMetric,
getCpuId,
} from './arm_telemetry_spec';
import {parseArmTelemetrySpec} from './arm_telemetry_spec_manager_impl';
import type {
ArmTelemetrySpecChange,
ArmTelemetrySpecManager,
} from './arm_telemetry_spec_manager';
import type {
ProbeSetting,
RecordProbe,
RecordSubpage,
} from '../dev.perfetto.RecordTraceV2/config/config_interfaces';
import type {TraceConfigBuilder} from '../dev.perfetto.RecordTraceV2/config/trace_config_builder';
import type {RecordingManager} from '../dev.perfetto.RecordTraceV2/recording_manager';
const DEFAULT_SAMPLE_FREQUENCY = 100;
const DEFAULT_SAMPLE_PERIOD = 10000;
export function pmuRecordSection(
recMgr: RecordingManager,
app: App,
specManager: ArmTelemetrySpecManager,
): RecordSubpage {
return {
kind: 'PROBES_PAGE',
id: 'pmu',
title: 'PMU',
subtitle: 'Hardware Telemetry',
icon: 'monitoring',
probes: [tracedPerf(recMgr, app, specManager)],
};
}
type SettingRowAttrs = {
readonly label: string;
readonly description: string;
readonly component: m.ClassComponent;
};
class SettingRow implements m.ClassComponent<SettingRowAttrs> {
view({attrs, children}: m.CVnode<SettingRowAttrs>) {
return m(
'.pf-pmu-selector',
m('header', attrs.label),
m('header.pf-pmu-selector__description', attrs.description),
children,
);
}
}
type CpuId = string;
export interface PerfCpuConfigurationState {
cpuSelected: string | undefined;
pmusSelected: string[];
onCpuSelected: undefined | ((cpuid: CpuId) => void);
cpuLeader: string | undefined;
cpuLeaderUserSelected: boolean;
targetCpus: string | undefined;
cpuPerfType: string | undefined;
sampleByFrequency: boolean;
period: number;
frequency: number;
captureCallstack: boolean;
}
export interface PerfConfigurationState {
currentTabKey: undefined | string;
tabs: {
key: string;
configuration: PerfCpuConfigurationState;
}[];
}
export interface SerializedPerfCpuConfigurationState {
cpuSelected?: string;
pmusSelected: string[];
cpuLeader?: string;
cpuLeaderUserSelected: boolean;
targetCpus?: string;
cpuPerfType?: string;
sampleByFrequency: boolean;
period: number;
frequency: number;
captureCallstack: boolean;
}
export interface SerializedPerfConfigurationState {
currentTabKey?: string;
tabs: {
key: string;
configuration: SerializedPerfCpuConfigurationState;
}[];
}
type MethodologyNode = {
name: string;
children?: MethodologyNode[];
isMetric: boolean;
};
function createDefaultPerfCpuConfigurationState(): PerfCpuConfigurationState {
return {
cpuSelected: undefined,
pmusSelected: [],
onCpuSelected: undefined,
targetCpus: undefined,
cpuPerfType: undefined,
cpuLeader: undefined,
cpuLeaderUserSelected: false,
sampleByFrequency: true,
period: DEFAULT_SAMPLE_PERIOD,
frequency: DEFAULT_SAMPLE_FREQUENCY,
captureCallstack: false,
};
}
function buildCpuList(
specManager: ArmTelemetrySpecManager,
): Map<CpuId, string> {
const res = new Map<CpuId, string>();
specManager.registeredCpuids().forEach((cpuid) => {
const desc = specManager.getCpuDesc(cpuid);
res.set(cpuid, desc.product_configuration.product_name);
});
return res;
}
function getRegisteredCpuDesc(
specManager: ArmTelemetrySpecManager,
cpuid: string,
): ArmTelemetryCpuSpec | undefined {
if (!specManager.registeredCpuids().includes(cpuid)) {
return undefined;
}
return specManager.getCpuDesc(cpuid);
}
function isObject(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function stringValue(value: unknown): string | undefined {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed === '' ? undefined : trimmed;
}
function uniqueStrings(value: unknown): string[] {
if (!Array.isArray(value)) {
return [];
}
return Array.from(
new Set(value.filter((item): item is string => typeof item === 'string')),
);
}
function positiveIntegerOrDefault(
value: unknown,
defaultValue: number,
): number {
if (typeof value === 'number' && Number.isSafeInteger(value) && value > 0) {
return value;
}
return defaultValue;
}
function positiveIntegerString(value: unknown): string | undefined {
const stringified =
typeof value === 'number' ? String(value) : stringValue(value);
if (stringified === undefined) {
return undefined;
}
const numericValue = Number(stringified);
if (Number.isSafeInteger(numericValue) && numericValue > 0) {
return stringified;
}
return undefined;
}
function targetCpusValue(value: unknown): string | undefined {
const cpus = stringValue(value);
if (cpus === undefined) {
return undefined;
}
const [, error] = parseTargetCpus(cpus);
return error === undefined ? cpus : undefined;
}
export function normalizeCpuLeader(
cpuLeader: unknown,
pmusSelected: string[],
keepCurrentLeader: boolean,
): string | undefined {
const leader = stringValue(cpuLeader);
if (
keepCurrentLeader &&
leader !== undefined &&
pmusSelected.includes(leader)
) {
return leader;
}
if (pmusSelected.includes('CPU_CYCLES')) {
return 'CPU_CYCLES';
}
return pmusSelected[0];
}
function deserializePerfCpuConfigurationState(
specManager: ArmTelemetrySpecManager,
value: unknown,
): PerfCpuConfigurationState {
const config = createDefaultPerfCpuConfigurationState();
if (!isObject(value)) {
return config;
}
const cpuSelected = stringValue(value.cpuSelected);
const cpuDesc =
cpuSelected === undefined
? undefined
: getRegisteredCpuDesc(specManager, cpuSelected);
if (cpuDesc !== undefined) {
const availablePmus = new Set(Object.keys(cpuDesc.events));
config.cpuSelected = cpuSelected;
config.pmusSelected = uniqueStrings(value.pmusSelected).filter((pmu) =>
availablePmus.has(pmu),
);
const savedLeader = stringValue(value.cpuLeader);
config.cpuLeader = normalizeCpuLeader(
savedLeader,
config.pmusSelected,
true,
);
config.cpuLeaderUserSelected =
typeof value.cpuLeaderUserSelected === 'boolean'
? value.cpuLeaderUserSelected
: false;
}
config.targetCpus = targetCpusValue(value.targetCpus);
config.cpuPerfType = positiveIntegerString(value.cpuPerfType);
config.sampleByFrequency =
typeof value.sampleByFrequency === 'boolean'
? value.sampleByFrequency
: config.sampleByFrequency;
config.period = positiveIntegerOrDefault(value.period, config.period);
config.frequency = positiveIntegerOrDefault(
value.frequency,
config.frequency,
);
config.captureCallstack =
typeof value.captureCallstack === 'boolean'
? value.captureCallstack
: config.captureCallstack;
return config;
}
export function serializePerfConfigurationState(
state: PerfConfigurationState,
): SerializedPerfConfigurationState {
return {
currentTabKey: state.currentTabKey,
tabs: state.tabs.map((tab) => ({
key: tab.key,
configuration: {
cpuSelected: tab.configuration.cpuSelected,
pmusSelected: [...tab.configuration.pmusSelected],
cpuLeader: tab.configuration.cpuLeader,
cpuLeaderUserSelected: tab.configuration.cpuLeaderUserSelected,
targetCpus: tab.configuration.targetCpus,
cpuPerfType: tab.configuration.cpuPerfType,
sampleByFrequency: tab.configuration.sampleByFrequency,
period: tab.configuration.period,
frequency: tab.configuration.frequency,
captureCallstack: tab.configuration.captureCallstack,
},
})),
};
}
export function deserializePerfConfigurationState(
specManager: ArmTelemetrySpecManager,
state: PerfConfigurationState,
value: unknown,
): void {
state.currentTabKey = undefined;
state.tabs = [];
if (!isObject(value) || !Array.isArray(value.tabs)) {
return;
}
const usedKeys = new Set<string>();
state.tabs = value.tabs.map((tab) => {
const serializedTab = isObject(tab) ? tab : {};
const serializedKey = stringValue(serializedTab.key);
const key =
serializedKey !== undefined && !usedKeys.has(serializedKey)
? serializedKey
: `tab-perf-${v4()}`;
usedKeys.add(key);
return {
key,
configuration: deserializePerfCpuConfigurationState(
specManager,
serializedTab.configuration,
),
};
});
const currentTabKey = stringValue(value.currentTabKey);
state.currentTabKey =
currentTabKey !== undefined && usedKeys.has(currentTabKey)
? currentTabKey
: state.tabs[0]?.key;
}
function refreshPmuTabsForCpuSpec(
state: PerfConfigurationState,
change: ArmTelemetrySpecChange,
): void {
if (change.kind === 'ADD') {
return;
}
if (change.kind === 'CLEAR') {
state.tabs.forEach((tab) => {
tab.configuration.cpuSelected = undefined;
tab.configuration.pmusSelected = [];
tab.configuration.cpuLeader = undefined;
tab.configuration.cpuLeaderUserSelected = false;
});
return;
}
const cpuId = getCpuId(change.desc);
state.tabs.forEach((tab) => {
if (
tab.configuration.cpuSelected === cpuId &&
tab.configuration.onCpuSelected
) {
tab.configuration.onCpuSelected(cpuId);
}
});
}
class PerfCpuConfigurationController {
state: PerfCpuConfigurationState | undefined = undefined;
pmuOptions: MultiSelectOption[] = [];
metricOptions: MultiSelectOption[] = [];
topDownTree: MethodologyNode | undefined = undefined;
availableMetrics: {[keyof: string]: ArmTelemetryPmuMetric} = {};
app?: App;
specManager?: ArmTelemetrySpecManager;
initialize(
state: PerfCpuConfigurationState,
app: App,
specManager: ArmTelemetrySpecManager,
) {
this.state = state;
this.app = app;
this.specManager = specManager;
this.state.onCpuSelected = this.onCpuSelectionChange.bind(this);
this.initOptions();
}
get cpus(): Map<CpuId, string> {
return buildCpuList(this.specManager!);
}
onCpuSelectionChange(cpuid: CpuId) {
this.state!.cpuSelected = cpuid;
const cpuEntry = this.cpus.get(cpuid);
if (cpuEntry === undefined) {
return;
}
this.state!.pmusSelected = [];
this.state!.cpuLeader = undefined;
this.state!.cpuLeaderUserSelected = false;
this.initOptions();
}
initOptions() {
const cpu = this.cpuDesc;
if (!cpu) {
this.pmuOptions = [];
this.metricOptions = [];
this.topDownTree = undefined;
this.app?.raf.scheduleFullRedraw();
return;
}
this.pmuOptions = Object.entries(cpu.events).map(([key, _value]) => {
return {
id: key,
name: key,
checked: this.state!.pmusSelected.includes(key),
};
});
this.metricOptions = Object.entries(cpu.metrics).map(([key, value]) => {
return {
id: key,
name: value.title,
checked: value.events.every((eventName) =>
this.state!.pmusSelected.includes(eventName),
),
};
});
const decisionTree = cpu.methodologies.topdown_methodology.decision_tree;
const createNode = (name: string): MethodologyNode => {
const metricNode = decisionTree.metrics.find(
(metric) => metric.name === name,
);
if (metricNode) {
return {
name,
isMetric: Object.keys(cpu.metrics).includes(name),
children: metricNode.next_items.map((childName) =>
createNode(childName),
),
};
}
const metricGroup = Object.entries(cpu.groups.metrics).find(
([key, _value]) => key === name,
);
if (metricGroup) {
return {
name,
isMetric: Object.keys(cpu.metrics).includes(name),
children: metricGroup[1].metrics.map((metric) => createNode(metric)),
};
}
assertTrue(Object.keys(cpu.metrics).includes(name));
return {
name,
isMetric: true,
children: [],
};
};
this.topDownTree = {
name: 'Top-down Methodology',
children: decisionTree.root_nodes.map((metric) => createNode(metric)),
isMetric: false,
};
this.app?.raf.scheduleFullRedraw();
}
get cpuDesc(): ArmTelemetryCpuSpec | undefined {
const state = this.state;
if (state?.cpuSelected === undefined) {
return undefined;
}
return this.specManager!.getCpuDesc(state.cpuSelected);
}
updateOptions(diffs: MultiSelectDiff[], options: MultiSelectOption[]) {
diffs.forEach((diff) => {
const option = options.find((opt) => opt.id === diff.id);
if (option) {
option.checked = diff.checked;
}
});
}
processPmuSelection(diffs: MultiSelectDiff[]) {
const availableMetrics = this.cpuDesc?.metrics;
if (availableMetrics === undefined) {
return;
}
this.updateOptions(diffs, this.pmuOptions);
const completePmuList = new Set<string>(
this.pmuOptions
.filter((option) => option.checked)
.map((option) => option.id),
);
this.metricOptions.forEach((metric) => {
metric.checked = availableMetrics[metric.id].events.every((eventName) =>
completePmuList.has(eventName),
);
});
this.state!.pmusSelected = this.pmuOptions
.filter((option) => option.checked)
.map((option) => option.id);
this.updateCpuLeader();
}
get metricsPmus(): Set<string> {
const pmus = new Set<string>();
this.metricOptions.forEach((metric) => {
if (metric.checked) {
this.cpuDesc?.metrics[metric.id].events.forEach((eventName) =>
pmus.add(eventName),
);
}
});
return pmus;
}
processMetricsSelection(diffs: MultiSelectDiff[]) {
const availableMetrics = this.cpuDesc?.metrics;
if (availableMetrics === undefined) {
return;
}
const oldMetricsPmus = this.metricsPmus;
const pmusSelected = this.pmuOptions
.filter((option) => option.checked)
.map((option) => option.id);
const detachedPmus = pmusSelected.filter((pmu) => !oldMetricsPmus.has(pmu));
this.updateOptions(diffs, this.metricOptions);
const newMetricsPmus = this.metricsPmus;
const completePmuList = new Set([...newMetricsPmus, ...detachedPmus]);
this.metricOptions.forEach((metric) => {
if (!metric.checked) {
metric.checked = availableMetrics[metric.id].events.every((eventName) =>
completePmuList.has(eventName),
);
}
});
this.pmuOptions.forEach((option) => {
option.checked = completePmuList.has(option.id);
});
this.state!.pmusSelected = this.pmuOptions
.filter((option) => option.checked)
.map((option) => option.id);
this.updateCpuLeader();
}
updateCpuLeader() {
const previousLeader = this.state!.cpuLeader;
this.state!.cpuLeader = normalizeCpuLeader(
previousLeader,
this.state!.pmusSelected,
this.state!.cpuLeaderUserSelected,
);
this.state!.cpuLeaderUserSelected =
this.state!.cpuLeaderUserSelected &&
previousLeader !== undefined &&
this.state!.cpuLeader === previousLeader;
}
}
interface PerfCpuConfigurationAttrs {
readonly recMgr: RecordingManager;
readonly state: PerfCpuConfigurationState;
readonly app: App;
readonly specManager: ArmTelemetrySpecManager;
}
class PerfCpuConfiguration implements m.ClassComponent<PerfCpuConfigurationAttrs> {
constructor() {
this.controller = new PerfCpuConfigurationController();
}
private controller: PerfCpuConfigurationController;
oninit({attrs}: m.Vnode<PerfCpuConfigurationAttrs, this>) {
this.controller.initialize(attrs.state, attrs.app, attrs.specManager);
}
view() {
const options = [...this.controller.cpus.entries()];
return m(
'.pf-pmu-tab-content',
m('header', m('h1', 'CPU selection')),
m(
SettingRow,
{
label: 'CPU',
description: 'Select the CPU to record',
} as SettingRowAttrs,
m(
Select,
{
onchange: (e: Event) => {
const el = e.target as HTMLSelectElement;
this.controller.onCpuSelectionChange(el.value);
},
disabled: options.length === 0,
},
m(
'option',
{
value: '',
disabled: true,
hidden: true,
selected: this.controller.state?.cpuSelected === undefined,
},
'Select an Option',
),
...options.map(([cpuid, name]) =>
m(
'option',
{
value: cpuid,
selected: cpuid === this.controller.state?.cpuSelected,
},
name,
),
),
),
),
m(
SettingRow,
{
label: 'Target CPUs',
description: 'Enter a comma-separated list of integers or ranges.',
} as SettingRowAttrs,
m(TextInput, {
id: 'Target CPUs',
type: 'string',
placeholder: 'e.g., 0,2-5,7,9-12',
autofocus: false,
value: this.controller.state!.targetCpus ?? '',
oninput: (e: Event) => {
this.controller.state!.targetCpus = (
e.target as HTMLInputElement
).value.trim();
},
}),
),
m(
SettingRow,
{
label: 'Perf CPU Event type',
description:
'The event type perf_event associate with this CPU model. Retrieve it in /sys/bus/event_source/devices/<cpu-cluster>/type',
} as SettingRowAttrs,
m(TextInput, {
id: 'Perf CPU Event type',
type: 'string',
placeholder: '8',
autofocus: false,
value: this.controller.state!.cpuPerfType ?? '',
oninput: (e: Event) => {
this.controller.state!.cpuPerfType = (
e.target as HTMLInputElement
).value.trim();
},
}),
),
m('header', m('h1', 'PMU selection')),
m(
SettingRow,
{
label: 'Pmu',
description: 'Directly select the PMU you want to record.',
} as SettingRowAttrs,
m(PopupMultiSelect, {
position: PopupPosition.Left,
label: 'PMUs',
showNumSelected: true,
repeatCheckedItemsAtTop: true,
options: this.controller.pmuOptions,
onChange: (diffs: MultiSelectDiff[]) => {
this.controller.processPmuSelection(diffs);
},
}),
),
m(
SettingRow,
{
label: 'Metrics',
description: 'Select the Metric you want to record.',
} as SettingRowAttrs,
m(PopupMultiSelect, {
position: PopupPosition.Left,
label: 'Metrics',
showNumSelected: true,
repeatCheckedItemsAtTop: true,
options: this.controller.metricOptions,
onChange: (diffs: MultiSelectDiff[]) => {
this.controller.processMetricsSelection(diffs);
},
}),
),
this.controller.topDownTree === undefined
? ''
: m(
SettingRow,
{
label: 'Top-down methodology',
description:
'Review the metrics defined by the top down methodology you want to enable.',
} as SettingRowAttrs,
m(
Popup,
{
position: PopupPosition.Left,
trigger: m(Button, {label: 'Methodology tree', compact: false}),
} as PopupAttrs,
m(
Tree,
this.controller.topDownTree.children?.map((child) =>
this.makeTreeNode(child),
),
),
),
),
m('header', m('h1', 'Sampling configuration')),
m(
SettingRow,
{
label: 'Pmu leader',
description: 'Select the PMU which will drive the capture.',
} as SettingRowAttrs,
m(
Select,
{
oninput: (e: Event) => {
if (!e.target) return;
this.controller.state!.cpuLeader = (
e.target as HTMLSelectElement
).value;
this.controller.state!.cpuLeaderUserSelected = true;
},
},
this.controller.state!.pmusSelected.map((pmu) => {
return m('option', {
selected: this.controller.state!.cpuLeader === pmu,
value: pmu,
label: pmu,
key: pmu,
});
}),
),
),
new Toggle({
title: 'Capture callstack',
descr: `If enabled, the callstack is captured alongside the PMU value. At higher sampling frequencies, this may impact performance.`,
default: this.controller.state!.captureCallstack,
onChange: (enabled: boolean) => {
this.controller.state!.captureCallstack = enabled;
},
}).render(),
new Toggle({
title: 'Sample by frequency',
descr: `If this is enabled, the kernel attempts
to sample the leader at the frequency requested.`,
default: this.controller.state!.sampleByFrequency,
onChange: (enabled: boolean) => {
this.controller.state!.sampleByFrequency = enabled;
},
}).render(),
new Slider({
title: 'Period',
description:
'How many occurence of the leader event trigger a sample capture.',
cssClass: `.thin${
this.controller.state!.sampleByFrequency === true ? '.greyed-out' : ''
}`,
values: [
1, 2, 5, 10, 25, 50, 100, 500, 1000, 2000, 5000, 10000, 15000, 20000,
30000, 40000, 50000,
],
unit: 'event',
min: 0,
disabled: this.controller.state!.sampleByFrequency === true,
default: this.controller.state!.period,
onChange: (val: number) => {
this.controller.state!.period = val;
},
}).render(),
new Slider({
title: 'Frequency',
description: 'Frequency at which the leader should be sampled',
cssClass: `.thin${
this.controller.state!.sampleByFrequency === false
? '.greyed-out'
: ''
}`,
values: [
100, 500, 1000, 2000, 5000, 10000, 15000, 20000, 30000, 40000, 50000,
],
unit: 'Hz',
min: 0,
disabled: this.controller.state!.sampleByFrequency === false,
default: this.controller.state!.frequency,
onChange: (val: number) => {
this.controller.state!.frequency = val;
},
}).render(),
);
}
makeTreeNode(node: MethodologyNode): m.Children {
const checked = node.isMetric
? this.controller.metricOptions.find((option) => option.id === node.name)
?.checked
: false;
return m(
TreeNode,
{
left: node.name,
right: node.isMetric
? m(Checkbox, {
checked,
onchange: () => {
this.controller.processMetricsSelection([
{id: node.name, checked: !checked},
]);
},
})
: undefined,
startsCollapsed: true,
},
node.children?.map((child) => this.makeTreeNode(child)),
);
}
}
interface PerfConfigurationAttrs {
readonly recMgr: RecordingManager;
readonly state: PerfConfigurationState;
readonly app: App;
readonly specManager: ArmTelemetrySpecManager;
}
class PerfConfiguration implements m.ClassComponent<PerfConfigurationAttrs> {
private specChangeCallback?: Disposable;
oninit({
attrs: {state, app, specManager},
}: m.Vnode<PerfConfigurationAttrs, this>) {
this.specChangeCallback = specManager.addOnChangeCallback((change) => {
refreshPmuTabsForCpuSpec(state, change);
app.raf.scheduleFullRedraw();
});
}
onremove() {
this.specChangeCallback?.[Symbol.dispose]();
this.specChangeCallback = undefined;
}
view({
attrs: {recMgr, state, app, specManager},
}: m.Vnode<PerfConfigurationAttrs, this>) {
return m(
'div',
m('input.cpu_file[type=file]', {
style: 'display:none',
onchange: (e: Event) => {
if (!(e.target instanceof HTMLInputElement)) {
throw new Error('Not an input element');
}
if (!e.target.files) return;
const file = e.target.files[0];
const reader = new FileReader();
reader.onloadend = () => {
if (reader.error) {
throw reader.error;
}
if (reader.result === null || typeof reader.result !== 'string') {
throw new Error(
'Invalid data present in CPU description file: A JSON object is expected.',
);
}
const desc = parseArmTelemetrySpec(reader.result);
if (desc === undefined) {
throw new Error('Could not parse CPU description file.');
}
specManager.update(desc);
app.raf.scheduleFullRedraw();
};
reader.readAsText(file);
},
}),
m(Button, {
label: 'Load local CPU file',
intent: Intent.Primary,
onclick: (e: Event) => {
e.preventDefault();
const fileElement =
document.querySelector<HTMLInputElement>('input.cpu_file');
if (!fileElement) return;
fileElement.click();
},
} as ButtonAttrs),
(() => {
const addTabButton = m(Button, {
icon: 'add',
className: 'pf-tabs__new-tab-btn',
onclick: () => {
const key = `tab-perf-${v4()}`;
state.tabs.push({
key,
configuration: createDefaultPerfCpuConfigurationState(),
});
state.currentTabKey = key;
},
});
const tabs = state.tabs.map((tab) => ({
key: tab.key,
title: `Tab`,
content: m(PerfCpuConfiguration, {
key: tab.key,
recMgr,
state: tab.configuration,
app,
specManager,
}),
closeButton: true,
}));
return m(Tabs, {
tabs,
activeTabKey: state.currentTabKey,
newTabContent: addTabButton,
onTabChange: (key: string) => {
state.currentTabKey = key;
app.raf.scheduleFullRedraw();
},
onTabClose: (key: string) => {
const closedIndex = state.tabs.findIndex((tab) => tab.key === key);
state.tabs = state.tabs.filter((tab) => tab.key !== key);
if (state.currentTabKey === key) {
const fallbackTab =
state.tabs[closedIndex] ?? state.tabs[state.tabs.length - 1];
state.currentTabKey = fallbackTab?.key;
}
app.raf.scheduleFullRedraw();
},
});
})(),
);
}
}
function tracedPerf(
recMgr: RecordingManager,
app: App,
specManager: ArmTelemetrySpecManager,
): RecordProbe {
const state: PerfConfigurationState = {
currentTabKey: undefined,
tabs: [],
};
const settings = {
testSetting: {
render() {
return m(
'.pf-pmu-record-setting',
m(PerfConfiguration, {
recMgr,
state,
app,
specManager,
}),
);
},
serialize() {
return serializePerfConfigurationState(state);
},
deserialize(serializedState: unknown) {
deserializePerfConfigurationState(specManager, state, serializedState);
},
} as ProbeSetting,
};
return {
id: 'pmu',
title: 'PMU configuration',
description: 'Record hardware counters and apply top-down methodology',
supportedPlatforms: ['ANDROID', 'LINUX'],
settings,
genConfig: function (tc: TraceConfigBuilder) {
state.tabs.forEach((tab, index) => {
const cpuState = tab.configuration;
if (
cpuState.cpuSelected === undefined ||
cpuState.pmusSelected.length === 0 ||
cpuState.cpuLeader === undefined
) {
return;
}
const cpuDesc = specManager.getCpuDesc(cpuState.cpuSelected);
if (cpuDesc === undefined) {
return;
}
if (cpuState.cpuPerfType === undefined) {
alert('PMU Configuration Error: Perf CPU event type is required');
return;
}
const cpuPerfType = Number(cpuState.cpuPerfType);
if (!Number.isSafeInteger(cpuPerfType) || cpuPerfType <= 0) {
alert('PMU Configuration Error: Invalid perf CPU event type');
return;
}
if (
cpuState.sampleByFrequency &&
(!Number.isSafeInteger(cpuState.frequency) || cpuState.frequency <= 0)
) {
alert('PMU Configuration Error: Invalid sampling frequency');
return;
}
if (
!cpuState.sampleByFrequency &&
(!Number.isSafeInteger(cpuState.period) || cpuState.period <= 0)
) {
alert('PMU Configuration Error: Invalid sampling period');
return;
}
let targetCpus: number[] | undefined;
if (cpuState.targetCpus) {
const [cpus, error] = parseTargetCpus(cpuState.targetCpus);
if (error !== undefined) {
alert(`PMU Configuration Error: ${error}`);
return;
}
targetCpus = cpus;
}
tc.addDataSource('linux.system_info');
const config = tc.addDataSource(`linux.perf ${index}`);
config.name = 'linux.perf';
config.perfEventConfig = {};
const perfConf = config.perfEventConfig;
perfConf.timebase = {
name: cpuState.cpuLeader,
rawEvent: {
type: cpuPerfType,
config: Number(cpuDesc.events[cpuState.cpuLeader].code),
},
};
perfConf.followers = cpuState.pmusSelected
.filter((pmu) => pmu !== cpuState.cpuLeader)
.map((pmu) => {
return {
name: pmu,
rawEvent: {
type: cpuPerfType,
config: Number(cpuDesc.events[pmu].code),
},
};
});
if (targetCpus !== undefined) {
perfConf.targetCpu = targetCpus;
} else if (cpuState.cpuSelected) {
perfConf.cpuid = [getCpuId(cpuDesc).replace('0x', '')];
}
if (cpuState.sampleByFrequency) {
perfConf.timebase.frequency = cpuState.frequency;
} else {
perfConf.timebase.period = cpuState.period;
}
if (cpuState.captureCallstack) {
perfConf.callstackSampling = {};
}
});
},
};
}
function parseCpuNumber(cpu: string): number | undefined {
if (!/^\d+$/.test(cpu)) {
return undefined;
}
const value = Number(cpu);
return Number.isSafeInteger(value) ? value : undefined;
}
function parseTargetCpus(cpus: string | undefined): [number[], string?] {
const cpuSet = new Set<number>();
if (cpus === undefined) {
return [[], 'Undefined cpu input string'];
}
const parts = cpus.split(',');
for (const partRaw of parts) {
const part = partRaw.trim();
if (part === '') {
return [[], 'Invalid target CPU value'];
}
if (part.includes('-')) {
const rangeParts = part.split('-');
if (rangeParts.length !== 2) {
return [[], `Invalid CPU range: ${part}`];
}
const [startStrRaw, endStrRaw] = rangeParts;
const startStr = startStrRaw.trim();
const endStr = endStrRaw.trim();
const start = parseCpuNumber(startStr);
const end = parseCpuNumber(endStr);
if (start === undefined || end === undefined || start > end) {
return [[], `Invalid CPU range: ${part}`];
}
for (let i = start; i <= end; i++) {
cpuSet.add(i);
}
} else {
const val = parseCpuNumber(part);
if (val === undefined) {
return [[], `Invalid target CPU value: ${part}`];
}
cpuSet.add(val);
}
}
return [Array.from(cpuSet)];
}