feat(perf): software-event counters and HW_CACHE events#1601
feat(perf): software-event counters and HW_CACHE events#1601JosephJoshua wants to merge 37 commits into
Conversation
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.
There was a problem hiding this comment.
评审总结
本 PR 在 stacked-PR 链(#1577 → 回溯图 → 本 PR)之上,为 StarryOS 实现了两项 perf 事件覆盖:
-
软件事件计数器(新增
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。 -
HW_CACHE 组合缓存事件(新增
ax_cpu::pmu::hw_cache_to_arm):将PERF_TYPE_HW_CACHE的cache_id | op<<8 | result<<16编码映射到 ARM PMUv3 事件号,覆盖 L1D/L1I/LL cache、D/I TLB、分支预测的 access/miss 组合;PREFETCH 和 NODE 无架构对应事件,正确返回None(与 LinuxCACHE_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_leave(task/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 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.
|
感谢建议。已补充 HW_CACHE 映射的确定性回归测试(
该测试不依赖任何 CPU 实际实现的事件集,因此能捕获硬件门控测试无法发现的映射回归。 验证:映射逻辑的全部断言(20 组受支持 + 各不支持项)均通过; 一点说明: |
There was a problem hiding this comment.
评审总结
本 PR 在 stacked-PR 链(#1577 → 回溯图 → 本 PR)之上,为 StarryOS 实现了两项 perf stat 事件覆盖:
-
软件事件计数器(新增
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。 -
HW_CACHE组合缓存事件(新增ax_cpu::pmu::hw_cache_to_arm):将PERF_TYPE_HW_CACHE的cache_id | op<<8 | result<<16编码映射到 ARM PMUv3 事件号,覆盖 L1D/L1I/LL cache、D/I TLB、分支预测的 access/miss 组合;PREFETCH 和 NODE 无架构对应事件,正确返回None(与 LinuxCACHE_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_leave(task/mod.rs)集成,与现有的perf_sched_in/perf_sched_out模式一致,添加最小化。 - 缺页钩子分别挂在
mm/access.rs(内核态修正路径)和task/user.rs(用户态缺页返回路径),两条路径互补不重复。
评审线程与先前评论
- ZCShou 的评论:「HW_CACHE 映射缺少确定性回归测试」(commit
2ba5dcb)——已在当前 headce5a7dd1a中充分解决:新增#[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.rs的hw_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 的依赖,已声明。
- #1602(
feat(perf): correct sample attribution and in-band lost records):同为 JosephJoshua 的 stacked-PR 链后续 PR,与本 PR 互补(#1601 → #1602),无冲突。 - #1603(
feat(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
left a comment
There was a problem hiding this comment.
软件事件计数器和 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。
在回溯图(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.rs的ReturnReason::PageFault,而非mm/access.rs(那是内核态修正路径),因此 page-faults 需要挂在两条路径上。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 一致)。event_supported干净地判为不支持(而非路由错误),PREFETCH 被拒绝——已验证;计数精度在开发板(RK3588 A55/A76)上验证。测试
新增
perf-sw-counters、perf-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 均通过。