blob: b2e4bf336416ed674d7fb9ea07f3f6d58a4b89e3 [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.
import type {App} from '../../public/app';
import type {PerfettoPlugin} from '../../public/plugin';
import type {Trace} from '../../public/trace';
import m from 'mithril';
import {NUM, STR} from '../../trace_processor/query_result';
import {exists, getOrCreate} from '../../base/utils';
import StandardGroupsPlugin from '../dev.perfetto.StandardGroups';
import {TrackNode} from '../../public/workspace';
import {COUNTER_TRACK_KIND} from '../../public/track_kinds';
import {sqlNameSafe} from '../../base/string_utils';
import {CounterTrack} from '../../components/tracks/counter_track';
import {uuidv4} from '../../base/uuid';
import {CpuPage} from './cpu_page';
import {ARM_TELEMETRY_CPU_SPEC_SCHEMA} from './arm_telemetry_spec';
import type {ArmTelemetryCpuSpec} from './arm_telemetry_spec';
import {z} from 'zod';
import type {ArmTelemetrySpecManager} from './arm_telemetry_spec_manager';
import {ArmTelemetrySpecManagerImpl} from './arm_telemetry_spec_manager_impl';
type ApplicableMetricDesc = {
name: string;
cpus: number[];
events: string[];
formula: string;
};
const ARM_TELEMETRY_CPU_SPECS_SCHEMA = z.array(ARM_TELEMETRY_CPU_SPEC_SCHEMA);
const SME2_SPEC_CPUID = '0x41d8d';
function eventTableName(eventName: string): string {
return `arm_telemetry_event_${sqlNameSafe(eventName)}`;
}
function metricTableName(metricName: string): string {
return `arm_telemetry_metric_${sqlNameSafe(metricName)}`;
}
export default class ArmTelemetryPlugin implements PerfettoPlugin {
static readonly id = 'com.arm.ArmTelemetry';
static readonly description = `
Computes Arm CPU telemetry metrics from PMU counters in the loaded trace.
CPU specification files define the available events, metrics, and formulas
used to create per-CPU metric tracks.
Use the Arm CPU page to load and manage CPU specification files.
`;
static readonly dependencies = [StandardGroupsPlugin];
private static specManager: ArmTelemetrySpecManager;
static onActivate(app: App): void {
const cpuSpecsSetting = app.settings.register<ArmTelemetryCpuSpec[]>({
id: `${ArmTelemetryPlugin.id}.cpuSpecs`,
name: 'Arm telemetry CPU specs',
description: `CPU telemetry specifications used to compute Arm telemetry metrics.
To edit, go to SUPPORT -> Arm cpu page.`,
schema: ARM_TELEMETRY_CPU_SPECS_SCHEMA,
defaultValue: [],
requiresReload: true,
});
ArmTelemetryPlugin.specManager = new ArmTelemetrySpecManagerImpl(
cpuSpecsSetting,
);
app.pages.registerPage({
route: '/arm_cpu',
render: () =>
m(CpuPage, {
specManager: ArmTelemetryPlugin.specManager,
}),
});
app.sidebar.addMenuItem({
section: 'support',
text: 'Arm cpu',
href: '#!/arm_cpu',
icon: 'memory',
});
}
async onTraceLoad(trace: Trace): Promise<void> {
if (!(await this.checkRequirements(trace))) {
return;
}
// Fetch the perf counters to process them in a way that suits
// metric computation
const countersPerCpu = await this.getPerfCountersNamePerCpu(trace);
// Retrieve the CPUID of each CPU
const cpuidMap = await this.getCpuIdMap(trace);
// Create activity views required by the counters and metrics
await this.createCpuActivityViews(trace);
// Create all the counters view and tables
const counters = [...new Set([...countersPerCpu.values()].flat())];
for (const c of counters) {
await this.createPerfCounterView(trace, c);
}
// Update the metric tracks if possible
await this.updateMetricTracks(trace, cpuidMap, countersPerCpu);
}
// Validate that all the requirements are met to execute the plugin.
// - At least one CPU specification has been uploaded
// - Existence meta data asociated with the CPU table
private async checkRequirements(ctx: Trace): Promise<boolean> {
if (!ArmTelemetryPlugin.specManager.hasSpecs()) {
return false;
}
// We need a CPU table to retrieve metrics and operate
const q = await ctx.engine.query(`
SELECT
cpu,
EXTRACT_ARG(ARG_SET_ID, 'arm_cpu_implementer') as implementer,
EXTRACT_ARG(ARG_SET_ID, 'arm_cpu_part') as part
FROM cpu
WHERE implementer IS NOT NULL and part IS NOT NULL
`);
return q.numRows() !== 0;
}
// Return a map describing the perf counters associated with their CPU.
private async getPerfCountersNamePerCpu(
trace: Trace,
): Promise<Map<number, string[]>> {
const q = await trace.engine.query(
'SELECT name, cpu FROM perf_counter_track',
);
const counters: Map<number, string[]> = new Map();
const it = q.iter({name: STR, cpu: NUM});
for (; it.valid(); it.next()) {
getOrCreate(counters, it.cpu, () => []).push(it.name);
}
return counters;
}
// Create views of the CPU activity (busy or idle).
// - cpu_busy_slice: Slices when the CPU is busy
// - ts: timestamp marking the begining of the slice
// - cpu: cpu of the slice
// - dur: duration of the slice
// - utid: Thread identifier associated with the slice.
// - cpu_idle_slice: Slices when the CPU is idle
// - ts: timestamp marking the begining of the slice
// - cpu: cpu of the slice
// - dur: duration of the slice
// - utid: Thread identifier associated with the slice.
private async createCpuActivityViews(trace: Trace) {
await trace.engine.query(`
CREATE PERFETTO TABLE cpu_busy_slice AS
WITH whole_counter_slice(ts, dur) AS
(VALUES ((SELECT ts FROM counter ORDER BY ts LIMIT 1),
(SELECT ts FROM counter ORDER BY ts DESC LIMIT 1) - (SELECT ts FROM counter ORDER BY ts LIMIT 1))
)
SELECT cpu,ts,dur,utid
FROM sched_slice
WHERE utid != 0
UNION
-- Add placeholder if the sched_slice is not available
SELECT DISTINCT cpu, ts, dur, NULL AS utid
FROM perf_counter_track
JOIN whole_counter_slice
WHERE NOT EXISTS (SELECT * FROM sched_slice)
ORDER BY cpu,ts;
`);
await trace.engine.query(`
CREATE PERFETTO TABLE cpu_idle_slice AS
SELECT
ts,
cpu,
0 as value,
dur,
utid
FROM sched_slice
WHERE utid = 0
ORDER BY cpu,ts;
`);
}
// Create the view in the DB of the counter named in input.
// The views is named after the arm_telemetry_event_{counterName}
// and contains the following fields:
// - ts: The timestamp of the sample
// - cpu: The cpu of the sample
// - {counter}_counter: The accumulated value of the counter
// - {counter}: The delta between the counter and the one from the next sample
// - dur: The duration of the sample
// The view is also SPAN_JOIN using the cpu_busy_slice table.
// As a result, no sample happens while the CPU is idle.
private async createPerfCounterView(trace: Trace, counterName: string) {
const tableName = eventTableName(counterName);
await trace.engine.query(`
DROP VIEW IF EXISTS __raw_${tableName};
CREATE VIEW __raw_${tableName} AS
SELECT
ts,
cpu,
value AS ${counterName}_counter,
LEAD(value, 1, value) OVER (PARTITION BY cpu ORDER BY ts) - value AS ${counterName},
LEAD(ts, 1, ts ) OVER (PARTITION BY cpu ORDER BY ts ) - ts AS dur
FROM counter
JOIN perf_counter_track
ON perf_counter_track.id = counter.track_id
WHERE
name like '${counterName}';
DROP TABLE IF EXISTS __virtual_${tableName};
CREATE VIRTUAL TABLE __virtual_${tableName} USING SPAN_JOIN(
__raw_${tableName} PARTITIONED cpu,
cpu_busy_slice PARTITIONED cpu
);
DROP TABLE IF EXISTS ${tableName};
CREATE TABLE ${tableName} AS
SELECT *
FROM __virtual_${tableName}
ORDER BY cpu,ts;
`);
}
// Update metric tracks.
//
// This function is called once when the trace is loaded. Specs are expected
// to be registered before trace load; changing specs requires a trace reload.
private async updateMetricTracks(
trace: Trace,
cpuidMap: Map<number, string>,
countersPerCpu: Map<number, string[]>,
): Promise<void> {
const metrics = this.getComputableMetrics(cpuidMap, countersPerCpu);
const metricEntries = Object.entries(metrics);
if (metricEntries.length === 0) {
return;
}
await this.createMetrics(trace, metrics, [...cpuidMap.keys()]);
// Retrieve the hardware node in the workspace
const hardwareGroup = trace.plugins
.getPlugin(StandardGroupsPlugin)
.getOrCreateStandardGroup(trace.workspaces.currentWorkspace, 'HARDWARE');
// Create and attach the main telemetry node
const telemetryRootNode = new TrackNode({name: 'CPU metrics'});
hardwareGroup.addChildInOrder(telemetryRootNode);
// Iterate over all the metrics to create tracks.
for (const [metricName, desc] of metricEntries) {
// retrieve CPU this metric applies to
const cpus = desc
.map((e) => e.cpus)
.flat()
.sort();
for (const c of cpus) {
const tableName = metricTableName(metricName);
// Note: It is not possible to deregister track explicitly.
// For this reason it is important to have a UID in the URI.
const uri = `/arm_telemetry/${metricName}_${c}#${uuidv4()}`;
const track = new CounterTrack({
trace,
uri,
sqlSource: `select * from ${tableName} where cpu = ${c}`,
});
trace.tracks.registerTrack({
uri,
tags: {
kind: COUNTER_TRACK_KIND,
cpu: c,
groupName: `arm_telemetry/${metricName}`,
},
renderer: track,
});
telemetryRootNode.addChildInOrder(
new TrackNode({uri, name: `${metricName} for CPU ${c}`}),
);
}
}
}
private getComputableMetrics(
cpuids: Map<number, string>,
cpuCountersMap: Map<number, string[]>,
) {
const telemetrySpecManager = ArmTelemetryPlugin.specManager;
// Align cpuids to process the counter available
cpuids = new Map<number, string>(
[...cpuids.entries()].filter(([k, _]) => cpuCountersMap.has(k)),
);
// Resolve the generic/co-processor spec if registered
const registered = telemetrySpecManager.registeredCpuids();
const genericSpec = registered.includes(SME2_SPEC_CPUID)
? telemetrySpecManager.getCpuDesc(SME2_SPEC_CPUID)
: undefined;
// Build the list of specs per CPU: CPUID-specific + generic (if present)
const cpuSpecs = new Map<number, ArmTelemetryCpuSpec[]>();
for (const [cpu, cpuid] of cpuids.entries()) {
const specs: ArmTelemetryCpuSpec[] = [];
if (registered.includes(cpuid)) {
specs.push(telemetrySpecManager.getCpuDesc(cpuid));
}
if (genericSpec) {
specs.push(genericSpec);
}
if (specs.length > 0) {
cpuSpecs.set(cpu, specs);
}
}
// For each cpu find applicable metrics based on its available counters
const allApplicableMetrics: ApplicableMetricDesc[] = [];
for (const [cpu, specs] of cpuSpecs.entries()) {
const counters = cpuCountersMap.get(cpu)!;
for (const spec of specs) {
for (const [name, metric] of Object.entries(spec.metrics)) {
if (metric.events.every((e) => counters.includes(e))) {
allApplicableMetrics.push({
name,
cpus: [cpu],
formula: metric.formula,
events: metric.events,
});
}
}
}
}
// Merge metrics together
const applicableMetrics: {[name: string]: ApplicableMetricDesc[]} = {};
allApplicableMetrics.forEach((m) => {
const name = m.name;
const entry = applicableMetrics[name];
if (exists(entry)) {
const found = entry.find((candidate) => {
return (
m.formula === candidate.formula &&
m.events.every((e) => candidate.events.includes(e))
);
});
if (exists(found)) {
const cpu = m.cpus[0];
if (!found.cpus.includes(cpu)) {
found.cpus.push(cpu);
}
} else {
entry.push(m);
}
} else {
applicableMetrics[name] = [m];
}
});
return applicableMetrics;
}
// Retrieve the cpuid for each CPU
private async getCpuIdMap(trace: Trace): Promise<Map<number, string>> {
const result = new Map<number, string>();
const q = await trace.engine.query(`
SELECT
cpu,
EXTRACT_ARG(ARG_SET_ID, "arm_cpu_implementer") as implementer,
EXTRACT_ARG(ARG_SET_ID, "arm_cpu_part") as part
FROM cpu
WHERE implementer IS NOT NULL and part IS NOT NULL
`);
const iter = q.iter({
cpu: NUM,
implementer: NUM,
part: NUM,
});
for (; iter.valid(); iter.next()) {
const cpuid = `0x${iter.implementer.toString(16)}${iter.part.toString(16)}`;
result.set(iter.cpu, cpuid);
}
return result;
}
// Compute a given metric from its formula. The metric is created as a
// table in the database. The name of the table arm_telemetry_metric_{metricName}.
// The table contains the following fields:
// - ts: timestamp of the sample
// - cpu: cpu of the sample
// - dur: duration of the sampling
// - value: value computed from the metric formula
// - utid: Thread associated with the sample.
private async createMetrics(
trace: Trace,
metricsPerName: {[name: string]: ApplicableMetricDesc[]},
applicableCpus: number[],
) {
const idleSegment = `
SELECT
ts,
cpu,
dur,
0 AS value,
0 AS utid
FROM cpu_idle_slice
`;
for (const [metricName, metrics] of Object.entries(metricsPerName)) {
const tableName = metricTableName(metricName);
// Implementation notes:
// - If all the CPUs share the same metric definition, just select the
// various counters of the metric then use the formula to compute the metric.
// - If not all the CPUs share the same metric definition then operate at that
// metric definition level for each definition and do a UNION.
// - All the metrics are unioned with the cpu_idle_slice to let the metric drop
// to 0 when the CPU is idle.
// Optimize query if it is applicable to all the CPUS
if (
metrics.length == 1 &&
metrics[0].cpus.length == applicableCpus.length &&
metrics[0].cpus.every((c) => applicableCpus.includes(c))
) {
const metric = metrics[0];
const firstTable = eventTableName(metric.events[0]);
const fromClause = metric.events.slice(1).reduce((acc, counter) => {
const counterTable = eventTableName(counter);
return (
acc +
` LEFT JOIN ${counterTable}` +
` ON ${firstTable}.ROWID = ${counterTable}.ROWID`
);
}, firstTable);
await trace.engine.query(`
DROP TABLE IF EXISTS ${tableName};
CREATE TABLE ${tableName} AS
SELECT
${firstTable}.ts AS ts,
${firstTable}.cpu AS cpu,
${firstTable}.dur AS dur,
ifnull(${metric.formula},0) AS value,
${firstTable}.utid AS utid
FROM ${fromClause}
UNION ${idleSegment}
ORDER BY ts;
`);
} else {
await trace.engine.query(`
DROP TABLE IF EXISTS ${tableName};
CREATE TABLE ${tableName} AS
${metrics.reduce<string>((acc, metric) => {
const firstTable = eventTableName(metric.events[0]);
const fromClause = metric.events.slice(1).reduce((acc, counter) => {
const counterTable = eventTableName(counter);
return (
acc +
` LEFT JOIN ${counterTable}` +
` ON ${firstTable}.ROWID = ${counterTable}.ROWID`
);
}, firstTable);
const whereClause = metric.cpus
.slice(1)
.reduce(
(acc, cpu) => acc + ` OR ${firstTable}.cpu = ${cpu}`,
`${firstTable}.cpu = ${metric.cpus[0]}`,
);
return (
acc +
`
SELECT
${firstTable}.ts AS ts,
${firstTable}.cpu AS cpu,
${firstTable}.dur AS dur,
ifnull(${metric.formula},0) AS value,
${firstTable}.utid AS utid
FROM ${fromClause}
WHERE ${whereClause}
UNION`
);
}, '')}
${idleSegment}
ORDER BY ts;
`);
}
}
}
}