feat(perf): correct sample attribution and in-band lost records#1602
feat(perf): correct sample attribution and in-band lost records#1602JosephJoshua wants to merge 36 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_RECORD_SAMPLE carried the axtask scheduler id as both pid and tid, while the COMM/MMAP2/FORK/EXIT side-band records already carry the real userspace tgid/tid. The mismatch broke `perf report`'s sample -> process/DSO-map join, misattributing samples across a multithreaded process. - sampling.rs: derive the sample (pid, tid) from the real userspace ids. For a per-task event use the owner (tgid, tid) captured at slice-arm time into the SampleSlot: the PMU overflow IRQ can be serviced after a context switch away from the monitored task, so `current()` in the handler would misattribute the sample; the captured owner is always the event's task. For a system-wide event (owner_ids == None) attribute to the interrupted `current()`, which matches the sampled IP -- reading proc.pid()/thr.tid() lock-free from IRQ context (task_ext downcast + field read + atomic load), falling back to the scheduler id for a kernel task with no Thread. - task.rs: arm_slice stamps the monitored thread's (tgid, tid) into the per-task SampleSlot; both callers (perf_sched_in, perf_rotate_current) hold the Thread. - hw.rs: the system-wide slot carries owner_ids = None. Single-threaded `perf record ./cmd` already joined by accident (clone derives the userspace tid from the axtask id, and the leader tid == getpid()); this fixes the multithreaded case where a sample's pid must be the shared tgid, not the per-thread tid. Adds perf-hw-sample-tid: a non-leader worker thread opens a per-task sampling event on its own tid and asserts every PERF_RECORD_SAMPLE reports pid == process tgid and tid == worker gettid(). Validated in QEMU (smp4): 1024/1024 samples correct, 0 misattributed. Skips-as-pass off aarch64.
When a sampling ring fills, the overflow handler drops samples and bumps the event's lost counter for the read-only PERF_FORMAT_LOST total. Linux also writes an in-band PERF_RECORD_LOST (type 2) record into the ring once it has room, so `perf report`/`perf script` show "LOST n events!" and place the gap on the timeline. Add it. - sampling.rs: a companion per-event `lost_reported` counter tracks how many drops have been emitted in-band. Before writing each sample, the handler flushes `lost - lost_reported` as a PERF_RECORD_LOST record (header + event id + lost count) when the ring has room — it was full when the drops happened, so the record is retried on later samples once userspace drains the ring. Reuses the existing back-pressure in ring_write (a record is only written when it fits), so a still-full ring simply leaves the count pending. - hw.rs / task.rs: carry the `lost_reported` AtomicU64 alongside the existing `lost` on both the system-wide (SamplingAnchors) and per-task (PerTaskCounter) events, and thread its pointer into the SampleSlot. Adds perf-hw-lost: opens a sampling event with a one-page ring, overflows it with a syscall-heavy workload, drains (advances data_tail) between bursts to free room, and asserts a PERF_RECORD_LOST record with a non-zero lost count is emitted. Validated in QEMU (smp4): 2033 samples, 15 lost records, 1318529 total lost reported in-band. Skips-as-pass off aarch64.
There was a problem hiding this comment.
评审总结
本 PR 在回溯图(callchain)PR 之上,修正了两个 perf 采样记录的正确性问题:样本归属(真实 tgid/tid)和 in-band PERF_RECORD_LOST 记录。净增改动集中在最上面的 2 个提交(约 200 行内核代码 + 2 个测试用例),范围聚焦、逻辑清晰。
变更分析
1. 样本归属修正(commit ee5a77241)
- 问题:此前
PERF_RECORD_SAMPLE的 pid/tid 取自 axtask 调度器 id,而 COMM/MMAP2 边带记录使用真实的 starry-process tgid/tid,二者不一致导致perf report无法将样本正确关联到进程/DSO 映射。 - 方案:对于 per-task 事件,在 slice-arm 时将 owner
(tgid, tid)捕获进SampleSlot,避免溢出 IRQ 在任务切换后处理时current()误归属。系统级事件(owner_ids == None)归到被中断的current(),与采样 IP 一致。 - 实现正确:
arm_slice在持有&Thread的调用点捕获 pid/tid,handler 中优先使用捕获值,fallback 路径对 kernel task 也有合理的调度器 id 兜底。
2. in-band PERF_RECORD_LOST(commit e966698d9)
- 问题:采样环写满时丢样本,此前仅有
PERF_FORMAT_LOST累计值(只读),perf report无法显示丢失事件。 - 方案:按 Linux 语义,在 handler 写样本前将
lost - lost_reported差值以 type-2 的PERF_RECORD_LOST记录写入环中(环有空间时),并通过lost_reported跟踪已上报数量。 - 实现正确:
build_lost_record使用 native-endian 序列化,与 Linuxperf_event_header布局一致。lost_reported仅在ring_write成功后更新,避免重复上报。
验证结果
| 检查项 | 结果 |
|---|---|
cargo fmt --check |
✅ 通过 |
cargo clippy --manifest-path components/axbacktrace/Cargo.toml --all-features -- -D warnings |
✅ 通过 |
cargo clippy --manifest-path components/axcpu/Cargo.toml --all-features -- -D warnings |
✅ 通过 |
| CI: Check formatting / run_host | ✅ success |
| CI: Run sync-lint / run_container | ✅ success |
| CI: Run spin-lint / run_container | ✅ success |
| CI: Test starry aarch64 qemu / run_container | ✅ success |
| CI: 自托管开发板测试 (visionfive2, orangepi-5-plus, roc-rk3568-pc, x86_64) | ✅ success |
[patch.crates-io] 检查 |
✅ 无违规 |
axbacktrace 单元测试 |
__start_debug_str 等),与本次变更无关 |
测试覆盖
新增两个 grouped C 测试用例,位置和发现机制正确:
test-suit/starryos/qemu/system/perf-hw-sample-tid/:worker(非 leader)线程对自身开 per-task 采样事件,断言每个样本pid == process tgid、tid == worker gettid()。使用pthread_create创建多线程,正确覆盖了之前单线程测试无法暴露的 tgid/tid 区分问题。test-suit/starryos/qemu/system/perf-hw-lost/:单页环 + 突发写满 + 突发间腾空,断言PERF_RECORD_LOST记录被生成且 lost 计数 > 0。
两个测试均通过 test-suit/starryos/qemu/system/CMakeLists.txt 的 CONFIGURE_DEPENDS "*" glob 自动发现,安装到 usr/bin/starry-test-suit,CI 中 Test starry aarch64 qemu 已覆盖。
重复/重叠分析
- 搜索
repo:rcore-os/tgoskits type:pr state:open perf sample lost仅返回本 PR (#1602) 和互补的 #1601(软件事件计数器,同一作者,不同功能)。 origin/dev上不存在等价的样本归属修正或 PERF_RECORD_LOST 实现。- 本 PR 与 #1577(多核 perf 基础设施)和 #1601(软件事件)是互补的 stacked PR,各自覆盖独立的功能维度,无功能重叠。
unsafe 代码审查
新增的 unsafe 块(lost_ptr/lost_reported_ptr 的原子操作)均遵循已有代码的相同模式:
- 指针在 slot 注册期间保持有效(teardown 先 unregister 再释放)
- 使用
Relaxedordering 合理(单生产者——overflow handler per CPU) - SAFETY 注释充分
注意事项
-
stacked PR 依赖:本 PR 的 diff 包含 36 个提交,其中前 34 个来自 #1577 + 回溯图 PR 的依赖栈。PR 描述已明确说明,待 #1577 合入
dev后将 rebase 为干净的 2-提交 diff。这不是阻塞问题。 -
axbacktrace单元测试:cargo test --manifest-path components/axbacktrace/Cargo.toml --all-features因 DWARF 链接符号(__start_debug_str等)失败,这是预存问题,与本次变更无关。
结论
变更逻辑正确、测试覆盖到位、CI 全部通过、无 crates.io patch 违规。批准合入。
Powered by deepseek-v4-pro
ZR233
left a comment
There was a problem hiding this comment.
真实 (tgid, tid) 归属与 in-band LOST 的净增实现/测试未发现新的独立阻塞,但本 PR 仍把未合入的 #1577 perf 基座整栈带入 dev,current-head OrangePi 5 Plus 硬门禁失败。CI 日志在自动 board case 中直接得到 /usr/local/bin/perf-validate: not found,随后等待到 900 秒超时;这不是可忽略的外部 flaky,而是分支声明的板级验证资产没有被 staging。请先修复底层栈的 board 资产部署并 rebase 收敛后复审。
在回溯图(callchain)PR 的基础上,修正采样记录的正确性:让每个样本携带真实的用户态
(tgid, tid),并补齐 in-band 的PERF_RECORD_LOST记录。样本归属:真实用户态
(tgid, tid)此前
PERF_RECORD_SAMPLE的 pid/tid 取自 axtask 调度器 id,而 COMM/MMAP2/FORK/EXIT 边带记录用的是真实的 starry-process tgid/tid;二者不一致会破坏perf report的“样本→进程/DSO 映射”连接,导致跨进程 / 多线程误归属。(tgid, tid)捕获进SampleSlot:PMU 溢出 IRQ 可能在切换离开被监控任务之后才被处理,此时 handler 里的current()会误归属;捕获的 owner 始终是事件所属任务。系统级事件(owner_ids == None)归属到被中断的current(),与采样 IP 一致。perf-hw-sample-tid:worker(非 leader)线程对自身采样,断言每个样本pid == 进程 tgid、tid == worker gettid()。QEMU(smp4):1024/1024 样本正确。in-band
PERF_RECORD_LOST记录采样环写满时会丢样本,此前仅有只读的
PERF_FORMAT_LOST累计值。现按 Linux 语义在环有空间时向环内写入 type-2 的PERF_RECORD_LOST记录(header + 事件 id + lost 计数),使perf report/perf script显示 “LOST n events!” 并在时间线上标注缺口。lost_reported追踪已上报数量;handler 在写样本前刷出lost - lost_reported(环仍满则保持挂起,待用户态腾空后重试)。perf-hw-lost:单页环,突发写满 + 突发间腾空。QEMU(smp4):2033 样本、15 条 lost 记录、共上报 1318529 丢失。本 PR 基于回溯图 PR(
feat/perf-callchain);待 #1577 合入 dev 后一并对齐到rcore-os:dev。fmt、clippy(starry-kernel 20/20)、QEMU 均通过。