use alloc::{
string::String,
sync::{Arc, Weak},
vec::Vec,
};
use core::{
any::Any,
sync::atomic::{AtomicBool, AtomicU32, AtomicU64, AtomicUsize, Ordering, fence},
};
use ax_alloc::GlobalPage;
use ax_runtime::hal::paging::MappingFlags;
use ax_task::IrqNotify;
use kbpf_basic::linux_bpf::perf_event_mmap_page;
use super::{
hw,
sampling::{self, SampleSlot},
sideband::{self, Mmap2Info, SidebandTarget},
};
use crate::{
sync::IrqMutex,
task::{PidIdentity, PidNamespaceId, TgidNumber, Thread, TidNumber},
};
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,
observer: PidNamespaceId,
rdpmc_page: IrqMutex<Option<Weak<GlobalPage>>>,
ring_vaddr: AtomicUsize,
ring_len: AtomicUsize,
notify_ptr: AtomicUsize,
anchors: IrqMutex<Option<SamplingAnchors>>,
redirect_anchor: IrqMutex<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,
pub observer: PidNamespaceId,
}
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,
observer: cfg.observer,
rdpmc_page: IrqMutex::new(None),
ring_vaddr: AtomicUsize::new(0),
ring_len: AtomicUsize::new(0),
notify_ptr: AtomicUsize::new(0),
anchors: IrqMutex::new(None),
redirect_anchor: IrqMutex::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);
let active = self.running.load(Ordering::Acquire);
if active {
if self.is_sampling {
ax_cpu::pmu::counter::preload(self.n, self.sample_period);
} else {
ax_cpu::pmu::counter::reset(self.n);
}
}
if !self.is_sampling {
self.publish_rdpmc_page(active);
}
}
pub fn rdpmc_page_mapped(&self) -> bool {
self.rdpmc_page
.lock()
.as_ref()
.and_then(Weak::upgrade)
.is_some()
}
pub fn install_rdpmc_page(&self, pages: Arc<GlobalPage>) -> bool {
let mut slot = self.rdpmc_page.lock();
if slot.as_ref().and_then(Weak::upgrade).is_some() {
return false;
}
self.write_rdpmc_snapshot(&pages, self.running.load(Ordering::Acquire));
*slot = Some(Arc::downgrade(&pages));
true
}
fn publish_rdpmc_page(&self, active: bool) {
let page = self.rdpmc_page.lock();
let Some(page) = page.as_ref().and_then(Weak::upgrade) else {
return;
};
self.write_rdpmc_snapshot(&page, active);
}
fn write_rdpmc_snapshot(&self, page: &GlobalPage, active: bool) {
let header = page.start_vaddr().as_usize() as *mut perf_event_mmap_page;
let index = if active { self.n as u32 + 1 } else { 0 };
let offset = self.accumulated.load(Ordering::Acquire) as i64;
let time_enabled = self.time_enabled_ns.load(Ordering::Acquire);
let time_running = self.time_running_ns.load(Ordering::Acquire);
unsafe {
let sequence = AtomicU32::from_ptr(core::ptr::addr_of_mut!((*header).lock));
let odd = sequence.load(Ordering::Relaxed).wrapping_add(1) | 1;
sequence.store(odd, Ordering::SeqCst);
core::ptr::addr_of_mut!((*header).index).write_volatile(index);
core::ptr::addr_of_mut!((*header).offset).write_volatile(offset);
core::ptr::addr_of_mut!((*header).time_enabled).write_volatile(time_enabled);
core::ptr::addr_of_mut!((*header).time_running).write_volatile(time_running);
fence(Ordering::Release);
sequence.store(odd.wrapping_add(1), Ordering::Release);
}
}
fn release_rdpmc_page(&self) {
*self.rdpmc_page.lock() = None;
}
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),
observer: ptc.observer,
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);
if !ptc.is_sampling {
ptc.publish_rdpmc_page(true);
}
}
}
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);
if !ptc.is_sampling {
ptc.publish_rdpmc_page(false);
}
}
}
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 visible_tgid(ptc: &PerTaskCounter, identity: &PidIdentity) -> Option<TgidNumber> {
identity
.visible_number_in(ptc.observer)
.map(TgidNumber::from)
}
fn visible_tid(ptc: &PerTaskCounter, identity: &PidIdentity) -> Option<TidNumber> {
identity
.visible_number_in(ptc.observer)
.map(TidNumber::from)
}
fn sideband_target(ptc: &PerTaskCounter, thread: &Thread) -> 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;
}
let pid = visible_tgid(ptc, &thread.proc_data.identity())?;
let tid = visible_tid(ptc, &thread.pid_identity())?;
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;
}
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, thr).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 targets: Vec<SidebandTarget> = {
let counters = thr.perf_counters.lock();
counters
.iter()
.filter(|ptc| ptc.want_mmap2)
.filter_map(|ptc| sideband_target(ptc, thr))
.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_process: &PidIdentity,
child_thread: &PidIdentity,
) {
if PERF_TASK_ACTIVE.load(Ordering::Acquire) == 0 {
return;
}
let targets: Vec<(SidebandTarget, TgidNumber, TidNumber, TgidNumber, TidNumber)> = {
let counters = parent_thr.perf_counters.lock();
counters
.iter()
.filter(|ptc| ptc.want_task)
.filter_map(|ptc| {
Some((
sideband_target(ptc, parent_thr)?,
visible_tgid(ptc, child_process)?,
visible_tid(ptc, child_thread)?,
visible_tgid(ptc, &parent_thr.proc_data.identity())?,
visible_tid(ptc, &parent_thr.pid_identity())?,
))
})
.collect()
};
for (target, child_pid, child_tid, parent_pid, parent_tid) in &targets {
sideband::emit_fork(target, *child_pid, *parent_pid, *child_tid, *parent_tid);
}
}
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,
observer: p.observer,
},
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 counters = thr.perf_counters.lock();
for ptc in counters.iter() {
if ptc.want_task
&& let Some(target) = sideband_target(ptc, thr)
{
let pid = target.pid;
let tid = target.tid;
let parent = thr.proc_data.proc.parent().and_then(|parent| {
let number = parent.identity().visible_number_in(ptc.observer)?;
Some((TgidNumber::from(number), TidNumber::from(number)))
});
sideband::emit_exit(
&target,
pid,
parent.map(|(pid, _)| pid),
tid,
parent.map(|(_, tid)| tid),
);
}
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 {
let delta = ax_cpu::pmu::counter::read(ptc.n);
ptc.accumulated.fetch_add(delta, Ordering::AcqRel);
ax_cpu::pmu::counter::disable(ptc.n);
let dt = now_ns().saturating_sub(ptc.last_in_ns.load(Ordering::Acquire));
ptc.time_enabled_ns.fetch_add(dt, Ordering::AcqRel);
ptc.time_running_ns.fetch_add(dt, Ordering::AcqRel);
}
ptc.publish_rdpmc_page(false);
ptc.release_rdpmc_page();
}
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)
}