From e4a48f81fb946a3008d9ca0c984eb7bf9ffb9c9f Mon Sep 17 00:00:00 2001 From: JosephJoshua Date: Sun, 12 Jul 2026 03:55:28 +0800 Subject: [PATCH 1/2] feat(starry): implement software perf events as real counters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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 ``. 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). --- os/StarryOS/kernel/src/mm/access.rs | 4 + os/StarryOS/kernel/src/perf/mod.rs | 18 +- os/StarryOS/kernel/src/perf/sw.rs | 377 ++++++++++++++++++ os/StarryOS/kernel/src/task/mod.rs | 14 + os/StarryOS/kernel/src/task/user.rs | 3 + .../system/perf-sw-counters/CMakeLists.txt | 12 + .../perf-sw-counters/src/perf_sw_counters.c | 223 +++++++++++ 7 files changed, 649 insertions(+), 2 deletions(-) create mode 100644 os/StarryOS/kernel/src/perf/sw.rs create mode 100644 test-suit/starryos/qemu/system/perf-sw-counters/CMakeLists.txt create mode 100644 test-suit/starryos/qemu/system/perf-sw-counters/src/perf_sw_counters.c diff --git a/os/StarryOS/kernel/src/mm/access.rs b/os/StarryOS/kernel/src/mm/access.rs index ac44c527d7..7f1a132585 100644 --- a/os/StarryOS/kernel/src/mm/access.rs +++ b/os/StarryOS/kernel/src/mm/access.rs @@ -258,6 +258,10 @@ fn handle_page_fault(vaddr: VirtAddr, access_flags: MappingFlags) -> bool { return false; }; + // Count this fault for any `PERF_COUNT_SW_PAGE_FAULTS` event on the thread + // (cheap no-op when none exists). + crate::perf::sw::on_page_fault(thr); + if unlikely(!thr.is_accessing_user_memory()) { // Still try to handle kernel-mode faults on user-space addresses. // Several syscall sites (e.g. event.rs, net/io.rs, fs/lock.rs) obtain diff --git a/os/StarryOS/kernel/src/perf/mod.rs b/os/StarryOS/kernel/src/perf/mod.rs index 0fe12c48ea..a6a8d756b7 100644 --- a/os/StarryOS/kernel/src/perf/mod.rs +++ b/os/StarryOS/kernel/src/perf/mod.rs @@ -28,6 +28,11 @@ pub mod sampling; /// gated like `sampling`. #[cfg(target_arch = "aarch64")] pub mod sideband; +/// Software events (`PERF_TYPE_SOFTWARE`) as real per-task counters — the default +/// `perf stat` rows (cpu-clock / task-clock / context-switches / cpu-migrations / +/// page-faults). Pure accounting driven by the scheduler + fault hooks, no PMU, +/// so it is arch-independent. +pub mod sw; /// Per-task hardware-PMU counting (`perf stat -- cmd`, M3). ARM PMUv3 only; the /// scheduler hooks call into CPU PMU register helpers, so it is gated like /// `sampling`. @@ -64,7 +69,7 @@ pub use bpf::BpfPerfEventWrapper; use hashbrown::HashMap; use kbpf_basic::{ linux_bpf::{PERF_FLAG_FD_CLOEXEC, perf_event_attr}, - perf::{PerfEventIoc, PerfProbeArgs, PerfTypeId}, + perf::{PerfEventIoc, PerfProbeArgs, PerfProbeConfig, PerfTypeId}, }; use crate::{ @@ -527,7 +532,16 @@ pub fn perf_event_open( .into_ax_result()?; match args.type_ { PerfTypeId::PERF_TYPE_KPROBE => Box::new(kprobe::perf_event_open_kprobe(args)?), - PerfTypeId::PERF_TYPE_SOFTWARE => Box::new(bpf::perf_event_open_bpf(args)), + // The five counting software events (`perf stat`'s default rows) become + // real per-task counters; every other software config (e.g. + // `PERF_COUNT_SW_DUMMY`, `perf record`'s tracking event) keeps the + // BPF/ring path. + PerfTypeId::PERF_TYPE_SOFTWARE => match &args.config { + PerfProbeConfig::PerfSwIds(sw_id) if sw::is_counting_sw(*sw_id) => { + Box::new(sw::perf_event_open_sw(attr, *sw_id, pid)?) + } + _ => Box::new(bpf::perf_event_open_bpf(args)), + }, PerfTypeId::PERF_TYPE_TRACEPOINT => { Box::new(tracepoint::perf_event_open_tracepoint(args)?) } diff --git a/os/StarryOS/kernel/src/perf/sw.rs b/os/StarryOS/kernel/src/perf/sw.rs new file mode 100644 index 0000000000..aeebdc28b4 --- /dev/null +++ b/os/StarryOS/kernel/src/perf/sw.rs @@ -0,0 +1,377 @@ +//! Software perf events (`PERF_TYPE_SOFTWARE`) as real per-task counters. +//! +//! `perf stat -- cmd` opens its default set with no `-e`: the hardware +//! `cycles`/`instructions` plus five *software* events — `cpu-clock`, +//! `task-clock`, `context-switches`, `cpu-migrations`, `page-faults`. Those five +//! used to dispatch to the BPF stub ([`super::bpf::BpfPerfEventWrapper`]), which +//! has no readable count, so `read(perf_fd)` returned `Unsupported` and every +//! default row printed ``. This module makes them real per-task +//! counters so a bare `perf stat -- cmd` looks correct. +//! +//! Each event is a lightweight [`SwPerTaskCounter`] attached to the monitored +//! [`Thread`], driven by cheap hooks: +//! +//! * [`sched_in`] / [`sched_out`] — called from the task scope enter/leave hooks +//! (the same switch path as the hardware [`super::task::perf_sched_in`]); they +//! accrue `task-clock` on-CPU time, count `context-switches` (one per +//! deschedule) and `cpu-migrations` (a slice on a different core than the last). +//! * [`on_page_fault`] — called from the user page-fault handler; counts +//! `page-faults` for the faulting thread. +//! +//! `cpu-clock` needs no hook: it is wall-clock time while the event is enabled. +//! +//! All hooks early-out on a single relaxed load of [`PERF_SW_ACTIVE`] when no +//! software event exists anywhere, so there is no cost on the hot paths in the +//! common case (mirrors [`super::task::PERF_TASK_ACTIVE`]). Mutation is entirely +//! through atomics, so the counter is `Sync` and the hooks need no allocation. + +use alloc::sync::Arc; +use core::{ + any::Any, + sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering}, + task::Context, +}; + +use ax_errno::{AxError, AxResult}; +use axpoll::{IoEvents, Pollable}; +use kbpf_basic::linux_bpf::{perf_event_attr, perf_sw_ids}; + +use super::{PerfEventOps, PerfReadValues}; +use crate::task::{AsThread, Thread}; + +/// Number of live software counters process-wide. The scheduler + fault hooks +/// early-out when this is zero, so there is no cost on those hot paths when no +/// software perf event exists. Incremented at open, decremented when the owning +/// fd drops. +static PERF_SW_ACTIVE: AtomicUsize = AtomicUsize::new(0); + +/// Sentinel for [`SwPerTaskCounter::last_cpu`] before the first slice, so the +/// first `sched_in` does not falsely count a migration. +const CPU_UNSET: u32 = u32::MAX; + +#[inline] +fn now_ns() -> u64 { + ax_runtime::hal::time::monotonic_time_nanos() +} + +/// The five software events implemented as counters. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SwId { + /// `PERF_COUNT_SW_CPU_CLOCK`: wall-clock ns while enabled. + CpuClock, + /// `PERF_COUNT_SW_TASK_CLOCK`: ns the task actually ran while enabled. + TaskClock, + /// `PERF_COUNT_SW_PAGE_FAULTS`: user page faults taken by the task. + PageFaults, + /// `PERF_COUNT_SW_CONTEXT_SWITCHES`: times the task was descheduled. + ContextSwitches, + /// `PERF_COUNT_SW_CPU_MIGRATIONS`: times the task resumed on a new core. + CpuMigrations, +} + +impl SwId { + /// Maps the `perf_sw_ids` config to a counter kind, or `None` for software + /// ids this module does not implement (e.g. `PERF_COUNT_SW_DUMMY`, which + /// `perf record` uses for its side-band tracking event and which stays on the + /// BPF/ring path). + fn from_raw(id: perf_sw_ids) -> Option { + Some(match id { + perf_sw_ids::PERF_COUNT_SW_CPU_CLOCK => SwId::CpuClock, + perf_sw_ids::PERF_COUNT_SW_TASK_CLOCK => SwId::TaskClock, + perf_sw_ids::PERF_COUNT_SW_PAGE_FAULTS => SwId::PageFaults, + perf_sw_ids::PERF_COUNT_SW_CONTEXT_SWITCHES => SwId::ContextSwitches, + perf_sw_ids::PERF_COUNT_SW_CPU_MIGRATIONS => SwId::CpuMigrations, + _ => return None, + }) + } +} + +/// Returns `true` if `id` is a software event this module implements as a real +/// counter (so the dispatcher routes it here instead of the BPF stub). +pub fn is_counting_sw(id: perf_sw_ids) -> bool { + SwId::from_raw(id).is_some() +} + +/// One software counter bound to a specific task. +/// +/// Interior-mutable and allocation-free (atomics only) so the scheduler and +/// fault hooks can drive it from IRQ-off / hot-path context. +#[derive(Debug)] +pub struct SwPerTaskCounter { + kind: SwId, + /// `attr.read_format`, controlling which fields `read(perf_fd)` emits. + read_format: u64, + /// Userspace wants this event counting (`!disabled` at open or after + /// `ioctl(ENABLE)`). Hooks and `read` ignore a disabled counter. + enabled: AtomicBool, + /// The owning fd has closed; hooks stop touching this counter and it may be + /// reaped from the thread's list. + dead: AtomicBool, + /// Event count for the discrete events (page-faults / context-switches / + /// cpu-migrations). + count: AtomicU64, + /// Accumulated on-CPU time for `task-clock` (ns). + runtime_ns: AtomicU64, + /// Monotonic ns this task last became on-CPU with the event enabled, or `0` + /// when off-CPU; the base for the in-flight `task-clock` slice. + run_since_ns: AtomicU64, + /// Accumulated wall time the event has been enabled across past windows (ns). + time_enabled_ns: AtomicU64, + /// Monotonic ns the current enabled window opened; valid iff `enabled`. + enabled_since_ns: AtomicU64, + /// Logical CPU id of the last slice, for `cpu-migrations`. `CPU_UNSET` until + /// the first `sched_in`. + last_cpu: AtomicU32, +} + +impl SwPerTaskCounter { + fn new(kind: SwId, attr: &perf_event_attr) -> Self { + // Enable at open unless the event is opened disabled *and* not armed to + // start on exec. `perf stat -- cmd` opens with enable_on_exec; treat that + // as enable-at-open (the pre-exec window is negligible) so counts appear + // without a dedicated exec hook. + let enabled = attr.disabled() == 0 || attr.enable_on_exec() != 0; + let now = now_ns(); + Self { + kind, + read_format: attr.read_format, + enabled: AtomicBool::new(enabled), + dead: AtomicBool::new(false), + count: AtomicU64::new(0), + runtime_ns: AtomicU64::new(0), + run_since_ns: AtomicU64::new(0), + time_enabled_ns: AtomicU64::new(0), + enabled_since_ns: AtomicU64::new(if enabled { now } else { 0 }), + last_cpu: AtomicU32::new(CPU_UNSET), + } + } + + fn enable(&self) { + if !self.enabled.swap(true, Ordering::AcqRel) { + self.enabled_since_ns.store(now_ns(), Ordering::Release); + } + } + + fn disable(&self) { + if self.enabled.swap(false, Ordering::AcqRel) { + let now = now_ns(); + // Close the enabled wall-time window. + let since = self.enabled_since_ns.load(Ordering::Acquire); + self.time_enabled_ns + .fetch_add(now.saturating_sub(since), Ordering::AcqRel); + // Fold any in-flight task-clock slice (the task may be running now). + let run_since = self.run_since_ns.swap(0, Ordering::AcqRel); + if run_since != 0 { + self.runtime_ns + .fetch_add(now.saturating_sub(run_since), Ordering::AcqRel); + } + } + } + + fn reset(&self) { + self.count.store(0, Ordering::Release); + self.runtime_ns.store(0, Ordering::Release); + self.time_enabled_ns.store(0, Ordering::Release); + self.run_since_ns.store(0, Ordering::Release); + if self.enabled.load(Ordering::Acquire) { + self.enabled_since_ns.store(now_ns(), Ordering::Release); + } + } + + fn snapshot(&self) -> PerfReadValues { + let now = now_ns(); + let enabled = self.enabled.load(Ordering::Acquire); + let time_enabled = self.time_enabled_ns.load(Ordering::Acquire) + + if enabled { + now.saturating_sub(self.enabled_since_ns.load(Ordering::Acquire)) + } else { + 0 + }; + let value = match self.kind { + SwId::CpuClock => time_enabled, + SwId::TaskClock => { + let run_since = self.run_since_ns.load(Ordering::Acquire); + self.runtime_ns.load(Ordering::Acquire) + + if enabled && run_since != 0 { + now.saturating_sub(run_since) + } else { + 0 + } + } + _ => self.count.load(Ordering::Acquire), + }; + PerfReadValues { + value, + time_enabled, + // No multiplexing for software counters: running == enabled. + time_running: time_enabled, + read_format: self.read_format, + lost: 0, + } + } +} + +/// `PERF_TYPE_SOFTWARE` counting event handle returned by `perf_event_open(2)`. +#[derive(Debug)] +pub struct SwPerfEvent { + ctr: Arc, +} + +impl Drop for SwPerfEvent { + fn drop(&mut self) { + // Mark dead (hooks stop touching it) and release the global gate exactly + // once. The counter's `Arc` may linger in the thread's list until the + // next open reaps it, or until the thread exits. + if !self.ctr.dead.swap(true, Ordering::AcqRel) { + PERF_SW_ACTIVE.fetch_sub(1, Ordering::AcqRel); + } + } +} + +impl PerfEventOps for SwPerfEvent { + fn enable(&mut self) -> AxResult<()> { + self.ctr.enable(); + Ok(()) + } + + fn disable(&mut self) -> AxResult<()> { + self.ctr.disable(); + Ok(()) + } + + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn read_values(&mut self) -> AxResult { + Ok(self.ctr.snapshot()) + } + + fn reset(&mut self) -> AxResult<()> { + self.ctr.reset(); + Ok(()) + } +} + +impl Pollable for SwPerfEvent { + fn poll(&self) -> IoEvents { + // A counting event is always readable: `read(perf_fd)` returns the + // current value without blocking. + IoEvents::IN + } + + fn register(&self, _context: &mut Context<'_>, _events: IoEvents) { + // Nothing to wake: the value is always immediately available. + } +} + +/// Attach `ctr` to `thr`'s software-counter list, reaping any dead entries left +/// by closed fds, and bump the global gate. +fn attach(thr: &Thread, ctr: Arc) { + let mut list = thr.perf_sw_counters.lock(); + list.retain(|c| !c.dead.load(Ordering::Acquire)); + list.push(ctr); + PERF_SW_ACTIVE.fetch_add(1, Ordering::AcqRel); +} + +/// Open a `PERF_TYPE_SOFTWARE` counting event on the task selected by `pid` +/// (`pid > 0` a specific tid, `pid == 0` the caller). System-wide software +/// counters (`pid < 0`) are not supported. +pub fn perf_event_open_sw( + attr: &perf_event_attr, + sw_id: perf_sw_ids, + pid: i32, +) -> AxResult { + let kind = SwId::from_raw(sw_id).ok_or(AxError::Unsupported)?; + let ctr = Arc::new(SwPerTaskCounter::new(kind, attr)); + + if pid > 0 { + let task = crate::task::get_task(pid as u32)?; + let thr = task.try_as_thread().ok_or(AxError::NoSuchProcess)?; + attach(thr, ctr.clone()); + } else if pid == 0 { + let curr = ax_task::current(); + let thr = curr.try_as_thread().ok_or(AxError::NoSuchProcess)?; + attach(thr, ctr.clone()); + } else { + return Err(AxError::Unsupported); + } + + Ok(SwPerfEvent { ctr }) +} + +/// Scheduler hook: `thr` is about to start running on this CPU. Opens the +/// `task-clock` slice and counts a `cpu-migrations` event when the core changed. +pub fn sched_in(thr: &Thread) { + if PERF_SW_ACTIVE.load(Ordering::Acquire) == 0 { + return; + } + let list = thr.perf_sw_counters.lock(); + if list.is_empty() { + return; + } + let now = now_ns(); + let this_cpu = ax_hal::percpu::this_cpu_id() as u32; + for c in list.iter() { + if c.dead.load(Ordering::Acquire) || !c.enabled.load(Ordering::Acquire) { + continue; + } + match c.kind { + SwId::TaskClock => c.run_since_ns.store(now, Ordering::Release), + SwId::CpuMigrations => { + let last = c.last_cpu.swap(this_cpu, Ordering::AcqRel); + if last != CPU_UNSET && last != this_cpu { + c.count.fetch_add(1, Ordering::Relaxed); + } + } + _ => {} + } + } +} + +/// Scheduler hook: `thr` is about to stop running on this CPU. Folds the +/// `task-clock` slice and counts a `context-switches` event (one per deschedule). +pub fn sched_out(thr: &Thread) { + if PERF_SW_ACTIVE.load(Ordering::Acquire) == 0 { + return; + } + let list = thr.perf_sw_counters.lock(); + if list.is_empty() { + return; + } + let now = now_ns(); + for c in list.iter() { + if c.dead.load(Ordering::Acquire) || !c.enabled.load(Ordering::Acquire) { + continue; + } + match c.kind { + SwId::TaskClock => { + let run_since = c.run_since_ns.swap(0, Ordering::AcqRel); + if run_since != 0 { + c.runtime_ns + .fetch_add(now.saturating_sub(run_since), Ordering::AcqRel); + } + } + SwId::ContextSwitches => { + c.count.fetch_add(1, Ordering::Relaxed); + } + _ => {} + } + } +} + +/// Fault hook: `thr` just took a user page fault. Counts a `page-faults` event. +pub fn on_page_fault(thr: &Thread) { + if PERF_SW_ACTIVE.load(Ordering::Acquire) == 0 { + return; + } + let list = thr.perf_sw_counters.lock(); + for c in list.iter() { + if c.kind == SwId::PageFaults + && !c.dead.load(Ordering::Acquire) + && c.enabled.load(Ordering::Acquire) + { + c.count.fetch_add(1, Ordering::Relaxed); + } + } +} diff --git a/os/StarryOS/kernel/src/task/mod.rs b/os/StarryOS/kernel/src/task/mod.rs index d0ebb3cdfc..4386227520 100644 --- a/os/StarryOS/kernel/src/task/mod.rs +++ b/os/StarryOS/kernel/src/task/mod.rs @@ -205,6 +205,13 @@ pub struct Thread { /// where no per-task perf event targets this thread. #[cfg(target_arch = "aarch64")] pub(crate) perf_counters: SpinNoIrq>>, + + /// Per-task software perf counters (`PERF_TYPE_SOFTWARE`) attached by + /// `perf_event_open`. Driven by the scheduler + fault hooks + /// ([`crate::perf::sw::sched_in`] / `sched_out` / `on_page_fault`). Empty for + /// the common case where no software perf event targets this thread. Software + /// events are pure accounting (no PMU), so this is arch-independent. + pub(crate) perf_sw_counters: SpinNoIrq>>, } impl Thread { @@ -253,6 +260,7 @@ impl Thread { #[cfg(target_arch = "aarch64")] perf_counters: SpinNoIrq::new(Vec::new()), + perf_sw_counters: SpinNoIrq::new(Vec::new()), }) } @@ -506,6 +514,9 @@ impl TaskExt for Box { // no per-task perf event exists anywhere. #[cfg(target_arch = "aarch64")] crate::perf::task::perf_sched_in(self); + // Open the software counters' slice (task-clock / cpu-migrations); cheap + // no-op when no software perf event exists. + crate::perf::sw::sched_in(self); } fn on_leave(&self) { @@ -513,6 +524,9 @@ impl TaskExt for Box { // before the scope is torn down. Same hot-path constraints as on_enter. #[cfg(target_arch = "aarch64")] crate::perf::task::perf_sched_out(self); + // Fold the software counters' slice (task-clock) and count the deschedule + // (context-switches); cheap no-op when no software perf event exists. + crate::perf::sw::sched_out(self); ActiveScope::set_global(); unsafe { self.proc_data.scope.force_read_decrement() }; } diff --git a/os/StarryOS/kernel/src/task/user.rs b/os/StarryOS/kernel/src/task/user.rs index 836c11fe39..4e6a7c7f18 100644 --- a/os/StarryOS/kernel/src/task/user.rs +++ b/os/StarryOS/kernel/src/task/user.rs @@ -124,6 +124,9 @@ pub fn new_user_task(name: &str, mut uctx: UserContext, set_child_tid: usize) -> // addresses are counted separately in the mm page-fault handler. crate::mm::PAGE_FAULT_COUNT .fetch_add(1, core::sync::atomic::Ordering::Relaxed); + // Count it for any `PERF_COUNT_SW_PAGE_FAULTS` event on + // this thread (cheap no-op when none exists). + crate::perf::sw::on_page_fault(thr); // Classify si_code while holding the aspace lock: an // existing mapping that rejected the access is a // permission violation (SEGV_ACCERR), otherwise the diff --git a/test-suit/starryos/qemu/system/perf-sw-counters/CMakeLists.txt b/test-suit/starryos/qemu/system/perf-sw-counters/CMakeLists.txt new file mode 100644 index 0000000000..e044bd5fa6 --- /dev/null +++ b/test-suit/starryos/qemu/system/perf-sw-counters/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(perf-sw-counters C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +# Opens the five PERF_TYPE_SOFTWARE default `perf stat` rows as per-task counters, +# runs a workload that generates each, and asserts read(perf_fd) returns a real +# count (not ). Software events are pure accounting, so this is not +# arch-gated; it skips-as-pass only if perf_event_open is unwired (ENOSYS). +add_executable(perf-sw-counters src/perf_sw_counters.c) +target_compile_options(perf-sw-counters PRIVATE -Wall -Wextra -Werror -O0) +install(TARGETS perf-sw-counters RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu/system/perf-sw-counters/src/perf_sw_counters.c b/test-suit/starryos/qemu/system/perf-sw-counters/src/perf_sw_counters.c new file mode 100644 index 0000000000..3022654291 --- /dev/null +++ b/test-suit/starryos/qemu/system/perf-sw-counters/src/perf_sw_counters.c @@ -0,0 +1,223 @@ +/* + * perf_sw_counters.c -- PERF_TYPE_SOFTWARE events return real counts. + * + * `perf stat -- cmd` opens its default set with no `-e`: hardware cycles/ + * instructions plus five SOFTWARE events -- cpu-clock, task-clock, + * context-switches, cpu-migrations, page-faults. Those used to dispatch to a BPF + * stub with no readable count, so read(perf_fd) failed and every default row + * printed ``. This test opens all five as per-task counters on the + * calling thread, runs a workload that exercises each, and asserts read(perf_fd) + * succeeds with a sensible value. + * + * SUCCESS == + * every event opens AND read(perf_fd) returns 8 bytes (a real u64 count) + * AND cpu-clock / task-clock / page-faults / context-switches are all > 0 + * (the workload provably generates each). + * cpu-migrations is only required to be READABLE (>= 0): a migration cannot be + * forced deterministically under the test scheduler. + * + * If perf_event_open is not wired on this arch (ENOSYS) the test skips-as-pass. + * On success exactly one line `STARRY_PERF_SW_COUNTERS_OK` is printed. + * + * All ABI structs are defined locally (no dependency). + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifndef MADV_DONTNEED +#define MADV_DONTNEED 4 +#endif + +#define PERF_TYPE_SOFTWARE 1u + +#define PERF_COUNT_SW_CPU_CLOCK 0ull +#define PERF_COUNT_SW_TASK_CLOCK 1ull +#define PERF_COUNT_SW_PAGE_FAULTS 2ull +#define PERF_COUNT_SW_CONTEXT_SWITCHES 3ull +#define PERF_COUNT_SW_CPU_MIGRATIONS 4ull + +#ifndef PERF_EVENT_IOC_ENABLE +#define PERF_EVENT_IOC_ENABLE 0x2400u +#endif +#ifndef PERF_EVENT_IOC_DISABLE +#define PERF_EVENT_IOC_DISABLE 0x2401u +#endif +#ifndef PERF_EVENT_IOC_RESET +#define PERF_EVENT_IOC_RESET 0x2403u +#endif + +struct perf_event_attr { + uint32_t type; + uint32_t size; + uint64_t config; + union { + uint64_t sample_period; + uint64_t sample_freq; + }; + uint64_t sample_type; + uint64_t read_format; + uint64_t flags; + union { + uint32_t wakeup_events; + uint32_t wakeup_watermark; + }; + uint32_t bp_type; + union { + uint64_t bp_addr; + uint64_t config1; + }; + union { + uint64_t bp_len; + uint64_t config2; + }; + uint64_t branch_sample_type; + uint64_t sample_regs_user; + uint32_t sample_stack_user; + int32_t clockid; + uint64_t sample_regs_intr; + uint32_t aux_watermark; + uint16_t sample_max_stack; + uint16_t __reserved_2; + uint32_t aux_sample_size; + uint32_t __reserved_3; +}; + +#define PERF_ATTR_FLAG_DISABLED (1ull << 0) + +#ifndef SYS_perf_event_open +#define SYS_perf_event_open 241 +#endif + +static long perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, + int group_fd, unsigned long flags) { + return syscall(SYS_perf_event_open, attr, pid, cpu, group_fd, flags); +} + +static int open_sw(uint64_t config) { + struct perf_event_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.type = PERF_TYPE_SOFTWARE; + attr.size = (uint32_t)sizeof(attr); + attr.config = config; + attr.read_format = 0; /* read() returns just the u64 value */ + attr.flags = PERF_ATTR_FLAG_DISABLED; + /* pid = 0 => this thread; cpu = -1 => any cpu (per-task). */ + long fd = perf_event_open(&attr, 0, -1, -1, 0ul); + return (int)fd; +} + +/* Workload that provably generates each software event. */ +static volatile uint64_t g_sink; +static void workload(void) { + /* task-clock / cpu-clock: burn some CPU (time-based, so TCG cycle + * undercounting does not matter). */ + for (uint64_t i = 0; i < 20000000ull; i++) { + g_sink += i * 2654435761ull + 1ull; + } + /* page-faults: fault in anonymous pages. Touch once (populates whether the + * kernel maps lazily or eagerly), then MADV_DONTNEED to drop the pages and + * re-touch -- the re-touch is a guaranteed demand-zero fault regardless of + * the mmap population policy. */ + const size_t pages = 256; + const size_t len = pages * 4096; + void *p = mmap(NULL, len, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, + -1, 0); + if (p != MAP_FAILED) { + volatile uint8_t *b = (volatile uint8_t *)p; + for (size_t i = 0; i < pages; i++) { + b[i * 4096] = (uint8_t)i; + } + (void)madvise(p, len, MADV_DONTNEED); + for (size_t i = 0; i < pages; i++) { + b[i * 4096] = (uint8_t)(i + 1); + } + (void)munmap(p, len); + } + /* context-switches: block repeatedly so the scheduler deschedules us. */ + for (int i = 0; i < 20; i++) { + struct timespec ts = {0, 1000000}; /* 1 ms */ + (void)nanosleep(&ts, NULL); + } +} + +struct sw_event { + const char *name; + uint64_t config; + int require_positive; +}; + +int main(void) { + struct sw_event events[] = { + {"cpu-clock", PERF_COUNT_SW_CPU_CLOCK, 1}, + {"task-clock", PERF_COUNT_SW_TASK_CLOCK, 1}, + {"page-faults", PERF_COUNT_SW_PAGE_FAULTS, 1}, + {"context-switches", PERF_COUNT_SW_CONTEXT_SWITCHES, 1}, + {"cpu-migrations", PERF_COUNT_SW_CPU_MIGRATIONS, 0}, + }; + const size_t n = sizeof(events) / sizeof(events[0]); + int fds[8]; + + for (size_t i = 0; i < n; i++) { + fds[i] = open_sw(events[i].config); + if (fds[i] < 0) { + /* Not wired on this arch (ENOSYS) -> skip-as-pass. Any other errno on + * the FIRST event is a real failure. */ + if (errno == ENOSYS && i == 0) { + printf("STARRY_PERF_SW_COUNTERS skip: perf_event_open ENOSYS\n"); + printf("STARRY_PERF_SW_COUNTERS_OK\n"); + return 0; + } + printf("perf-sw-counters FAILED: open %s errno=%d\n", events[i].name, + errno); + return 1; + } + } + + for (size_t i = 0; i < n; i++) { + (void)ioctl(fds[i], PERF_EVENT_IOC_RESET, 0); + (void)ioctl(fds[i], PERF_EVENT_IOC_ENABLE, 0); + } + workload(); + for (size_t i = 0; i < n; i++) { + (void)ioctl(fds[i], PERF_EVENT_IOC_DISABLE, 0); + } + + int rc = 0; + for (size_t i = 0; i < n; i++) { + uint64_t val = 0; + ssize_t got = read(fds[i], &val, sizeof(val)); + if (got != (ssize_t)sizeof(val)) { + printf("perf-sw-counters FAILED: read %s got=%zd errno=%d (counter " + "not readable -- still )\n", + events[i].name, got, errno); + rc = 1; + } else { + printf("STARRY_PERF_SW_COUNTERS %s=%llu\n", events[i].name, + (unsigned long long)val); + if (events[i].require_positive && val == 0) { + printf("perf-sw-counters FAILED: %s counted 0 (workload should " + "have generated events)\n", + events[i].name); + rc = 1; + } + } + close(fds[i]); + } + + if (rc == 0) { + printf("STARRY_PERF_SW_COUNTERS_OK\n"); + } + return rc; +} From 2ba5dcb02bb75fbbc76bfae3df507e16d515e82f Mon Sep 17 00:00:00 2001 From: JosephJoshua Date: Sun, 12 Jul 2026 04:17:53 +0800 Subject: [PATCH 2/2] feat(perf): add PERF_TYPE_HW_CACHE combinatorial cache events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- components/axcpu/src/aarch64/pmu.rs | 46 +++++ os/StarryOS/kernel/src/perf/hw.rs | 13 ++ os/StarryOS/kernel/src/perf/mod.rs | 1 + .../qemu/system/perf-hw-cache/CMakeLists.txt | 12 ++ .../system/perf-hw-cache/src/perf_hw_cache.c | 178 ++++++++++++++++++ 5 files changed, 250 insertions(+) create mode 100644 test-suit/starryos/qemu/system/perf-hw-cache/CMakeLists.txt create mode 100644 test-suit/starryos/qemu/system/perf-hw-cache/src/perf_hw_cache.c diff --git a/components/axcpu/src/aarch64/pmu.rs b/components/axcpu/src/aarch64/pmu.rs index c7ade89be6..cdc6ba256e 100644 --- a/components/axcpu/src/aarch64/pmu.rs +++ b/components/axcpu/src/aarch64/pmu.rs @@ -343,6 +343,52 @@ pub fn hw_event_to_arm(hw_id: u32) -> Option { } } +/// Maps a `PERF_TYPE_HW_CACHE` config to an ARM PMUv3 event number, or `None` +/// for a combination ARM PMUv3 does not define (matching Linux's +/// `CACHE_OP_UNSUPPORTED`, which rejects the event at open). +/// +/// The config packs `cache_id | (op << 8) | (result << 16)`: `cache_id` is +/// L1D(0)/L1I(1)/LL(2)/DTLB(3)/ITLB(4)/BPU(5)/NODE(6), `op` is READ(0)/WRITE(1)/ +/// PREFETCH(2), `result` is ACCESS(0)/MISS(1). ARM's basic cache events do not +/// split read vs write, so both map to the same event; PREFETCH and NODE have no +/// architectural counterpart and are rejected. +pub fn hw_cache_to_arm(config: u64) -> Option { + const READ: u8 = 0; + const WRITE: u8 = 1; + const ACCESS: u8 = 0; + const MISS: u8 = 1; + + let cache_id = (config & 0xff) as u8; + let op = ((config >> 8) & 0xff) as u8; + let result = ((config >> 16) & 0xff) as u8; + + Some(match (cache_id, op, result) { + // L1D: L1D_CACHE / L1D_CACHE_REFILL. + (0, READ | WRITE, ACCESS) => 0x04, + (0, READ | WRITE, MISS) => 0x03, + // L1I: L1I_CACHE / L1I_CACHE_REFILL. + (1, READ, ACCESS) => 0x14, + (1, READ, MISS) => 0x01, + // LL (last level): LL_CACHE_RD / LL_CACHE_MISS_RD for reads, else + // LL_CACHE / LL_CACHE_MISS. + (2, READ, ACCESS) => 0x36, + (2, READ, MISS) => 0x37, + (2, WRITE, ACCESS) => 0x32, + (2, WRITE, MISS) => 0x33, + // DTLB: L1D_TLB / L1D_TLB_REFILL. + (3, READ | WRITE, ACCESS) => 0x25, + (3, READ | WRITE, MISS) => 0x05, + // ITLB: L1I_TLB / L1I_TLB_REFILL. + (4, READ, ACCESS) => 0x26, + (4, READ, MISS) => 0x02, + // BPU (branch prediction): BR_PRED / BR_MIS_PRED. + (5, READ | WRITE, ACCESS) => 0x12, + (5, READ | WRITE, MISS) => 0x10, + // PREFETCH ops, NODE cache, and every other combination are unsupported. + _ => return None, + }) +} + /// The generic programmable event counters (`PMEVCNTRn_EL0` / `PMEVTYPERn_EL0`). /// /// `n` is the logical counter index in `0..num_counters` (from diff --git a/os/StarryOS/kernel/src/perf/hw.rs b/os/StarryOS/kernel/src/perf/hw.rs index 5617db143f..625ca7ab79 100644 --- a/os/StarryOS/kernel/src/perf/hw.rs +++ b/os/StarryOS/kernel/src/perf/hw.rs @@ -134,6 +134,8 @@ fn resolve_hw_event(hw_id: u32) -> Option { fn decode_arm_event(type_: u32, config: u64) -> Option { if type_ == perf_type_id::PERF_TYPE_HARDWARE as u32 { resolve_hw_event(config as u32) + } else if type_ == perf_type_id::PERF_TYPE_HW_CACHE as u32 { + ax_cpu::pmu::hw_cache_to_arm(config) } else if type_ == perf_type_id::PERF_TYPE_RAW as u32 || type_ == ARMV8_PMUV3_PERF_TYPE || type_ == ARMV8_CORTEX_A55_TYPE @@ -1413,6 +1415,17 @@ pub fn perf_event_open_hw(attr: &perf_event_attr, pid: i32, cpu: i32) -> AxResul }; alloc_programmable(event, exclude_user, exclude_kernel)? } + } else if attr.type_ == perf_type_id::PERF_TYPE_HW_CACHE as u32 { + // Combinatorial cache events (`perf stat -e L1-dcache-load-misses` etc.): + // decode `config` (cache_id | op<<8 | result<<16) to an ARM PMUv3 event. + let Some(event) = ax_cpu::pmu::hw_cache_to_arm(attr.config) else { + warn!( + "perf_event_open: unsupported HW_CACHE config {:#x}", + attr.config + ); + return Err(AxError::Unsupported); + }; + alloc_programmable(event, exclude_user, exclude_kernel)? } else if attr.type_ == perf_type_id::PERF_TYPE_RAW as u32 || attr.type_ == ARMV8_PMUV3_PERF_TYPE || attr.type_ == ARMV8_CORTEX_A55_TYPE diff --git a/os/StarryOS/kernel/src/perf/mod.rs b/os/StarryOS/kernel/src/perf/mod.rs index a6a8d756b7..176447bc0d 100644 --- a/os/StarryOS/kernel/src/perf/mod.rs +++ b/os/StarryOS/kernel/src/perf/mod.rs @@ -514,6 +514,7 @@ pub fn perf_event_open( // `PerfProbeArgs::try_from_perf_attr`, which maps any non-probe type through // `perf_sw_ids` and rejects hardware configs with `EINVAL`. let event: Box = if attr.type_ == PerfTypeId::PERF_TYPE_HARDWARE as u32 + || attr.type_ == PerfTypeId::PERF_TYPE_HW_CACHE as u32 || attr.type_ == PerfTypeId::PERF_TYPE_RAW as u32 || attr.type_ == hw::ARMV8_PMUV3_PERF_TYPE || attr.type_ == hw::ARMV8_CORTEX_A55_TYPE diff --git a/test-suit/starryos/qemu/system/perf-hw-cache/CMakeLists.txt b/test-suit/starryos/qemu/system/perf-hw-cache/CMakeLists.txt new file mode 100644 index 0000000000..2225251c9b --- /dev/null +++ b/test-suit/starryos/qemu/system/perf-hw-cache/CMakeLists.txt @@ -0,0 +1,12 @@ +cmake_minimum_required(VERSION 3.20) +project(perf-hw-cache C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +# Opens the common PERF_TYPE_HW_CACHE combinations (L1-dcache-load-misses, LLC-*, +# dTLB/iTLB-misses, branch-misses) and asserts each opens + reads, while an +# unsupported PREFETCH combination is rejected. Hardware PMU (ARM PMUv3), so +# main() skips-as-pass off aarch64. +add_executable(perf-hw-cache src/perf_hw_cache.c) +target_compile_options(perf-hw-cache PRIVATE -Wall -Wextra -Werror -O0) +install(TARGETS perf-hw-cache RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu/system/perf-hw-cache/src/perf_hw_cache.c b/test-suit/starryos/qemu/system/perf-hw-cache/src/perf_hw_cache.c new file mode 100644 index 0000000000..287a05ac05 --- /dev/null +++ b/test-suit/starryos/qemu/system/perf-hw-cache/src/perf_hw_cache.c @@ -0,0 +1,178 @@ +/* + * perf_hw_cache.c -- PERF_TYPE_HW_CACHE combinatorial cache events. + * + * `perf stat -e L1-dcache-load-misses,LLC-loads,...` opens PERF_TYPE_HW_CACHE + * events whose config packs cache_id | (op<<8) | (result<<16). These used to be + * rejected at open (only PERF_TYPE_HARDWARE cache-references/misses worked). This + * test opens the common, ARM-defined combinations and asserts each opens and is + * readable, and that an unsupported combination (an L1D PREFETCH, which ARM + * PMUv3 has no event for) is correctly rejected. + * + * CI note: QEMU-TCG's PMU implements no cache events, so on QEMU every mapped + * event is rejected at open with ENOSYS/EOPNOTSUPP by the `event_supported` gate + * -- that is a QEMU limitation, not a mapping error. This test therefore accepts, + * for each well-defined combination, EITHER a successful open+read (real hardware + * where the event exists) OR a clean unsupported-event rejection (QEMU). Count + * accuracy is validated on the board. What it hard-asserts everywhere: + * - a mapped combination never fails with a routing error (e.g. EINVAL) or a + * crash: it is either accepted or cleanly reported unsupported; + * - an UNSUPPORTED combination (an L1D PREFETCH -- ARM PMUv3 has no such event) + * is rejected at open. + * On success exactly one line `STARRY_PERF_HW_CACHE_OK` is printed. + * + * aarch64-only (ARM PMUv3); skips-as-pass elsewhere. + */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include +#include +#include +#include +#include +#include +#include + +#define PERF_TYPE_HW_CACHE 3u + +/* config = cache_id | (op << 8) | (result << 16). */ +#define C_L1D 0ull +#define C_L1I 1ull +#define C_LL 2ull +#define C_DTLB 3ull +#define C_ITLB 4ull +#define C_BPU 5ull +#define OP_READ 0ull +#define OP_WRITE 1ull +#define OP_PREFETCH 2ull +#define RES_ACCESS 0ull +#define RES_MISS 1ull +#define CACHE_CFG(id, op, res) ((id) | ((op) << 8) | ((res) << 16)) + +struct perf_event_attr { + uint32_t type; + uint32_t size; + uint64_t config; + union { + uint64_t sample_period; + uint64_t sample_freq; + }; + uint64_t sample_type; + uint64_t read_format; + uint64_t flags; + union { + uint32_t wakeup_events; + uint32_t wakeup_watermark; + }; + uint32_t bp_type; + union { + uint64_t bp_addr; + uint64_t config1; + }; + union { + uint64_t bp_len; + uint64_t config2; + }; + uint64_t branch_sample_type; + uint64_t sample_regs_user; + uint32_t sample_stack_user; + int32_t clockid; + uint64_t sample_regs_intr; + uint32_t aux_watermark; + uint16_t sample_max_stack; + uint16_t __reserved_2; + uint32_t aux_sample_size; + uint32_t __reserved_3; +}; + +#ifndef SYS_perf_event_open +#define SYS_perf_event_open 241 +#endif + +static long perf_event_open(struct perf_event_attr *attr, pid_t pid, int cpu, + int group_fd, unsigned long flags) { + return syscall(SYS_perf_event_open, attr, pid, cpu, group_fd, flags); +} + +static int open_cache(uint64_t config) { + struct perf_event_attr attr; + memset(&attr, 0, sizeof(attr)); + attr.type = PERF_TYPE_HW_CACHE; + attr.size = (uint32_t)sizeof(attr); + attr.config = config; + return (int)perf_event_open(&attr, 0, -1, -1, 0ul); +} + +int main(void) { +#if !defined(__aarch64__) + printf("STARRY_PERF_HW_CACHE_OK\n"); + return 0; +#endif + struct cache_event { + const char *name; + uint64_t config; + } supported[] = { + {"L1-dcache-loads", CACHE_CFG(C_L1D, OP_READ, RES_ACCESS)}, + {"L1-dcache-load-misses", CACHE_CFG(C_L1D, OP_READ, RES_MISS)}, + {"L1-icache-loads", CACHE_CFG(C_L1I, OP_READ, RES_ACCESS)}, + {"LLC-loads", CACHE_CFG(C_LL, OP_READ, RES_ACCESS)}, + {"LLC-load-misses", CACHE_CFG(C_LL, OP_READ, RES_MISS)}, + {"dTLB-load-misses", CACHE_CFG(C_DTLB, OP_READ, RES_MISS)}, + {"iTLB-load-misses", CACHE_CFG(C_ITLB, OP_READ, RES_MISS)}, + {"branch-misses", CACHE_CFG(C_BPU, OP_READ, RES_MISS)}, + }; + const size_t ns = sizeof(supported) / sizeof(supported[0]); + + int rc = 0; + unsigned opened = 0, unsupported = 0; + for (size_t i = 0; i < ns; i++) { + int fd = open_cache(supported[i].config); + if (fd < 0) { + /* Accept a clean unsupported-event rejection (QEMU-TCG has no cache + * events); flag any other errno as a routing error. */ + if (errno == ENOSYS || errno == EOPNOTSUPP) { + unsupported++; + printf("STARRY_PERF_HW_CACHE %s unsupported-on-this-pmu errno=%d\n", + supported[i].name, errno); + } else { + printf("perf-hw-cache FAILED: %s open errno=%d (routing error, " + "expected open or unsupported)\n", + supported[i].name, errno); + rc = 1; + } + continue; + } + uint64_t val = 0; + ssize_t got = read(fd, &val, sizeof(val)); + if (got != (ssize_t)sizeof(val)) { + printf("perf-hw-cache FAILED: %s not readable got=%zd errno=%d\n", + supported[i].name, got, errno); + rc = 1; + } else { + opened++; + printf("STARRY_PERF_HW_CACHE %s=%llu\n", supported[i].name, + (unsigned long long)val); + } + close(fd); + } + printf("STARRY_PERF_HW_CACHE summary opened=%u unsupported=%u\n", opened, + unsupported); + + /* An L1D PREFETCH has no ARM PMUv3 event -> must be rejected. */ + int bad = open_cache(CACHE_CFG(C_L1D, OP_PREFETCH, RES_ACCESS)); + if (bad >= 0) { + printf("perf-hw-cache FAILED: unsupported L1D-prefetch opened (should be " + "rejected)\n"); + close(bad); + rc = 1; + } else { + printf("STARRY_PERF_HW_CACHE L1D-prefetch correctly rejected errno=%d\n", + errno); + } + + if (rc == 0) { + printf("STARRY_PERF_HW_CACHE_OK\n"); + } + return rc; +}