8000 Adding Cortex cores (A76 and A55) perf counters by Meandres · Pull Request #48 · miniosv/miniosv · GitHub
[go: up one dir, main page]

Skip to content

Adding Cortex cores (A76 and A55) perf counters - #48

< 10BC8 div class="prc-PageHeader-Actions-wawWm flex-items-center gap-2 position-relative d-none" data-component="PH_Actions">
Merged
Meandres merged 19 commits into
masterfrom
perf/cortex-counters
Oct 18, 2026
Merged

Meandres merged 19 commits into
masterfrom
perf/cortex-counters

Conversation

@Meandres
Copy link
Copy Markdown
Collaborator

Also adds some event definitions that were missing in aarch64.
Also introduces a wrap-around feature since some ARM cores only count with 32bits registers (overflows if you count cycles over more than a few seconds).

Also adds some event definitions that were missing in aarch64.
Also introduces a wrap-around feature since some ARM cores only count
with 32bits registers (overflows if you count cycles over more than a
few seconds).
Also adds some event definitions that were missing in aarch64.
Also introduces a wrap-around feature since some ARM cores only count
with 32bits registers (overflows if you count cycles over more than a
few seconds).
Martin-Lndbl and others added 14 commits October 12, 2026 15:50
Everything the front-end caches about the PMU was resolved once, on
whichever cpu got there first. That is fine on a symmetric machine and
wrong on a Cortex-A76 + A55 SoC, where the two clusters expose different
counter counts and implement different events.

Add pmu_design_id() (MIDR on ARM, the CPUID signature on x86, which has
the same problem on hybrid parts) so callers can key their caches on it,
and make the one existing cached choice honour it: L2D_CACHE_MISS now
resolves 0x4009-vs-L2D_CACHE_REFILL on the cpu that reads it instead of
being latched at static-init time. It converts to PMCEvent, so it is
still spelled like the plain event constants beside it.

pmu_probe_event_counter_width() moves into enable_pmu(). The probe writes
PMEVCNTR0, and reaching it lazily from Event::start() clobbered whatever
measurement was already running on counter 0.

Also add the primitives the next commit needs to keep the handlers on the
shared PMU interrupt out of each other's counters -- pmc_clear_overflow,
pmc_overflow_pending and pmc_disable_overflow_int -- restore the comment
in enable_pmu() explaining why the three clears are needed, and put the
Cortex MIDR constants to work naming the core in pmu_dump_state().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ADghBqma78RJZSL1XZ348
…flows

Sharing default_core_pmcs() between PerfEvent and PMCSampler put both
handlers on the same PMU interrupt, and neither checked whose overflow it
was:

  - the wrap handler acked the whole PMOVSCLR word, swallowing a
    sampler's overflow;
  - the sampler's handler recorded a sample on every PMU interrupt,
    including wraps of a counter it does not own;
  - PMCSampler::stop() cleared PMINTENSET wholesale, silently switching
    off wrap counting for a PerfEvent still running -- which then
    undercounts by multiples of 2^32 with no diagnostic.

Each handler now looks at, and acks, only the counters it owns.
PMCSelect tracks its armed counters in armed_mask and ignores interrupts
raised on a cpu of a different design; the sampler asks
pmc_overflow_pending() first; and pmc_detach_overflow_handler() takes the
mask of the counters to release.

The reservation tables move behind one std::map keyed on pmu_design_id(),
so an asymmetric part gets a table per cluster instead of a single one
built on the boot cpu that could hand out a counter the other cluster
does not implement.

Fixes to the wrap-counting lifecycle while here:

  - arm after start_with_conf() has zeroed the counter, and clear a flag
    left by the counter's previous owner first: the PMU interrupt is
    level-trig
E870
gered, so arming on top of a stale flag keeps it asserted;
  - disarm in Event::stop(), which nothing did;
  - guard the attach with a mutex, since two threads can call
    startCounters() at once and only one handler may be attached;
  - report() no longer shifts by the counter's full width when that is
    64, which is undefined.

Event::pollCounter() becomes Event::poll() to match start()/stop(), and
both it and PerfEvent::pollCounters() document that the caller has to run
them more often than the counter can wrap for the fallback to work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011ADghBqma78RJZSL1XZ348
The four STALL* events had empty names: their report columns had no header
and getCounter() could not address them. The comments of LL_CACHE,
LL_CACHE_MISS, L1D_CACHE_MISS and L2D_CACHE_LMISS_RD now say that they count
reads / long-latency reads only (Linux include/linux/perf/arm_pmuv3.h:
LL_CACHE_RD, LL_CACHE_MISS_RD, L1D_CACHE_LMISS_RD, L2D_CACHE_LMISS_RD).
Report names are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Event::stop() added 1ull << width when the counter read lower than at the
last poll, also for width == 64 (the cycle counter with PMCR_EL0.LC, every
counter with FEAT_PMUv3p5). Shifting a 64-bit value by 64 is undefined
behaviour ([expr.shift]); report() is already guarded. A 64-bit counter does
not wrap, so nothing is added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…immed

PMCSelectCore dropped counter 0 on a Neoverse V1 under KVM only when no
counters had been trimmed (else if). Both conditions are independent: on a
VM with sliced counters (the AWS case named in the comment) running on a V1,
counter 0 stayed in the pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
pmc_read() only handles event counters 0..5 and the cycle counter; for any
other id it returned an uninitialised variable (undefined behaviour). It
reads 0 now.

pmc_start_with_conf() had no case for those ids either: it wrote no event
type, still enabled the counter in PMCNTENSET_EL0 and returned true, so the
counter counted whatever stale event type it held and the event was taken
as valid. It now returns false for them, and Event marks the event invalid.

PMUv3 has up to 31 event counters (PMCR_EL0.N); using counters 6..30 needs
the PMSELR_EL0 + PMXEVTYPER_EL0 / PMXEVCNTR_EL0 indirection (Arm ARM DDI
0487), left for a later change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Starting the cycle counter only set PMCNTENSET_EL0 bit 31 and left
PMCCFILTR_EL0 as it was. Its filter bits reset to an UNKNOWN value (Arm ARM
DDI 0487, PMCCFILTR_EL0); with P set the counter does not count at EL1, where
MiniOSv runs, and cpu-cycles / IPC are wrong. The filter bits of the event
bitmap are written now (none for PERF_COUNT_HW::CPU_CYCLES: count at EL0 and
EL1), like the event counters' PMEVTYPER<n>_EL0. Linux does the same
(drivers/perf/arm_pmuv3.c, write_pmccfiltr()).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
enable_pmu() set PMCR_EL0.LP unconditionally, but LP is RES0 before
FEAT_PMUv3p5 (software must write 0), and decided on 64-bit counters by
reading it back. pmu_probe_event_counter_width() wrote 2^32 into event
counter 0 to find the width: counter 0 is excluded on Neoverse V1 under KVM,
and the result was cached once, from whichever cpu enabled the PMU first,
which is the wrong answer for the other cluster of an asymmetric SoC.

PMUVer >= 6 (FEAT_PMUv3p5, Arm ARM DDI 0487, ID_AA64DFR0_EL1) is the
architected check, read on the calling cpu, as in Linux
(drivers/perf/arm_pmuv3.c, armv8pmu_has_long_event()). LP is only written
there; pmu_event_counter_width() replaces the probe; pmu_dump_state() also
prints PMUVer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
is_kvm_guest() executed HVC unconditionally. HVC is UNDEFINED at EL1 without
EL2 or with SCR_EL3.HCE == 0 (Arm ARM DDI 0487, HVC), and where EL2 exists
without a hypervisor it lands in whatever EL2 code firmware left. It now
takes the conduit from the ACPI FADT ARM boot flags, like
arch/aarch64/psci.cc, and only calls over HVC (KVM always uses HVC; Linux
likewise only uses the firmware-described conduit, drivers/firmware/smccc).

Also, per the SMC Calling Convention (Arm DEN 0028): 0x8600FF01 is an
SMC32/HVC32 call, so only W0..W3 are compared, and X4..X17 are declared
clobbered (SMCCC v1.0 lets the callee corrupt them). The result is computed
once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
default_core_pmcs() kept one PMCSelectCore per cpu design (MIDR / CPUID
signature), shared by every cpu of that design, although every cpu has its
own PMU. On a homogeneous machine that is one table for all cpus: concurrent
PerfEvents in pinned threads on different cpus took counters from it, and on
x64 under KVM with 4 pinned threads and the default PerfEvent (4 events),
10 of the 16 events were reported as occupied and measured 0. The wrap
counting handler was attached once per table, from the first cpu that
counted, and the PMU interrupt (a per-cpu PPI) was only enabled there.

default_core_pmcs() now returns a PerCpuPMCSelect: acquire() reserves from
the calling cpu's own table, created on that cpu on first use (it reads that
cpu's PMU, so the clusters of an asymmetric part still get matching tables);
release(), arm_wrap_counting() and disarm_wrap_counting() go to the table
the counter came from. Every table attaches its own wrap counting handler
from its cpu; the handler ignores other cpus' interrupts (instead of other
designs') and keeps acking only the counters its table armed. PerfEvent,
PerfEventBlock and the sampler keep their interface; PMCSelect's reservation
functions are virtual now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dispatcher runs the first handler whose ack() returns true and
ppi_interrupt had none, so only the oldest handler on the PMU interrupt
ever ran, leaving a foreign overflow unacked on a level-triggered PPI.
A KVM host derives the sample period from -counter truncated to the
counter's width, which is zero for a counter left at zero, so the
overflow is never reported. "before" is read after, so totals stay exact.
ID_AA64DFR0_EL1.PMUVer of 0 means PMCR_EL0 and the counter registers are
undefined. Reading them took the kernel down on a VM started with
-cpu host,pmu=off.
counts_wraps() is now asked on the cpu the counter was reserved on, and
the wrap handler reads its cpu id under the migration lock local() takes.
@Meandres
Meandres commented Oct 14, 2026 •
Copy link
Copy Markdown
Collaborator Author

Tested the fixes on an Orange Pi Ultra (with an RK3588 SoC: big.LITTLE architecture with Cortex A76 and A55 cores).
The fixes work well but needed a couple more fixes.
@Martin-Lndbl Can you try to run it on graviton to check if nothing broke ?

@Martin-Lndbl
Copy link
Copy Markdown
Collaborator

Everything still works. I found another unrelated bug and fixed it. shouldn't conflict with anything on the cortex side

@Martin-Lndbl Martin-Lndbl left a comment
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be merged from where I'm standing

@Meandres
Meandres merged commit 6928ac3 into master Oct 18, 2026
2 checks passed
@Meandres
Meandres deleted the perf/cortex-counters branch October 28, 2026 07:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants

0