Repository navigation
Cortex Perf Counter additions - #52
Merged
Martin-Lndbl merged 10 commits intoOct 12, 2026
Merged
Conversation
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-triggered, 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is a first attempt to fix possible problems that may occur. I'd like to merge a better version of this into #48 once I had time to actually review and test this myself.