use alloc::{string::String, sync::Arc, vec::Vec};
use core::{
any::Any,
sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering},
};
use ax_alloc::GlobalPage;
use ax_kspin::SpinNoIrq;
use ax_runtime::hal::paging::MappingFlags;
use ax_task::IrqNotify;
use super::{
hw,
sampling::{self, SampleSlot},
sideband::{self, Mmap2Info, SidebandTarget},
};
use crate::task::Thread;
const PROT_READ: u32 = 1;
const PROT_WRITE: u32 = 2;
const PROT_EXEC: u32 = 4;
const MAP_SHARED: u32 = 1;
const MAP_PRIVATE: u32 = 2;
static PERF_TASK_ACTIVE: AtomicUsize = AtomicUsize::new(0);
#[derive(Debug)]
pub struct PerTaskCounter {
n: usize,
event: u16,
exclude_user: bool,
exclude_kernel: bool,
read_format: u64,
enable_on_exec: bool,
enabled: AtomicBool,
running: AtomicBool,
accumulated: AtomicU64,
time_enabled_ns: AtomicU64,
time_running_ns: AtomicU64,
last_in_ns: AtomicU64,
enabled_at_ns: AtomicU64,
dead: AtomicBool,
hw_freed: AtomicBool,
is_sampling: bool,
sample_period: u32,
sample_type: u64,
freq: bool,
freq_target: u32,
sample_id: AtomicU64,
want_comm: bool,
want_mmap2: bool,
want_task: bool,
sample_id_all: bool,
inherit: bool,
ring_vaddr: AtomicUsize,
ring_len: AtomicUsize,
notify_ptr: AtomicUsize,
anchors: SpinNoIrq<Option<SamplingAnchors>>,
redirect_anchor: SpinNoIrq<Option<Arc<dyn Any + Send + Sync>>>,
}
struct SamplingAnchors {
ring_pages: Arc<GlobalPage>,
notify: Arc<IrqNotify>,
poll_ready: Arc<axpoll::PollSet>,
poll_alive: Arc<AtomicBool>,
}
impl core::fmt::Debug for SamplingAnchors {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SamplingAnchors").finish_non_exhaustive()
}
}
pub struct PerTaskConfig {
pub n: usize,
pub event: u16,
pub exclude_user: bool,
pub exclude_kernel: bool,
pub read_format: u64,
pub enabled: bool,
pub enable_on_exec: bool,
pub sample_period: u32,
pub sample_type: u64,
pub freq: bool,
pub target_freq: u32,
pub want_comm: bool,
pub want_mmap2: bool,
pub want_task: bool,
pub sample_id_all: bool,
pub inherit: bool,
}
impl PerTaskCounter {
pub fn new(cfg: PerTaskConfig) -> Self {
PerTaskCounter {
n: cfg.n,
event: cfg.event,
exclude_user: cfg.exclude_user,
exclude_kernel: cfg.exclude_kernel,
read_format: cfg.read_format,
enable_on_exec: cfg.enable_on_exec,
enabled: AtomicBool::new(cfg.enabled),
running: AtomicBool::new(false),
accumulated: AtomicU64::new(0),
time_enabled_ns: AtomicU64::new(0),
time_running_ns: AtomicU64::new(0),
last_in_ns: AtomicU64::new(0),
enabled_at_ns: AtomicU64::new(0),
dead: AtomicBool::new(false),
hw_freed: AtomicBool::new(false),
is_sampling: cfg.sample_period > 0,
sample_period: cfg.sample_period,
sample_type: cfg.sample_type,
freq: cfg.freq,
freq_target: cfg.target_freq,
sample_id: AtomicU64::new(0),
want_comm: cfg.want_comm,
want_mmap2: cfg.want_mmap2,
want_task: cfg.want_task,
sample_id_all: cfg.sample_id_all,
inherit: cfg.inherit,
ring_vaddr: AtomicUsize::new(0),
ring_len: AtomicUsize::new(0),
notify_ptr: AtomicUsize::new(0),
anchors: SpinNoIrq::new(None),
redirect_anchor: SpinNoIrq::new(None),
}
}
pub fn read_format(&self) -> u64 {
self.read_format
}
pub fn set_sample_id(&self, id: u64) {
self.sample_id.store(id, Ordering::Relaxed);
}
pub fn set_enabled(&self) {
if !self.enabled.swap(true, Ordering::AcqRel) {
self.enabled_at_ns.store(now_ns(), Ordering::Relaxed);
}
}
pub fn set_disabled(&self) {
self.enabled.store(false, Ordering::Release);
}
pub fn reset(&self) {
self.accumulated.store(0, Ordering::Release);
}
pub fn is_sampling(&self) -> bool {
self.is_sampling
}
pub fn set_ring(
&self,
ring_pages: Arc<GlobalPage>,
ring_vaddr: usize,
ring_len: usize,
notify: Arc<IrqNotify>,
poll_ready: Arc<axpoll::PollSet>,
poll_alive: Arc<AtomicBool>,
) {
let notify_ptr = Arc::as_ptr(¬ify) as usize;
*self.anchors.lock() = Some(SamplingAnchors {
ring_pages,
notify,
poll_ready,
poll_alive,
});
self.notify_ptr.store(notify_ptr, Ordering::Release);
self.ring_len.store(ring_len, Ordering::Release);
self.ring_vaddr.store(ring_vaddr, Ordering::Release);
}
pub fn ring_mapped(&self) -> bool {
self.ring_vaddr.load(Ordering::Acquire) != 0
}
pub fn output_ring(&self) -> Option<(usize, usize, Arc<dyn Any + Send + Sync>)> {
let vaddr = self.ring_vaddr.load(Ordering::Acquire);
if vaddr == 0 {
return None;
}
let len = self.ring_len.load(Ordering::Acquire);
let guard = self.anchors.lock();
let anchors = guard.as_ref()?;
let pages: Arc<dyn Any + Send + Sync> = anchors.ring_pages.clone();
Some((vaddr, len, pages))
}
pub fn inherit_ring(&self) -> Option<(usize, usize, Arc<dyn Any + Send + Sync>)> {
let vaddr = self.ring_vaddr.load(Ordering::Acquire);
if vaddr == 0 {
return None;
}
let len = self.ring_len.load(Ordering::Acquire);
if let Some(anchors) = self.anchors.lock().as_ref() {
let pages: Arc<dyn Any + Send + Sync> = anchors.ring_pages.clone();
return Some((vaddr, len, pages));
}
let anchor = self.redirect_anchor.lock().as_ref()?.clone();
Some((vaddr, len, anchor))
}
pub fn set_redirect_ring(
&self,
ring_vaddr: usize,
ring_len: usize,
anchor: Arc<dyn Any + Send + Sync>,
) {
*self.redirect_anchor.lock() = Some(anchor);
self.notify_ptr.store(0, Ordering::Release);
self.ring_len.store(ring_len, Ordering::Release);
self.ring_vaddr.store(ring_vaddr, Ordering::Release);
}
pub fn ring_has_data(&self) -> bool {
let vaddr = self.ring_vaddr.load(Ordering::Acquire);
if vaddr == 0 {
return false;
}
let guard = self.anchors.lock();
if guard.is_none() {
return false;
}
let header = vaddr as *const kbpf_basic::linux_bpf::perf_event_mmap_page;
let (head, tail) = unsafe {
(
core::ptr::addr_of!((*header).data_head).read_volatile(),
core::ptr::addr_of!((*header).data_tail).read_volatile(),
)
};
head != tail
}
pub fn register_poll(&self, context: &mut core::task::Context<'_>) {
let guard = self.anchors.lock();
if let Some(anchors) = guard.as_ref() {
unsafe {
anchors
.poll_ready
.register(context.waker(), axpoll::IoEvents::IN)
};
}
}
}
#[inline]
fn now_ns() -> u64 {
ax_runtime::hal::time::monotonic_time_nanos()
}
pub fn attach(thr: &Thread, ptc: Arc<PerTaskCounter>) {
thr.perf_counters.lock().push(ptc);
PERF_TASK_ACTIVE.fetch_add(1, Ordering::AcqRel);
}
pub fn perf_sched_in(thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let counters = thr.perf_counters.lock();
if counters.is_empty() {
return;
}
let now = now_ns();
for ptc in counters.iter() {
if ptc.dead.load(Ordering::Acquire) {
continue;
}
if !ptc.enabled.load(Ordering::Acquire) {
continue;
}
if ptc.running.load(Ordering::Acquire) {
continue;
}
if ptc.is_sampling {
if !ptc.ring_mapped() {
continue;
}
sampling::ensure_pmu_irq_registered();
ax_cpu::pmu::counter::configure(ptc.n, ptc.event, ptc.exclude_user, ptc.exclude_kernel);
ax_cpu::pmu::counter::preload(ptc.n, ptc.sample_period);
sampling::register(
ptc.n,
SampleSlot {
ring_vaddr: ptc.ring_vaddr.load(Ordering::Acquire),
ring_len: ptc.ring_len.load(Ordering::Acquire),
period: ptc.sample_period,
sample_type: ptc.sample_type,
id: ptc.sample_id.load(Ordering::Relaxed),
notify: ptc.notify_ptr.load(Ordering::Acquire) as *const (),
freq: ptc.freq,
target_freq: ptc.freq_target,
last_time: 0,
},
);
ax_cpu::pmu::overflow::enable_irq(ptc.n);
ax_cpu::pmu::counter::enable(ptc.n);
} else {
ax_cpu::pmu::counter::configure(ptc.n, ptc.event, ptc.exclude_user, ptc.exclude_kernel);
ax_cpu::pmu::counter::enable(ptc.n);
}
ptc.last_in_ns.store(now, Ordering::Release);
ptc.running.store(true, Ordering::Release);
}
}
pub fn perf_sched_out(thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let counters = thr.perf_counters.lock();
if counters.is_empty() {
return;
}
let now = now_ns();
for ptc in counters.iter() {
if ptc.dead.load(Ordering::Acquire) {
continue;
}
if !ptc.running.load(Ordering::Acquire) {
continue;
}
if ptc.is_sampling {
ax_cpu::pmu::counter::disable(ptc.n);
ax_cpu::pmu::overflow::disable_irq(ptc.n);
sampling::unregister(ptc.n);
} else {
let delta = ax_cpu::pmu::counter::read(ptc.n);
ptc.accumulated.fetch_add(delta, Ordering::AcqRel);
ax_cpu::pmu::counter::disable(ptc.n);
}
ptc.running.store(false, Ordering::Release);
let last_in = ptc.last_in_ns.load(Ordering::Acquire);
let dt = now.saturating_sub(last_in);
ptc.time_enabled_ns.fetch_add(dt, Ordering::AcqRel);
ptc.time_running_ns.fetch_add(dt, Ordering::AcqRel);
}
}
pub fn on_exec(thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let now = now_ns();
{
let counters = thr.perf_counters.lock();
for ptc in counters.iter() {
if ptc.dead.load(Ordering::Acquire) {
continue;
}
if ptc.enable_on_exec && !ptc.enabled.swap(true, Ordering::AcqRel) {
ptc.enabled_at_ns.store(now, Ordering::Release);
}
}
}
perf_sched_in(thr);
}
fn sideband_target(ptc: &PerTaskCounter, pid: u32, tid: u32) -> Option<SidebandTarget> {
let ring_vaddr = ptc.ring_vaddr.load(Ordering::Acquire);
if ring_vaddr == 0 || !(ptc.want_comm || ptc.want_mmap2 || ptc.want_task) {
return None;
}
Some(SidebandTarget {
ring_vaddr,
ring_len: ptc.ring_len.load(Ordering::Acquire),
sample_type: ptc.sample_type,
sample_id_all: ptc.sample_id_all,
id: ptc.sample_id.load(Ordering::Relaxed),
pid,
tid,
})
}
fn collect_exec_maps(thr: &Thread) -> Vec<Mmap2Info> {
let aspace = thr.proc_data.aspace();
let mm = aspace.lock();
let mut maps = Vec::new();
for area in mm.areas() {
let flags = area.flags();
if !flags.contains(MappingFlags::EXECUTE) {
continue;
}
let Ok(fi) = area.backend().file_info() else {
continue;
};
let mut prot = 0u32;
if flags.contains(MappingFlags::READ) {
prot |= PROT_READ;
}
if flags.contains(MappingFlags::WRITE) {
prot |= PROT_WRITE;
}
prot |= PROT_EXEC;
maps.push(Mmap2Info {
addr: area.start().as_usize() as u64,
len: (area.end().as_usize() - area.start().as_usize()) as u64,
pgoff: fi.offset.unwrap_or(0),
maj: 0,
min: 0,
ino: fi.inode.unwrap_or(0),
prot,
flags: if fi.shared { MAP_SHARED } else { MAP_PRIVATE },
filename: fi.path,
});
}
maps
}
pub fn on_exec_sideband(thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let pid = thr.proc_data.proc.pid() as u32;
let tid = thr.tid() as u32;
struct WantTarget {
target: SidebandTarget,
comm: bool,
mmap2: bool,
}
let targets: Vec<WantTarget> = {
let counters = thr.perf_counters.lock();
counters
.iter()
.filter_map(|ptc| {
sideband_target(ptc, pid, tid).map(|target| WantTarget {
target,
comm: ptc.want_comm,
mmap2: ptc.want_mmap2,
})
})
.collect()
};
if targets.is_empty() {
return;
}
let curr = ax_task::current();
let name = curr.name();
for wt in &targets {
if wt.comm {
sideband::emit_comm(&wt.target, &name, true);
}
}
if targets.iter().any(|wt| wt.mmap2) {
let maps = collect_exec_maps(thr);
for wt in &targets {
if wt.mmap2 {
for m in &maps {
sideband::emit_mmap2(&wt.target, m);
}
}
}
}
}
pub fn on_mmap_sideband(
thr: &Thread,
addr: usize,
len: usize,
pgoff: usize,
prot: u32,
shared: bool,
filename: &str,
) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let pid = thr.proc_data.proc.pid() as u32;
let tid = thr.tid() as u32;
let targets: Vec<SidebandTarget> = {
let counters = thr.perf_counters.lock();
counters
.iter()
.filter(|ptc| ptc.want_mmap2)
.filter_map(|ptc| sideband_target(ptc, pid, tid))
.collect()
};
if targets.is_empty() {
return;
}
let m = Mmap2Info {
addr: addr as u64,
len: len as u64,
pgoff: pgoff as u64,
maj: 0,
min: 0,
ino: 0,
prot,
flags: if shared { MAP_SHARED } else { MAP_PRIVATE },
filename: String::from(filename),
};
for t in &targets {
sideband::emit_mmap2(t, &m);
}
}
pub fn on_clone_sideband(parent_thr: &Thread, child_pid: u32, child_tid: u32) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let ppid = parent_thr.proc_data.proc.pid();
let ptid = parent_thr.tid();
let targets: Vec<SidebandTarget> = {
let counters = parent_thr.perf_counters.lock();
counters
.iter()
.filter(|ptc| ptc.want_task)
.filter_map(|ptc| sideband_target(ptc, ppid, ptid))
.collect()
};
for t in &targets {
sideband::emit_fork(t, child_pid, ppid, child_tid, ptid);
}
}
pub fn on_clone_inherit(parent_thr: &Thread, child_thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
struct InheritSpec {
cfg: PerTaskConfig,
sample_id: u64,
ring: Option<(usize, usize, Arc<dyn Any + Send + Sync>)>,
is_sampling: bool,
}
let specs: Vec<InheritSpec> = {
let counters = parent_thr.perf_counters.lock();
counters
.iter()
.filter(|p| p.inherit && !p.dead.load(Ordering::Acquire))
.map(|p| InheritSpec {
cfg: PerTaskConfig {
n: 0, event: p.event,
exclude_user: p.exclude_user,
exclude_kernel: p.exclude_kernel,
read_format: p.read_format,
enabled: p.enabled.load(Ordering::Acquire),
enable_on_exec: false,
sample_period: p.sample_period,
sample_type: p.sample_type,
freq: p.freq,
target_freq: p.freq_target,
want_comm: p.want_comm,
want_mmap2: p.want_mmap2,
want_task: p.want_task,
sample_id_all: p.sample_id_all,
inherit: true,
},
sample_id: p.sample_id.load(Ordering::Relaxed),
ring: p.inherit_ring(),
is_sampling: p.is_sampling,
})
.collect()
};
for mut spec in specs {
if spec.is_sampling && spec.ring.is_none() {
continue;
}
let Some(n) = hw::alloc_programmable_counter() else {
warn!(
"perf: attr.inherit skipped for child tid {} (no free PMU counter)",
child_thr.tid()
);
continue;
};
spec.cfg.n = n;
let child = Arc::new(PerTaskCounter::new(spec.cfg));
child.set_sample_id(spec.sample_id);
if let Some((vaddr, len, anchor)) = spec.ring {
child.set_redirect_ring(vaddr, len, anchor);
}
attach(child_thr, child);
}
}
pub fn on_task_exit(thr: &Thread) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let pid = thr.proc_data.proc.pid();
let tid = thr.tid();
let (ppid, ptid) = match thr.proc_data.proc.parent() {
Some(p) => {
let ppid = p.pid();
(ppid, ppid)
}
None => (0, 0),
};
let counters = thr.perf_counters.lock();
for ptc in counters.iter() {
if ptc.want_task
&& let Some(t) = sideband_target(ptc, pid, tid)
{
sideband::emit_exit(&t, pid, ppid, tid, ptid);
}
free_hw(ptc);
}
}
pub fn free_hw(ptc: &PerTaskCounter) {
if ptc
.hw_freed
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_err()
{
return;
}
ptc.dead.store(true, Ordering::Release);
let was_running = ptc.running.swap(false, Ordering::AcqRel);
if ptc.is_sampling {
if was_running {
ax_cpu::pmu::counter::disable(ptc.n);
ax_cpu::pmu::overflow::disable_irq(ptc.n);
sampling::unregister(ptc.n);
}
let anchors = ptc.anchors.lock().take();
if let Some(anchors) = anchors {
anchors.poll_alive.store(false, Ordering::Release);
anchors.notify.notify();
}
*ptc.redirect_anchor.lock() = None;
ptc.ring_vaddr.store(0, Ordering::Release);
ptc.notify_ptr.store(0, Ordering::Release);
} else if was_running {
ax_cpu::pmu::counter::disable(ptc.n);
}
hw::free_programmable_counter(ptc.n);
PERF_TASK_ACTIVE.fetch_sub(1, Ordering::AcqRel);
}
pub fn read_values(ptc: &PerTaskCounter) -> (u64, u64, u64) {
let mut value = ptc.accumulated.load(Ordering::Acquire);
let mut time_enabled = ptc.time_enabled_ns.load(Ordering::Acquire);
let mut time_running = ptc.time_running_ns.load(Ordering::Acquire);
if ptc.running.load(Ordering::Acquire) {
value += ax_cpu::pmu::counter::read(ptc.n);
let dt = now_ns().saturating_sub(ptc.last_in_ns.load(Ordering::Acquire));
time_enabled += dt;
time_running += dt;
}
(value, time_enabled, time_running)
}