Skip to content

feat(perf): correct sample attribution and in-band lost records#1602

Open
JosephJoshua wants to merge 36 commits into
rcore-os:devfrom
oscomp-posad:feat/perf-record-correctness
Open

feat(perf): correct sample attribution and in-band lost records#1602
JosephJoshua wants to merge 36 commits into
rcore-os:devfrom
oscomp-posad:feat/perf-record-correctness

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 的基础上,修正采样记录的正确性:让每个样本携带真实的用户态 (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 映射”连接,导致跨进程 / 多线程误归属。

  • 对于 per-task 事件,在切片装载(slice-arm)时把 owner (tgid, tid) 捕获进 SampleSlot:PMU 溢出 IRQ 可能在切换离开被监控任务之后才被处理,此时 handler 里的 current() 会误归属;捕获的 owner 始终是事件所属任务。系统级事件(owner_ids == None)归属到被中断的 current(),与采样 IP 一致。
  • 新增 perf-hw-sample-tid:worker(非 leader)线程对自身采样,断言每个样本 pid == 进程 tgidtid == 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 均通过。

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.
Copilot AI review requested due to automatic review settings July 14, 2026 06:22

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 在回溯图(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 序列化,与 Linux perf_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 单元测试 ⚠️ 预存 DWARF 链接符号问题(__start_debug_str 等),与本次变更无关

测试覆盖

新增两个 grouped C 测试用例,位置和发现机制正确:

  • test-suit/starryos/qemu/system/perf-hw-sample-tid/:worker(非 leader)线程对自身开 per-task 采样事件,断言每个样本 pid == process tgidtid == 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.txtCONFIGURE_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 再释放)
  • 使用 Relaxed ordering 合理(单生产者——overflow handler per CPU)
  • SAFETY 注释充分

注意事项

  1. stacked PR 依赖:本 PR 的 diff 包含 36 个提交,其中前 34 个来自 #1577 + 回溯图 PR 的依赖栈。PR 描述已明确说明,待 #1577 合入 dev 后将 rebase 为干净的 2-提交 diff。这不是阻塞问题。

  2. 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 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.

真实 (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 收敛后复审。

@ZR233
ZR233 requested a review from shilei-massclouds July 15, 2026 01:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants