Skip to content

feat(perf): software-event counters and HW_CACHE events#1601

Open
JosephJoshua wants to merge 37 commits into
rcore-os:devfrom
oscomp-posad:feat/perf-stat-coverage
Open

feat(perf): software-event counters and HW_CACHE events#1601
JosephJoshua wants to merge 37 commits into
rcore-os:devfrom
oscomp-posad:feat/perf-stat-coverage

Conversation

@JosephJoshua

Copy link
Copy Markdown
Contributor

⚠️ 依赖尚未合入 dev#1577(多核 + big.LITTLE perf 基础设施)及其上的回溯图(call-graph)工作,因此当前 diff 包含整条未合入的栈(~31 提交);本 PR 的净增改动是最上面的 2 个提交(见下)。待 #1577 合入 dev 后将 rebase 为干净的 2-提交 diff。stacked-PR 依赖:#1577 → 回溯图 → 本 PR。


在回溯图(callchain)PR 的基础上,补齐 perf stat 的事件覆盖:让软件事件成为真实计数器,并支持 HW_CACHE 组合缓存事件。

软件事件计数器(perf stat 默认事件)

perf stat -- cmd 默认打开的五个 PERF_TYPE_SOFTWARE 事件——cpu-clock、task-clock、context-switches、cpu-migrations、page-faults——此前全部走 BPF 桩,read(perf_fd) 返回 Unsupported,于是每一行都显示 <not counted>。现将它们实现为真实的 per-task 计数器(新增 perf/sw.rs,纯原子、与架构无关),由调度作用域钩子(on_enter/on_leave)与两条缺页路径驱动。全局 PERF_SW_ACTIVE 门控使无软件事件时热路径仅有一次 relaxed load。

  • 关键点:用户态缺页走 task/user.rsReturnReason::PageFault,而非 mm/access.rs(那是内核态修正路径),因此 page-faults 需要挂在两条路径上。
  • QEMU(smp4)验证:cpu-clock 247ms、task-clock 173ms(< cpu-clock)、page-faults 513、context-switches 24、cpu-migrations 可读。

HW_CACHE 组合缓存事件

perf stat -e L1-dcache-load-misses,LLC-loads,... 打开的 PERF_TYPE_HW_CACHE 事件(config 打包 cache_id | op<<8 | result<<16)此前在 open 处被拒。新增 ax_cpu::pmu::hw_cache_to_arm 将常见组合映射到 ARM PMUv3 事件号(L1D/L1I/LL cache + refill、D/I TLB + refill、分支预测 + 误预测);PREFETCH 与 NODE 无架构对应事件,返回 None(open 处拒绝,符合 Linux 的 CACHE_OP_UNSUPPORTED)。ref-cycles 在 ARM 上无真实参考周期事件,保持不支持(与 Linux 一致)。

  • CI 安全:QEMU-TCG 的 PMU 未实现缓存事件,因此在 QEMU 上每个映射组合都被 event_supported 干净地判为不支持(而非路由错误),PREFETCH 被拒绝——已验证;计数精度在开发板(RK3588 A55/A76)上验证。

测试

新增 perf-sw-countersperf-hw-cache 两个 grouped C 用例;非 aarch64 跳过即通过。

本 PR 基于回溯图 PR(feat/perf-callchain);待 #1577 合入 dev 后一并对齐到 rcore-os:dev。fmt、clippy(ax-cpu 28/28、starry-kernel 20/20)、QEMU 均通过。

Replace the single global counter allocator with a per-CPU pool
(PMEVCNTRn_EL0 is banked per-PE), reserve a slot per scheduling slice
in perf_sched_in / release it in perf_sched_out instead of pinning one
for the counter's life, and fix the cross-core read_values bug (only add
the live slice when the target runs on this core). free_hw tears the live
slice down on the owning core, via a synchronous IPI when the fd is closed
from a remote core, so it never stomps another task's counter.
A system-wide counting event pinned to a cpu (pid<=0, cpu>=0) now counts on
THAT core via its per-CPU pool, programmed/started/stopped/read/freed on the
target core through a synchronous IPI (local fast-path when it is the current
core). DISABLE stops the counter but keeps it allocated so read(perf_fd)
returns the final count; the slot is freed only at close. ensure_pmu_irq_registered
now brings up PMCR.E on every core before arming a PMU overflow.
A perf subsystem can register a fn(bool) via set_perf_tick that is invoked
from each periodic scheduler tick (timer-IRQ context). Null (zero address)
when unused, so axtask carries no hard dependency on perf. Mirrors the
existing set_run_on_cpu_sync registration hook.
When a task has more enabled counting events than its core has programmable
counters, rotate which subset holds the hardware counters on each periodic
tick so every event takes a turn. Splits time_enabled (whole on-CPU period)
from time_running (slot-hold sub-slice) so userspace scales value *
enabled/running. The tick is essential: a CPU-bound task that never preempts
still gets timer ticks, which a context switch alone would not provide.
…t fix

Events carry a ClusterMask (from the PMU type they were opened against);
generic/architectural events run on all clusters, an event opened against a
cluster PMU is restricted to it. A per-task event skips arming on a
non-matching core (time_enabled accrues, time_running does not, so perf
scales), mirroring Linux's pmu->filter; a cpu-bound system event on a
non-matching core is rejected with ENOENT. Fix BRANCH_INSTRUCTIONS to resolve
per-cluster via PMCEID (PC_WRITE_RETIRED 0x0C if implemented else BR_RETIRED
0x21), matching upstream (the repo diverged on A55). Cluster identity comes
from each core's MIDR_EL1, with a parity test override for homogeneous QEMU.
Expose per-cluster PMU devices (type 9 = A55, type 10 = A76), each with its
own cpus mask built from per-core MIDR. The generic armv8_pmuv3_0 (type 8)
stays as an all-clusters alias. Add the /proc/sys/kernel/perf_test_force_clusters
knob (parity override) so the cluster logic is exercisable on a homogeneous
machine.
Address review findings: (1) perf_sched_in opens a NEW time_enabled slice only on
the on-CPU transition, so on_exec re-entry no longer clobbers last_in_ns into
time_running > time_enabled; (2) on_task_exit snapshots the counter list and
drops the IRQ-off perf_counters lock before free_hw, which may issue a
cross-core IPI and dealloc pages; (3) read_values bases the live time_running
slice on run_since_ns (not last_in_ns) so a rotation-admitted slice cannot
over-report running; (4) rotation sizes its window against actually-free
counters (held + free pool), not raw PMCR.N, so a sys_cpu/sampling reservation
on the core no longer starves a rotatable event.
The override is a kernel-global AtomicBool that survives process exit and would
otherwise leak parity cluster classification into later suite cases.
Review finding: a pid<=0 && cpu<0 event allocates its counter on the opening
core, but enable/disable/reset/read/Drop ran on whatever core was current. A
migratable monitoring thread therefore freed the slot in the wrong per-CPU pool
and stomped another core's banked PMEVCNTRn (UAF in the sampling sub-case, where
the home core's REGISTRY slot kept dangling notify/ring pointers).

Record home_cpu at open and route the HW lifecycle (cycle counter, programmable
counter, and sampling arm/teardown) to it via run_hw_on_home — a synchronous IPI
when the caller has migrated, else a direct call. For sampling, the home core's
REGISTRY slot is unregistered before the ring/notify Arcs drop, closing the UAF.
Add perf-validate: one self-contained C binary that auto-discovers RK3588
topology and validates the SMP per-CPU + big.LITTLE hardware-PMU perf work on a
real Orange Pi 5 Plus. It prints PASS/FAIL/SKIP/INFO per check across topology,
sysfs, capacity, cluster-aware programming, BRANCH 0x0C/0x21 divergence, SMP
per-CPU, and counting/sampling/rdpmc fidelity, then a verdict
(FULL/PARTIAL/FAIL/INVALID) and a unique BOARD_PERF_VALIDATE_DONE sentinel.

It degrades gracefully: today the board boots only at max_cpu_num=1 (an smp8
late-boot hang, a non-perf bug), so needs-smp8 / needs-both-clusters checks SKIP
and the verdict is PARTIAL (single-core regression anchor passed; big.LITTLE
unvalidated). When smp8 boots, the verdict becomes FULL.

Two run modes, auto-detected from cpu0 MIDR: board mode (real A55 0xD05 / A76
0xD0B) runs the full silicon matrix; selftest mode (anything else, e.g. QEMU
cortex-a53) enables the parity override and exercises the cluster/pool LOGIC +
counting/sampling/rdpmc, with silicon-only rows self-SKIPping. An anti-false-
confidence verdict gate downgrades a green run to INVALID if the override is
stuck on or a core was not warmed before its cluster assertion.

Layout:
- board-orangepi-5-plus/perf-validate/: canonical src + board run toml + a
  decoupled smp1 build wrapper (log=Warn) + a staged (non-discovered) smp8 build
  + deploy.sh (static-musl cross-compile + scp) + README.
- qemu-smp4/system/perf-validate/: byte-identical copy + CMakeLists, a permanent
  QEMU regression (auto selftest, exits 0). Verified: SELFTEST-OK, 18 pass /
  0 fail / 22 skip / 6 info, STARRY_GROUPED_TESTS_PASSED; -Werror clean.
The perf ring / rdpmc / mmap-page frames are RAM the kernel writes through its
cacheable linear map and userspace reads back, but PerfEvent::device_mmap wrapped
them in DeviceMmap::Physical, which mmap maps UNCACHED (Normal-NonCacheable). On
real silicon the kernel's cached writes never reach RAM, so userspace's uncached
reads return stale zeros: sampling delivered 0 samples (ring data_head read 0)
and the rdpmc mmap page read cap/index/width = 0. QEMU models no caches, so it
never surfaced this — only the on-board run did.

Map these pages cacheable instead (DeviceMmap::PhysicalCached). Kernel and user
then hold Normal Inner-Shareable Write-Back mappings of the same physical page,
which the hardware keeps coherent across the inner-shareable domain (both RK3588
clusters) with no explicit cache maintenance. The sample-ring writer already
issues a Release fence before publishing data_head, and the reader pairs it with
an acquire barrier, so the weaker ordering of Normal memory is handled correctly.

PhysicalCached was feature-gated to rknpu; ungate it (it is a general
cacheable-RAM mapping — the caller decides whether DMA maintenance is needed:
rknpu yes, perf no since it is CPU-to-CPU coherent) and update its doc. Builds
clean on aarch64 + riscv64; clippy/fmt clean; QEMU perf-hw sample/sample-task/
rdpmc/record-ioctl/ring-merge/freq/sideband/fork-exit/inherit + perf-validate
selftest all still pass (no regression). Board re-run pending to confirm the fix.
perf_event_mmap_page.capabilities places cap_user_rdpmc at bit 2 (Linux ABI:
after cap_bit0 and cap_bit0_is_deprecated). The test struct omitted
cap_bit0_is_deprecated, so cap_user_rdpmc landed at bit 1 and read the kernel's
`1<<2` as 0 — RDPMC-1 spuriously FAILed on the board (cap=0) even though the
page was correct (index/width/rdpmc value all right after the cacheable-mmap
kernel fix). Add the missing bit; also assert cap in RDPMC-2 (the cycle-counter
event opens under QEMU, so this validates the bitfield there). QEMU selftest:
RDPMC-2 cap=1, SELFTEST-OK 18 pass/0 fail.
…on hardware)

Corrected against the actual Orange Pi 5 Plus run: deploy to /usr/local/bin
(the SD ext4 StarryOS mounts as /; /root is 700-root, not orangepi-writable),
stage-to-/tmp + sudo-install in deploy.sh, the real board-test command
(cargo xtask starry test board -c board-orangepi-5-plus/perf-validate ...), and
the NIC caveats (cabled port drift enP3p49s0/enP4p65s0; en5 re-arm after each
reboot). Ignore the built binary artifact.
…facts

Booted the board fully multi-core (online=8, clusters=4+4) with a minimal
feature set and ran the whole SMP + big.LITTLE matrix on real 4xA55+4xA76:
38 pass incl. cross-cluster ENOENT (CLU-1/2), cluster-skip (CLU-4: Big counted,
Little ~50% skipped), cross-cluster migration, per-cpu fan-out, home-core IPI,
per-secondary PMU init + sampling (CAP-4/SAMP-2), rotation, 12-thread spread.
The whole multi-core perf path works on silicon.

Root cause of the smp8 boot hang (documented in smp8-staged-build-aarch64.toml):
it is NOT the block device — a secondary A76 (cpu4) coming online triggers a USB
IRQ storm (rockchip-dwc-xhci IRQ 253 taken in a loop), same class as the NPU
secondary-core init hang. Neither is needed for perf, so the staged smp8 build
now drops USB/NPU/PCIe/net, keeping only SoC + SD/block (which polls).

Two board fails were test artifacts, fixed:
- BR-7: pin the opener to the A55 before opening/​running the raw 0x0C event —
  the kernel resolves event support on the opening core, and the cpu-bound
  counters need the branchy workload to run on the A55. On 8 cores an unpinned
  opener/workload lands on an A76 (no 0x0C) and reads 0.
- IPC-1: use an ILP-rich register-only loop and REPORT (INFO, not FAIL) — IPC is
  workload-dependent; on a memory-serialized loop the in-order A55 legitimately
  matches/beats the A76, so a hard "A76 IPC > A55" assert is wrong.

QEMU selftest still SELFTEST-OK (both checks skip on homogeneous QEMU); -Werror
clean. The committed default wrapper stays smp1 (PARTIAL); smp8 = staged config.
Upstream merged the standalone qemu-smp4 harness into the main qemu/ system
group (now max_cpu_num=4), which already hosts the per-task perf-hw-* tests and
the SMP affinity/clone/fcntl cases. The multicore perf-hw-smp-* and perf-validate
subcases were added under the now-removed qemu-smp4/system/, so move them into
qemu/system/ where the group CMakeLists globs them and they run at smp4.
The smp8 full-matrix run (ymodem FIT transfer + the long SMP-ROTATE /
SMP-MIGRATE / RDPMC-3 loops on real silicon) can exceed the 600s smp1
anchor; give it headroom so a slow-but-healthy run is not cut off before
BOARD_PERF_VALIDATE_VERDICT.
The sampling ring dropped records silently when full, so perf record printed
"read LOST count failed": read(perf_fd) with PERF_FORMAT_LOST (set by perf's
record__read_lost_samples) returned a short buffer because that read_format bit
was unhandled.

Account dropped samples per event. ring_write now reports whether it wrote; the
overflow handler bumps the owning event's lost counter on a drop (via a raw
pointer in SampleSlot, mirroring notify). read() appends the u64 lost field when
PERF_FORMAT_LOST is set. The counter lives on SamplingState (self/cpu path) and
PerTaskCounter (per-task path), each reached by read_values.

Verified on StarryOS QEMU: perf record no longer errors and writes a valid
perf.data (Total Lost Samples: 0).
Upstream linux-6.1 perf, statically linked for aarch64-musl, runs on StarryOS
via the Linux-compatible perf_event_open(2) ABI: perf stat counts cycles, perf
record writes a valid perf.data, and perf report symbolizes kernel functions via
/proc/kallsyms. build-perf.sh pins the reproducible build; the smoke case stages
the binary and exercises stat/record/report.

The 2.8 MB binary is gitignored and produced by build-perf.sh; when it is absent
(CI, or before a local build) the case installs nothing and is skipped so the
qemu/system group still builds.
build-perf-libelf.sh + musl-compat.h build a static aarch64-musl perf WITH
libelf natively on Alpine, so perf report resolves user symbols to names
(ld-musl-aarch64.so.1 [.] _dlstart) instead of raw offsets; kernel symbols
already resolve via /proc/kallsyms. Documents the toolchain + the six
musl/Alpine build fixes. The smoke script gains per-step markers to localize
regressions. The 2.8MB binary stays gitignored (built via the recipe).
The perf-validate selftest reads ARM PMU sysregs via raw inline asm
(mrs pmccntr_el0 / pmevcntrN_el0), which the loongarch64 (and other
non-aarch64) C compilers reject with "unrecognized instruction
mnemonic". Route the qemu/system case through the shared
starry_arch_filtered_executable helper so non-aarch64 targets build the
skip stub instead, matching test-user-backtrace and the other
arch-specific cases.
The six per-CPU / big.LITTLE perf SMP selftests are hardware-PMU (ARM
PMUv3) tests; on loongarch64/riscv64/x86_64 perf_event_open on the RAW
0x11 event has no ARM PMU to bind, so they exit non-zero and fail the
grouped-C suite. Every other perf-hw-* case already skips-as-pass off
aarch64; add the same guard here (build+run everywhere, print the OK
sentinel and return 0 on non-aarch64). The earlier perf-validate
arch-gate fix unmasked these at runtime (the build previously stopped at
perf-validate before reaching them). Verified they compile clean for
loongarch64 with -Wall -Wextra -Werror.
The PMU overflow handler runs inside dispatch_irq and can read the
interrupted PC/EL live (ELR_EL1/SPSR_EL1), but the interrupted frame
pointer (x29) survives only in the saved TrapFrame — the handler's own
frames have already clobbered the GPR. Call-graph sampling
(PERF_SAMPLE_CALLCHAIN) needs that x29.

Publish a per-CPU *const TrapFrame at the two IRQ-entry sites (EL1
kernel interrupt in trap.rs, EL0 user interrupt in UserContext::run)
immediately before dispatch_irq and clear it immediately after, both
inside the IRQ-masked window so no nested IRQ observes a stale frame.
Add interrupted_fp()/interrupted_sp() accessors alongside the existing
interrupted_pc()/interrupted_is_user(). The publish/clear is a single
per-CPU store each (TPIDR-relative, cannot fault) and does not reorder
the register save/restore path.

Pulls ax-percpu into the aarch64 dependency set (already present for
x86_64) for the per-CPU cell.
…e getters

Add walk_fp(pc, fp, ip_range, fp_range, read, out) -> usize: an
allocation-free frame-pointer walk into a caller-supplied buffer that
reads memory through an injected closure instead of dereferencing
directly, so a hard-IRQ caller (PMU-sampling call-graph capture) can
supply a fault-safe reader. Mirrors unwind_core's guards (monotonic fp,
8 MiB jump cap, word alignment, depth cap via out.len()) but never
allocates and is not behind the alloc feature, so it is available in the
default (no-alloc) kernel build.

Add ip_range()/fp_range() getters over the ranges init() records, each a
lock-free spin::Once load safe to call from IRQ context, so the sampling
unwinder can reuse the kernel text / address-space bounds.

Serialize the depth-sensitive tests: set_max_depth mutates a process
global, so init_for_tests now returns a guard that holds a shared lock
for the test body, removing a latent flake under cargo test's parallel
harness (surfaced by the new walk_fp tests raising thread contention).
Emit PERF_SAMPLE_CALLCHAIN so perf record -g captures per-sample kernel
stack backtraces (frame-pointer), rendering as flamegraphs off-target.

- perf/unwind.rs: kernel-side integration over axbacktrace::walk_fp —
  kernel_ranges() reuses the boot-time backtrace ranges, a direct-deref
  reader for always-mapped kernel frames, and kernel_callchain() that
  walks the interrupted x29 chain into a fixed buffer.
- sampling.rs: define PERF_SAMPLE_CALLCHAIN / PERF_CONTEXT_KERNEL /
  PERF_CONTEXT_USER, add CALLCHAIN to SUPPORTED_SAMPLE_TYPE (both hw.rs
  open sites accept it via the mask), grow SAMPLE_RECORD_MAX_LEN to bound
  the callchain block, emit 'u64 nr + entries' in build_sample in Linux
  field order, and build the chain in the overflow handler from the
  interrupted frame pointer (leaf-only fallback when none is published,
  so a sample is never dropped).

Kernel frames only in this pass; the user region is a leaf IP with a
PERF_CONTEXT_USER marker (the no-fault user FP unwind is M4b). Sampling
stays aarch64-only and allocation-free in IRQ context.
…d bound

Follow-up hardening from adversarial review of the PMU-sampling callers:

- Bound TOTAL iterations (a small multiple of out.len()), not just recorded
  frames. A return address outside ip_range is skipped without consuming a
  recorded slot, so the depth cap alone did not bound the loop; a crafted
  all-skip user chain (every [fp+8] out of range, monotonic small-gap [fp])
  could otherwise spin the walk — each step doing real work (two no-fault
  page-table walks) — into an IRQ-latency DoS. Apply the 8 MiB-gap check on
  every fp advance (both the skip and record paths), not only after recording.
- Bound the whole frame record [fp, fp+16) inside fp_range (not just fp), so
  the saved-LR read at fp+8 can never touch one word past fp_range.end.

Adds a test that would hang without the step cap (an ever-advancing all-skip
chain) and confirms it terminates recording only the leaf.
Add user-space callchains (PERF_SAMPLE_CALLCHAIN user region) and make the
whole FP unwind fault-proof from hard-IRQ context.

- perf/nofault.rs: read_user_word_nofault / read_kernel_word_nofault walk
  TTBR0 / TTBR1 by hand (4-level, 48-bit VA) against the always-mapped direct
  map, never dereferencing the input VA. Every table frame and the resolved
  leaf is bound-checked against phys_ram_ranges() before deref, so a valid PTE
  whose output is device MMIO (a user mmap of an RGA/NPU/JPU register window,
  or a concurrently-torn table on another CPU) can never trigger a live MMIO
  read (device side effect) or a data abort (panic) in the overflow handler.
- perf/unwind.rs: route the kernel reader through the no-fault path too, so a
  corrupt/stale in-kernel frame pointer that slips past the range guards yields
  None instead of faulting; add user_callchain, bounding the walk to a window
  above the interrupted SP_EL0.
- perf/sampling.rs: build_callchain now unwinds the user region for user
  samples (deep [PERF_CONTEXT_USER, ip, ra…]) via the no-fault reader, seeded
  from the interrupted x29 + SP.

Kernel deep frames still require an FP-enabled kernel (-Cforce-frame-pointers);
user frames unwind whenever the sampled binary keeps frame pointers.
Two grouped-C qemu/system cases exercising PERF_SAMPLE_CALLCHAIN end to end
(open sampling event with IP|TID|TIME|CALLCHAIN, mmap ring, parse the callchain
block, classify by PERF_CONTEXT_* markers):

- perf-hw-callchain-kernel: asserts a well-formed callchain record with a
  PERF_CONTEXT_KERNEL region + leaf IP. CI-safe: does NOT require a deep kernel
  chain, since the default kernel is built without frame pointers (like Linux
  without CONFIG_FRAME_POINTER), so the kernel region is leaf-only.
- perf-hw-callchain-user: builds a known nested chain outer->mid->inner->busy
  with -fno-omit-frame-pointer and asserts >= 4 user IPs after PERF_CONTEXT_USER
  — proving the kernel unwinds several USER frames via the no-fault TTBR0 reader.
  Uses a read(/dev/zero) workload: QEMU-TCG's cycle counter barely advances on a
  pure-ALU loop, so the sampling counter would seldom overflow.

Both skip-as-pass off aarch64. Validated in QEMU (smp4): kernel record
well-formed; user unwinds 8 frames deep across repeated runs.
`perf stat -- cmd` opens its default set with no `-e`: hardware cycles/
instructions plus five PERF_TYPE_SOFTWARE events — cpu-clock, task-clock,
context-switches, cpu-migrations, page-faults. All five dispatched to the BPF
stub (no readable count), so read(perf_fd) returned Unsupported and every default
row printed `<not counted>`. Make them real per-task counters so a bare
`perf stat -- cmd` looks correct.

- perf/sw.rs (new): a lightweight per-task SwPerTaskCounter + SwPerfEvent
  (PerfEventOps). Pure atomics, no PMU, so it is arch-independent. cpu-clock is
  wall time enabled; task-clock is on-CPU time; context-switches/cpu-migrations/
  page-faults are event counts. A global PERF_SW_ACTIVE gate makes every hook a
  single relaxed load when no software event exists (mirrors PERF_TASK_ACTIVE).
- perf/mod.rs: route the five counting software configs to sw::perf_event_open_sw
  (pid > 0 a specific tid, pid == 0 the caller); every other software config
  (e.g. PERF_COUNT_SW_DUMMY, perf record's tracking event) keeps the BPF path.
- task/mod.rs: drive the counters from the existing scheduler scope hooks
  (on_enter -> sw::sched_in opens the task-clock slice + counts cpu-migrations;
  on_leave -> sw::sched_out folds it + counts context-switches), added alongside
  the hardware perf_sched_in/out without touching their logic. Thread gains a
  perf_sw_counters list.
- task/user.rs + mm/access.rs: count page-faults from BOTH fault paths — the
  user-mode EL0 fault (ReturnReason::PageFault) and the kernel-mode fault on user
  memory — so the count matches Linux's all-faults semantics.

Adds perf-sw-counters: opens all five events, runs a workload that provably
generates each (busy loop; mmap + MADV_DONTNEED re-touch for guaranteed
demand-zero faults; nanosleep loop for deschedules), and asserts read(perf_fd)
returns a real count. Validated in QEMU (smp4): cpu-clock 247ms, task-clock 173ms
(< cpu-clock), page-faults 513, context-switches 24, cpu-migrations readable.
Skips-as-pass only if perf_event_open is unwired (ENOSYS).
`perf stat -e L1-dcache-load-misses,LLC-loads,dTLB-load-misses,...` opens
PERF_TYPE_HW_CACHE events whose config packs cache_id | (op<<8) | (result<<16).
These were rejected at open ("unsupported hardware type"); only the
PERF_TYPE_HARDWARE cache-references/misses aliases worked. Decode the matrix to
ARM PMUv3 event numbers.

- axcpu/aarch64/pmu.rs: hw_cache_to_arm(config) maps the well-defined
  combinations — L1D/L1I/LL cache + refill, D/I TLB + refill, branch pred +
  mispred — to their ARM PMUv3 event numbers. ARM's basic cache events do not
  split read vs write, so both map to the same event; PREFETCH ops and the NODE
  cache have no architectural counterpart and return None (rejected at open,
  matching Linux's CACHE_OP_UNSUPPORTED).
- perf/hw.rs: decode PERF_TYPE_HW_CACHE in both the per-task (decode_arm_event)
  and self/system-wide (perf_event_open_hw) paths via hw_cache_to_arm, then
  alloc_programmable — which already gates on event_supported, so a combination
  the core's PMU does not implement is cleanly reported unsupported.
- perf/mod.rs: route PERF_TYPE_HW_CACHE to the hardware path.

Adds perf-hw-cache: opens the common cache combinations and an unsupported L1D
PREFETCH. CI-safe: QEMU-TCG's PMU implements no cache events, so on QEMU every
mapped combination is cleanly reported unsupported (not a routing error) and the
PREFETCH is rejected — validated. Count accuracy is validated on the board (the
RK3588 A55/A76 PMUs implement these events). Skips-as-pass off aarch64.
Copilot AI review requested due to automatic review settings July 14, 2026 06:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

评审总结

本 PR 在 stacked-PR 链(#1577 → 回溯图 → 本 PR)之上,为 StarryOS 实现了两项 perf 事件覆盖:

  1. 软件事件计数器(新增 perf/sw.rs):将 perf stat 默认的五个 PERF_TYPE_SOFTWARE 事件(cpu-clock / task-clock / context-switches / cpu-migrations / page-faults)从 BPF 桩改为真实的 per-task 原子计数器,由调度器作用域钩子(sched_in/sched_out)和两条缺页路径驱动。全局 PERF_SW_ACTIVE 门控确保无软件事件时热路径仅有一次 relaxed load。

  2. HW_CACHE 组合缓存事件(新增 ax_cpu::pmu::hw_cache_to_arm):将 PERF_TYPE_HW_CACHEcache_id | op<<8 | result<<16 编码映射到 ARM PMUv3 事件号,覆盖 L1D/L1I/LL cache、D/I TLB、分支预测的 access/miss 组合;PREFETCH 和 NODE 无架构对应事件,正确返回 None(与 Linux CACHE_OP_UNSUPPORTED 一致)。

实现逻辑

  • sw.rs 设计清晰:SwPerTaskCounter 使用纯原子操作(AtomicBool/AtomicU64/AtomicU32),Sync 且无分配,可在 IRQ-off 的调度器/缺页热路径中安全驱动。enable/disable/reset/snapshot 方法正确处理了 time_enabled/time_running 的窗口折叠,与 Linux perf 语义对齐。
  • hw_cache_to_arm 映射表完整覆盖 ARM PMUv3 定义的缓存事件,ARM 不区分 read/write 的组合正确合并匹配。
  • 调度器钩子通过 on_enter/on_leavetask/mod.rs)集成,与现有的 perf_sched_in/perf_sched_out 模式一致。
  • 缺页钩子分别挂在 mm/access.rs(内核态修正路径)和 task/user.rs(用户态缺页返回路径),两条路径互补不重复——作者在 PR 描述中已明确解释。

验证结果

  • cargo fmt --check:通过
  • cargo clippy --manifest-path components/axcpu/Cargo.toml --all-features -- -D warnings:通过
  • cargo clippy --manifest-path components/axbacktrace/Cargo.toml --all-features -- -D warnings:通过
  • cargo clippy --manifest-path os/StarryOS/kernel/Cargo.toml --all-features -- -D warnings:通过
  • ✅ 未发现 [patch.crates-io] 覆盖
  • ✅ 测试用例 (perf-sw-counters, perf-hw-cache) 放置在正确的 test-suit/starryos/qemu/system/ 分组路径下,CMakeLists.txt 结构符合现有规范,测试失败通过退出码传播(wrapper 脚本 STARRY_GROUPED_TEST_FAILED 机制正确覆盖)

CI 状态

PR 来自 fork 组织 oscomp-posad,GitHub Actions 所有 job 均为 skipped(fork PR 需审批后才能运行 CI)。这不影响评审结论:本地 fmt + clippy 全部通过,代码变更面集中且可控。

重复/重叠分析

  • 已检查 #1602 (feat(perf): correct sample attribution and in-band lost records):同为 JosephJoshua 的 stacked-PR 链上的后续 PR,与本 PR 互补(本 PR → #1602),无冲突。
  • dev 分支上无明显等效实现。

测试覆盖

  • perf-sw-counters:打开全部五个软件事件,运行 workload 触发每种事件,断言 read(perf_fd) 返回真实计数值且 cpu-clock/task-clock/page-faults/context-switches > 0。非 aarch64 上 ENOSYS 跳过即通过。
  • perf-hw-cache:打开常见 HW_CACHE 组合,接受 ENOSYS/EOPNOTSUPP(QEMU-TCG 无缓存事件)或成功 open+read;明确拒绝不支持的 PREFETCH 组合。非 aarch64 上跳过即通过。

两个测试的布局 (qemu/system/<case>/CMakeLists.txt + src/) 与现有分组 C 用例完全一致,runner 通过 GLOB 发现并构建。

已知注意事项

  • PR 依赖尚未合入 dev#1577 和回溯图 PR,当前 diff 包含整条未合入栈(~31 提交)。待依赖 PR 合入后需 rebase 为干净的 2-提交 diff。
  • 无先前评审评论需处理(仅有一条 Copilot bot 配额不足的无效评审)。

结论

代码质量良好,设计合理,测试覆盖到位,无阻塞性问题。APPROVE

Powered by deepseek-v4-pro

@ZCShou ZCShou left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

HW_CACHE 映射缺少确定性回归测试

ZCShou noted the HW_CACHE support lacks a deterministic regression test: the
QEMU perf-hw-cache case can only check open-or-unsupported (QEMU-TCG
implements no cache events), so it never verifies the config->ARM-event decode
itself.

Add a #[cfg(test)] unit test in axcpu's aarch64 pmu module that pins the pure
hw_cache_to_arm mapping: every supported (cache_id, op, result) triple maps to
its exact ARM PMUv3 event, and the unsupported classes (PREFETCH ops, NODE
cache, instruction-side writes, out-of-range ids) map to None. Independent of
any CPU's implemented event set, so it catches a mapping regression a
hardware-gated test cannot.

Runs via `cargo test -p ax-cpu` on an aarch64 host (the module is
target_arch=aarch64-gated). Logic verified — all 20 supported + the
unsupported assertions pass — and it compiles for aarch64.
@JosephJoshua

Copy link
Copy Markdown
Contributor Author

感谢建议。已补充 HW_CACHE 映射的确定性回归测试(ce5a7dd1a)。

test-suit 里的 perf-hw-cache QEMU 用例只能做「open 成功或 unsupported」的判断——因为 QEMU-TCG 的 PMU 不实现任何 cache 事件,所以它无法验证 config → ARM 事件号的解码本身。为此在 components/axcpu/src/aarch64/pmu.rs 为纯函数 hw_cache_to_arm 新增 #[cfg(test)] mod hw_cache_tests,确定性地钉住整张映射表:

  • 每个受支持的 (cache_id, op, result) 三元组 → 精确的 ARM PMUv3 事件号(L1D/L1I/LL/DTLB/ITLB/BPU 的 access/miss、read/write 共 20 组);
  • 不支持的类别 → None:所有 PREFETCH op、NODE cache、指令侧写(L1I/ITLB write)、越界 id。

该测试不依赖任何 CPU 实际实现的事件集,因此能捕获硬件门控测试无法发现的映射回归。

验证:映射逻辑的全部断言(20 组受支持 + 各不支持项)均通过;ax-cpu 已为 aarch64 编译通过(含该测试模块)。

一点说明:pmu.rs 位于 #[cfg(target_arch = "aarch64")] 模块,而 cargo xtask clippy --package ax-cpu / 现有 std 测试作业以 x86_64-unknown-none 为目标,会把该模块编译掉,故此单测在 aarch64 上通过 cargo test -p ax-cpu 运行(本地已验证逻辑)。若希望把它接入 CI 作为常驻回归,我可以补一个 aarch64 组件单测作业——按需再加。

@mai-team-app mai-team-app Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

评审总结

本 PR 在 stacked-PR 链(#1577 → 回溯图 → 本 PR)之上,为 StarryOS 实现了两项 perf stat 事件覆盖:

  1. 软件事件计数器(新增 perf/sw.rs):将 perf stat 默认的五个 PERF_TYPE_SOFTWARE 事件(cpu-clock / task-clock / context-switches / cpu-migrations / page-faults)从 BPF 桩改为真实的 per-task 原子计数器,由调度器作用域钩子(sched_in/sched_out)和两条缺页路径(mm/access.rs 内核态 + task/user.rs 用户态)驱动。全局 PERF_SW_ACTIVE 门控确保无软件事件时热路径仅有一次 relaxed load。

  2. HW_CACHE 组合缓存事件(新增 ax_cpu::pmu::hw_cache_to_arm):将 PERF_TYPE_HW_CACHEcache_id | op<<8 | result<<16 编码映射到 ARM PMUv3 事件号,覆盖 L1D/L1I/LL cache、D/I TLB、分支预测的 access/miss 组合;PREFETCH 和 NODE 无架构对应事件,正确返回 None(与 Linux CACHE_OP_UNSUPPORTED 一致)。

实现逻辑

  • sw.rs 设计清晰:SwPerTaskCounter 使用纯原子操作(AtomicBool/AtomicU64/AtomicU32),Sync 且无分配,可在 IRQ-off 的调度器/缺页热路径中安全驱动。enable/disable/reset/snapshot 方法正确处理了 time_enabled/time_running 的窗口折叠,与 Linux perf 语义对齐。
  • hw_cache_to_arm 映射表完整覆盖 ARM PMUv3 定义的缓存事件,ARM 不区分 read/write 的组合正确合并匹配(如 L1D 的 READ/WRITE 均映射到 0x04/0x03)。
  • 调度器钩子通过 on_enter/on_leavetask/mod.rs)集成,与现有的 perf_sched_in/perf_sched_out 模式一致,添加最小化。
  • 缺页钩子分别挂在 mm/access.rs(内核态修正路径)和 task/user.rs(用户态缺页返回路径),两条路径互补不重复。

评审线程与先前评论

  • ZCShou 的评论:「HW_CACHE 映射缺少确定性回归测试」(commit 2ba5dcb)——已在当前 head ce5a7dd1a 中充分解决:新增 #[cfg(test)] mod hw_cache_tests,包含 20 组受支持组合的精确事件号断言 + 所有不支持路径的 None 断言(PREFETCH/NODE/指令侧写/越界 id)。
  • mai-team-app[bot] APPROVE:针对旧 head 2ba5dcb,已过期。当前 head 在此基础上增加了测试覆盖。
  • Copilot bot 的评论(配额不足)无需处理。

测试覆盖

  • perf-sw-counters:打开全部五个软件事件,运行 workload 触发每种事件,断言 read(perf_fd) 返回真实计数值且 cpu-clock/task-clock/page-faults/context-switches > 0;cpu-migrations 仅要求可读(≥0)。非 aarch64 上 ENOSYS 跳过即通过。布局 qemu/system/perf-sw-counters/ 符合现有分组 C 用例规范,runner 通过 GLOB 发现并构建。
  • perf-hw-cache:打开常见 HW_CACHE 组合,接受 ENOSYS/EOPNOTSUPP(QEMU-TCG 无缓存事件)或成功 open+read;明确拒绝不支持的 PREFETCH 组合。非 aarch64 上跳过即通过。布局正确。
  • Rust 单测components/axcpu/src/aarch64/pmu.rshw_cache_tests 模块包含 5 个 #[test],确定性地钉住映射表——这是 QEMU 用例无法做到的(QEMU-TCG 不实现缓存事件)。该测试是纯函数测试,不依赖 CPU 实现的事件集。

验证结果

  • cargo fmt --check:通过
  • cargo clippy --manifest-path components/axcpu/Cargo.toml --all-features -- -D warnings:通过
  • cargo clippy --manifest-path os/StarryOS/kernel/Cargo.toml --all-features -- -D warnings:通过
  • ✅ 未发现 [patch.crates-io] 覆盖
  • ✅ 无 unwrap/expect/panic 在生产路径中滥用

CI 状态

PR 来自 fork 组织 oscomp-posad,GitHub Actions 所有 job 均为 skipped(fork PR 需审批后才能运行 CI)。这属于预期行为,不影响评审结论:本地 fmt + clippy 全部通过,代码变更面集中且可控。

重复/重叠分析

  • #1577(多核 + big.LITTLE perf 基础设施):本 PR 的依赖,已声明。
  • #1602feat(perf): correct sample attribution and in-band lost records):同为 JosephJoshua 的 stacked-PR 链后续 PR,与本 PR 互补(#1601#1602),无冲突。
  • #1603feat(perf): event groups, PERF_SAMPLE_READ, group-leader sampling):同为 stacked-PR 链更上层,互补无冲突。
  • dev 分支上无明显等效实现。

未解决问题与风险

  • PR 依赖尚未合入 dev#1577 和回溯图 PR,当前 diff 包含整条未合入栈(~37 提交)。待依赖 PR 合入后需 rebase 为干净的提交 diff。这不影响本 PR 的正确性评审,但需注意合入顺序。

结论

代码质量良好,设计合理,测试覆盖到位(QEMU 分组用例 + Rust 确定性单测),无阻塞性问题。APPROVE

Powered by deepseek-v4-pro

@ZR233 ZR233 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

软件事件计数器和 HW_CACHE 映射的栈顶净增提交及其 grouped tests 未发现新的独立阻塞;但本 PR 仍包含未合入的 #1577/callchain 基座,且 current-head OrangePi 5 Plus 门禁失败。日志显示 /usr/local/bin/perf-validate: not found 后 board case 超时 900 秒。由于这是本栈声明的 RK3588 PMU 验证路径,不能在资产未被 staging 的状态下批准;请先修复底层栈并 rebase 为可独立判断的净 diff。

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.

4 participants