use alloc::sync::Arc;
use core::sync::atomic::{AtomicU64, Ordering};
use ax_hal::irq::{IrqContext, IrqId, IrqReturn};
use kbpf_basic::linux_bpf::perf_event_mmap_page;
use super::{
counting::CounterExtender,
output::PerfRingOutput,
sampling_lifecycle::SampleRegistration,
sampling_registry::{RegisterError, SamplingRegistry, UnregisterError},
target::PerfCpuId,
};
use crate::{
sync::{IrqMutex, NoPreemptIrqSave},
task::{PidNamespaceId, TgidNumber, TidNumber, future::IrqNotify, try_current_user_irq_view},
};
fn pmu_irq() -> Result<IrqId, ax_hal::irq::IrqError> {
ax_hal::pmu::irq()
}
const MAX_COUNTER: usize = 30;
#[derive(Debug)]
pub(crate) struct SamplingCount(IrqMutex<SamplingCountState>);
#[derive(Debug, Default)]
struct SamplingCountState {
previous: u32,
total: u64,
remaining: i64,
period: u32,
}
impl SamplingCountState {
fn update(&mut self, raw: u32) -> u64 {
let delta = raw.wrapping_sub(self.previous);
self.total = self.total.saturating_add(u64::from(delta));
self.remaining = self.remaining.saturating_sub(i64::from(delta));
self.previous = raw;
self.total
}
fn hardware_period(&self) -> u32 {
self.remaining.clamp(1, i64::from(u32::MAX >> 1)) as u32
}
}
impl SamplingCount {
pub(crate) fn new() -> Self {
Self(IrqMutex::new(SamplingCountState::default()))
}
pub(crate) fn reset_value(&self) {
self.0.lock().total = 0;
}
fn period_or(&self, initial: u32) -> u32 {
let period = self.0.lock().period;
if period == 0 { initial } else { period }
}
pub(crate) fn value(&self) -> u64 {
self.0.lock().total
}
pub(crate) fn update(&self, index: usize) -> u64 {
let mut state = self.0.lock();
let raw = crate::perf::hw_owner::on_counter(index, |pmu, id| pmu.read(id)) as u32;
state.update(raw)
}
pub(crate) fn preload(&self, index: usize, period: u32) {
let mut state = self.0.lock();
if state.period == 0 {
state.period = period;
state.remaining = i64::from(period);
}
Self::program_chunk(index, &mut state);
}
fn period_complete(&self) -> bool {
self.0.lock().remaining <= 0
}
fn rearm(&self, index: usize, period: u32) {
let mut state = self.0.lock();
state.period = period;
if state.remaining <= 0 {
let period = i64::from(period);
state.remaining = if state.remaining <= -period {
period
} else {
state.remaining + period
};
}
Self::program_chunk(index, &mut state);
}
fn program_chunk(index: usize, state: &mut SamplingCountState) {
let chunk = state.hardware_period();
crate::perf::hw_owner::on_counter(index, |pmu, id| pmu.preload(id, u64::from(chunk)));
state.previous = 0u32.wrapping_sub(chunk);
}
}
const MIN_FREQ_PERIOD: u32 = 1;
const MAX_SAMPLE_PERIOD: u32 = u32::MAX;
pub const MAX_TARGET_FREQ: u32 = 100_000;
pub fn initial_period_for_freq(freq: u32) -> u32 {
(1_000_000_000u64 / freq.max(1) as u64).clamp(MIN_FREQ_PERIOD as u64, MAX_SAMPLE_PERIOD as u64)
as u32
}
fn next_freq_period(cur: u32, target_freq: u32, delta_ns: u64) -> u32 {
if delta_ns == 0 || target_freq == 0 {
return cur;
}
let ideal = (cur as u128 * 1_000_000_000u128) / (delta_ns as u128 * target_freq as u128);
let ideal = ideal.clamp(MIN_FREQ_PERIOD as u128, MAX_SAMPLE_PERIOD as u128) as i64;
let delta = (ideal - cur as i64 + 7) / 8;
(cur as i64 + delta).clamp(MIN_FREQ_PERIOD as i64, MAX_SAMPLE_PERIOD as i64) as u32
}
const PERF_RECORD_SAMPLE: u32 = 9;
const PERF_RECORD_MISC_KERNEL: u16 = 1;
const PERF_RECORD_MISC_USER: u16 = 2;
const MAX_STACK_DEPTH: usize = 64;
const MAX_CALLCHAIN_ENTRIES: usize = 1 + MAX_STACK_DEPTH;
pub const MAX_SAMPLE_READ_EVENTS: usize = 31;
const SAMPLE_READ_MAX_U64S: usize = 3 + MAX_SAMPLE_READ_EVENTS * 3;
const SAMPLE_RECORD_MAX_LEN: usize =
8 + 9 * 8 + SAMPLE_READ_MAX_U64S * 8 + (1 + MAX_CALLCHAIN_ENTRIES) * 8 + 16;
#[derive(Debug)]
pub struct LossState {
pending: AtomicU64,
total: AtomicU64,
}
impl LossState {
pub const fn new() -> Self {
Self {
pending: AtomicU64::new(0),
total: AtomicU64::new(0),
}
}
fn record_drop(&self) {
self.pending.fetch_add(1, Ordering::Relaxed);
self.total.fetch_add(1, Ordering::Relaxed);
}
pub fn total(&self) -> u64 {
self.total.load(Ordering::Acquire)
}
}
const PERF_SAMPLE_IP: u64 = 1 << 0;
pub(crate) const PERF_SAMPLE_TID: u64 = 1 << 1;
const PERF_SAMPLE_TIME: u64 = 1 << 2;
const PERF_SAMPLE_ADDR: u64 = 1 << 3;
pub(crate) const PERF_SAMPLE_READ: u64 = 1 << 4;
const PERF_SAMPLE_CALLCHAIN: u64 = 1 << 5;
const PERF_SAMPLE_ID: u64 = 1 << 6;
const PERF_SAMPLE_CPU: u64 = 1 << 7;
const PERF_SAMPLE_PERIOD: u64 = 1 << 8;
const PERF_SAMPLE_STREAM_ID: u64 = 1 << 9;
const PERF_SAMPLE_REGS_USER: u64 = 1 << 12;
const PERF_SAMPLE_IDENTIFIER: u64 = 1 << 16;
pub const SUPPORTED_SAMPLE_TYPE: u64 = PERF_SAMPLE_IP
| PERF_SAMPLE_TID
| PERF_SAMPLE_TIME
| PERF_SAMPLE_ADDR
| PERF_SAMPLE_READ
| PERF_SAMPLE_CALLCHAIN
| PERF_SAMPLE_ID
| PERF_SAMPLE_CPU
| PERF_SAMPLE_PERIOD
| PERF_SAMPLE_STREAM_ID
| PERF_SAMPLE_REGS_USER
| PERF_SAMPLE_IDENTIFIER;
#[derive(Clone, Copy, Default)]
pub struct SampleReadValue {
pub value: u64,
pub time_enabled: u64,
pub time_running: u64,
pub lost: u64,
}
type SampleReadCallback = unsafe fn(*const (), usize, u64) -> SampleReadValue;
#[derive(Clone)]
pub struct SampleReadEntry {
context: *const (),
callback: Option<SampleReadCallback>,
pub id: u64,
_owner: Option<Arc<dyn core::any::Any + Send + Sync>>,
}
unsafe impl Send for SampleReadEntry {}
unsafe impl Sync for SampleReadEntry {}
impl SampleReadEntry {
pub const EMPTY: Self = Self {
context: core::ptr::null(),
callback: None,
id: 0,
_owner: None,
};
pub(crate) fn owned<T: core::any::Any + Send + Sync>(
owner: Arc<T>,
callback: SampleReadCallback,
id: u64,
) -> Self {
Self {
context: Arc::as_ptr(&owner).cast(),
callback: Some(callback),
id,
_owner: Some(owner),
}
}
fn read(&self, slot: usize, now: u64) -> SampleReadValue {
self.callback
.map_or_else(SampleReadValue::default, |callback| {
unsafe { callback(self.context, slot, now) }
})
}
}
#[derive(Clone)]
pub struct SampleOutput {
ring: Option<PerfRingOutput>,
notify: Option<Arc<IrqNotify>>,
loss: Arc<LossState>,
}
impl core::fmt::Debug for SampleOutput {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("SampleOutput")
.field(
"ring",
&self
.ring
.as_ref()
.map(|ring| (ring.ring_vaddr(), ring.ring_len())),
)
.field("notifies", &self.notify.is_some())
.finish()
}
}
impl SampleOutput {
pub fn new(
ring: Option<PerfRingOutput>,
notify: Option<Arc<IrqNotify>>,
loss: Arc<LossState>,
) -> Self {
Self { ring, notify, loss }
}
}
pub struct SampleSlot {
pub(crate) count: Arc<SamplingCount>,
output: SampleOutput,
pub period: u32,
pub sample_type: u64,
pub sample_id_all: bool,
pub sample_user_lr: bool,
pub id: u64,
pub stream_id: u64,
pub read_format: u64,
pub read_entries: [SampleReadEntry; MAX_SAMPLE_READ_EVENTS],
pub read_len: u8,
pub observer: PidNamespaceId,
pub owner_ids: Option<(TgidNumber, TidNumber)>,
pub freq: bool,
pub target_freq: u32,
pub last_time: u64,
}
pub struct SampleSlotConfig {
pub(crate) count: Arc<SamplingCount>,
pub period: u32,
pub sample_type: u64,
pub sample_id_all: bool,
pub sample_user_lr: bool,
pub id: u64,
pub stream_id: u64,
pub read_format: u64,
pub read_entries: [SampleReadEntry; MAX_SAMPLE_READ_EVENTS],
pub read_len: u8,
pub observer: PidNamespaceId,
pub owner_ids: Option<(TgidNumber, TidNumber)>,
pub freq: bool,
pub target_freq: u32,
pub last_time: u64,
}
impl SampleSlot {
pub fn new(output: SampleOutput, config: SampleSlotConfig) -> Self {
let period = config.count.period_or(config.period);
Self {
count: config.count,
output,
period,
sample_type: config.sample_type,
sample_id_all: config.sample_id_all,
sample_user_lr: config.sample_user_lr,
id: config.id,
stream_id: config.stream_id,
read_format: config.read_format,
read_entries: config.read_entries,
read_len: config.read_len,
observer: config.observer,
owner_ids: config.owner_ids,
freq: config.freq,
target_freq: config.target_freq,
last_time: config.last_time,
}
}
}
#[ax_percpu::def_percpu]
static REGISTRY: SamplingRegistry<SampleSlot> = SamplingRegistry::new();
#[ax_percpu::def_percpu]
static COUNTING_REGISTRY: SamplingRegistry<Arc<IrqMutex<CounterExtender>>> =
SamplingRegistry::new();
static NEXT_REGISTRATION_GENERATION: AtomicU64 = AtomicU64::new(1);
static REGISTERED: core::sync::atomic::AtomicBool = core::sync::atomic::AtomicBool::new(false);
unsafe fn with_registry_mut<R>(
operation: impl for<'value> FnOnce(&'value mut SamplingRegistry<SampleSlot>) -> R,
) -> R {
unsafe {
ax_percpu::with_cpu_pin(|pin| {
ax_percpu::with_exclusive_cpu(pin, |exclusive| {
REGISTRY.with_current_mut(exclusive, operation)
})
})
}
.unwrap_or_else(|error| panic!("perf sampling CPU-local state is invalid: {error}"))
}
unsafe fn with_counting_registry_mut<R>(
operation: impl for<'value> FnOnce(
&'value mut SamplingRegistry<Arc<IrqMutex<CounterExtender>>>,
) -> R,
) -> R {
unsafe {
ax_percpu::with_cpu_pin(|pin| {
ax_percpu::with_exclusive_cpu(pin, |exclusive| {
COUNTING_REGISTRY.with_current_mut(exclusive, operation)
})
})
}
.unwrap_or_else(|error| panic!("perf counting CPU-local state is invalid: {error}"))
}
pub fn register(n: usize, slot: SampleSlot) -> Result<SampleRegistration, RegisterError> {
if n > MAX_COUNTER {
return Err(RegisterError::InvalidCounter);
}
let owner = PerfCpuId::new(ax_hal::percpu::this_cpu_id());
let generation = NEXT_REGISTRATION_GENERATION
.try_update(Ordering::Relaxed, Ordering::Relaxed, |generation| {
generation.checked_add(1)
})
.expect("PMU sampling registration generation exhausted");
let _guard = NoPreemptIrqSave::new();
unsafe { with_registry_mut(|registry| registry.register(n, generation, slot)) }?;
Ok(SampleRegistration::new(owner, n, generation))
}
pub(super) fn register_counting(
n: usize,
state: Arc<IrqMutex<CounterExtender>>,
) -> Result<SampleRegistration, RegisterError> {
if n > MAX_COUNTER {
return Err(RegisterError::InvalidCounter);
}
let owner = PerfCpuId::new(ax_hal::percpu::this_cpu_id());
let generation = NEXT_REGISTRATION_GENERATION
.try_update(Ordering::Relaxed, Ordering::Relaxed, |generation| {
generation.checked_add(1)
})
.expect("PMU counting registration generation exhausted");
let _guard = NoPreemptIrqSave::new();
unsafe { with_counting_registry_mut(|registry| registry.register(n, generation, state)) }?;
Ok(SampleRegistration::new(owner, n, generation))
}
pub(super) fn unregister_counting(
registration: SampleRegistration,
) -> Result<(), SamplingUnregisterError> {
drop(detach_counting(registration)?);
Ok(())
}
pub(super) fn detach_counting(
registration: SampleRegistration,
) -> Result<Arc<IrqMutex<CounterExtender>>, SamplingUnregisterError> {
if registration.owner().as_usize() != ax_hal::percpu::this_cpu_id() {
return Err(SamplingUnregisterError::WrongCpu);
}
let removed = {
let _guard = NoPreemptIrqSave::new();
unsafe {
with_counting_registry_mut(|registry| {
registry.unregister(registration.counter(), registration.generation())
})
}
.map_err(SamplingUnregisterError::Registry)?
};
Ok(removed)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum SamplingUnregisterError {
WrongCpu,
Registry(UnregisterError),
}
pub fn unregister(registration: SampleRegistration) -> Result<(), SamplingUnregisterError> {
if registration.owner().as_usize() != ax_hal::percpu::this_cpu_id() {
return Err(SamplingUnregisterError::WrongCpu);
}
let removed = {
let _guard = NoPreemptIrqSave::new();
unsafe {
with_registry_mut(|registry| {
let removed =
registry.unregister(registration.counter(), registration.generation())?;
removed.count.update(registration.counter());
Ok(removed)
})
}
.map_err(SamplingUnregisterError::Registry)?
};
drop(removed);
Ok(())
}
pub fn replace_output(
registration: SampleRegistration,
output: SampleOutput,
) -> Result<(), SamplingUnregisterError> {
if registration.owner().as_usize() != ax_hal::percpu::this_cpu_id() {
return Err(SamplingUnregisterError::WrongCpu);
}
let old = {
let _guard = NoPreemptIrqSave::new();
unsafe {
with_registry_mut(|registry| {
let slot = registry
.get_mut(registration.counter())
.ok_or(UnregisterError::Stale)?;
let config = SampleSlotConfig {
count: Arc::clone(&slot.count),
period: slot.period,
sample_type: slot.sample_type,
sample_id_all: slot.sample_id_all,
sample_user_lr: slot.sample_user_lr,
id: slot.id,
stream_id: slot.stream_id,
read_format: slot.read_format,
read_entries: slot.read_entries.clone(),
read_len: slot.read_len,
observer: slot.observer,
owner_ids: slot.owner_ids,
freq: slot.freq,
target_freq: slot.target_freq,
last_time: slot.last_time,
};
registry.replace(
registration.counter(),
registration.generation(),
SampleSlot::new(output, config),
)
})
}
.map_err(SamplingUnregisterError::Registry)?
};
drop(old);
Ok(())
}
pub fn ensure_pmu_irq_registered() -> Result<(), ax_hal::irq::IrqError> {
let pmu_irq = pmu_irq()?;
if REGISTERED
.compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
let cpus = ax_hal::irq::CpuMask::first_n(ax_hal::cpu_num());
if let Err(err) = ax_hal::irq::request_percpu_irq(pmu_irq, cpus, pmu_overflow_handler) {
REGISTERED.store(false, Ordering::Release);
return Err(err);
}
}
Ok(())
}
pub fn enable_local_pmu_irq() -> Result<(), ax_hal::irq::IrqError> {
ax_hal::irq::set_enable(pmu_irq()?, true)
}
fn service_overflowed_slots(
registry: &mut SamplingRegistry<SampleSlot>,
overflow: u32,
misc: u16,
interrupted: Option<ax_cpu::trap::InterruptedContext>,
ip: usize,
is_user: bool,
) -> u32 {
let current = try_current_user_irq_view();
let mut handled = 0;
for n in 0..=MAX_COUNTER {
if overflow & (1 << n) == 0 {
continue;
}
handled |= 1 << n;
let Some(slot) = registry.get_mut(n) else {
continue;
};
let sample_type = slot.sample_type;
let id = slot.id;
let cur_period = slot.period;
crate::perf::hw_owner::on_counter(n, |pmu, id| pmu.disable(id));
slot.count.update(n);
if !slot.count.period_complete() {
slot.count.rearm(n, cur_period);
crate::perf::hw_owner::on_counter(n, |pmu, id| pmu.enable(id));
continue;
}
let time = ax_runtime::hal::time::monotonic_time_nanos();
let cpu = ax_hal::percpu::this_cpu_id() as u32;
let read_len = usize::from(slot.read_len).min(MAX_SAMPLE_READ_EVENTS);
let mut read_values = [SampleReadValue::default(); MAX_SAMPLE_READ_EVENTS];
for (entry, value) in slot.read_entries[..read_len].iter().zip(&mut read_values) {
if sample_type & PERF_SAMPLE_READ != 0 {
*value = entry.read(n, time);
}
}
let (pid, tid) = slot.owner_ids.map_or_else(
|| {
current.as_ref().map_or((None, None), |task| {
(
task.visible_tgid(slot.observer),
task.visible_tid(slot.observer),
)
})
},
|(pid, tid)| (Some(pid), Some(tid)),
);
let mut callchain = [0u64; MAX_CALLCHAIN_ENTRIES];
let callchain_len = if sample_type & PERF_SAMPLE_CALLCHAIN != 0 {
build_callchain(interrupted, ip, is_user, &mut callchain)
} else {
0
};
let mut record = [0u8; SAMPLE_RECORD_MAX_LEN];
let data = SampleData {
ip: ip as u64,
pid,
tid,
time,
addr: 0,
id,
stream_id: slot.stream_id,
cpu,
period: cur_period as u64,
read_format: slot.read_format,
read_entries: &slot.read_entries[..read_len],
read_values: &read_values[..read_len],
callchain: &callchain[..callchain_len],
user_lr: interrupted
.filter(|context| {
slot.sample_user_lr
&& context.privilege == ax_cpu::trap::InterruptedPrivilege::User
})
.map(|context| context.lr as u64),
};
let len = build_sample(&mut record, sample_type, misc, &data);
if let Some(ring) = &slot.output.ring {
write_sample(ring, &slot.output.loss, slot, &data, &record[..len]);
}
let next_period = if slot.freq {
let next = if slot.last_time != 0 {
next_freq_period(
cur_period,
slot.target_freq,
time.saturating_sub(slot.last_time),
)
} else {
cur_period
};
slot.period = next;
slot.last_time = time;
next
} else {
cur_period
};
slot.count.rearm(n, next_period);
crate::perf::hw_owner::on_counter(n, |pmu, id| pmu.enable(id));
if let Some(notify) = &slot.output.notify {
notify.notify_irq();
}
}
handled
}
pub fn pmu_overflow_handler(_ctx: IrqContext) -> IrqReturn {
let context = ax_hal::irq::interrupted_context()
.expect("PMU trap must supply its interrupted register image");
let interrupted = Some(context);
let ip = context.pc;
let is_user = context.privilege == ax_cpu::trap::InterruptedPrivilege::User;
let ovf = crate::perf::hw_owner::on_pmu(|pmu| pmu.overflow_status()) as u32;
if ovf == 0 {
return IrqReturn::Unhandled;
}
crate::perf::hw_owner::on_pmu(|pmu| pmu.clear_overflow(u64::from(ovf)));
let misc = if is_user {
PERF_RECORD_MISC_USER
} else {
PERF_RECORD_MISC_KERNEL
};
unsafe {
with_counting_registry_mut(|registry| {
for n in 0..=MAX_COUNTER {
if ovf & (1 << n) != 0
&& let Some(state) = registry.get_mut(n)
{
state.lock().record_overflow();
}
}
})
};
let handled = crate::perf::hw_owner::with_counters_paused(|| {
unsafe {
with_registry_mut(|registry| {
service_overflowed_slots(registry, ovf, misc, interrupted, ip, is_user)
})
}
});
debug_assert_eq!(handled & !ovf, 0);
IrqReturn::Handled
}
fn build_callchain(
interrupted: Option<ax_cpu::trap::InterruptedContext>,
ip: usize,
is_user: bool,
chain: &mut [u64],
) -> usize {
let Some((marker, frames)) = chain.split_first_mut() else {
return 0;
};
*marker = if is_user {
(-512i64) as u64
} else {
(-128i64) as u64
};
let count = match interrupted {
Some(context) if is_user => {
super::unwind::user_callchain(ip, context.fp, context.sp, frames)
}
Some(context) => super::unwind::kernel_callchain(ip, context.fp, frames),
None => {
let Some(leaf) = frames.first_mut() else {
return 1;
};
*leaf = ip as u64;
1
}
};
1 + count
}
#[cfg(all(test, axtest))]
fn kernel_task_sample_ids_are_empty_for_test() -> bool {
try_current_user_irq_view().is_none()
}
struct SampleData<'a> {
ip: u64,
pid: Option<TgidNumber>,
tid: Option<TidNumber>,
time: u64,
addr: u64,
id: u64,
stream_id: u64,
cpu: u32,
period: u64,
read_format: u64,
read_entries: &'a [SampleReadEntry],
read_values: &'a [SampleReadValue],
callchain: &'a [u64],
user_lr: Option<u64>,
}
fn build_sample(buf: &mut [u8], sample_type: u64, misc: u16, d: &SampleData) -> usize {
let mut off = 0usize;
macro_rules! put {
($v:expr) => {{
let bytes = $v.to_ne_bytes();
buf[off..off + bytes.len()].copy_from_slice(&bytes);
off += bytes.len();
}};
}
put!(PERF_RECORD_SAMPLE); put!(misc); let size_off = off;
put!(0u16);
if sample_type & PERF_SAMPLE_IDENTIFIER != 0 {
put!(d.id);
}
if sample_type & PERF_SAMPLE_IP != 0 {
put!(d.ip);
}
if sample_type & PERF_SAMPLE_TID != 0 {
put!(d.pid.map_or(0, TgidNumber::get));
put!(d.tid.map_or(0, TidNumber::get));
}
if sample_type & PERF_SAMPLE_TIME != 0 {
put!(d.time);
}
if sample_type & PERF_SAMPLE_ADDR != 0 {
put!(d.addr);
}
if sample_type & PERF_SAMPLE_ID != 0 {
put!(d.id);
}
if sample_type & PERF_SAMPLE_STREAM_ID != 0 {
put!(d.stream_id);
}
if sample_type & PERF_SAMPLE_CPU != 0 {
put!(d.cpu);
put!(0u32);
}
if sample_type & PERF_SAMPLE_PERIOD != 0 {
put!(d.period);
}
if sample_type & PERF_SAMPLE_READ != 0 {
if d.read_format & super::PERF_FORMAT_GROUP != 0 {
put!(d.read_values.len() as u64);
if d.read_format & super::PERF_FORMAT_TOTAL_TIME_ENABLED != 0 {
put!(d.read_values.first().map_or(0, |value| value.time_enabled));
}
if d.read_format & super::PERF_FORMAT_TOTAL_TIME_RUNNING != 0 {
put!(d.read_values.first().map_or(0, |value| value.time_running));
}
for (entry, value) in d.read_entries.iter().zip(d.read_values) {
put!(value.value);
if d.read_format & super::PERF_FORMAT_ID != 0 {
put!(entry.id);
}
if d.read_format & super::PERF_FORMAT_LOST != 0 {
put!(value.lost);
}
}
} else {
let value = d.read_values.first().copied().unwrap_or_default();
put!(value.value);
if d.read_format & super::PERF_FORMAT_TOTAL_TIME_ENABLED != 0 {
put!(value.time_enabled);
}
if d.read_format & super::PERF_FORMAT_TOTAL_TIME_RUNNING != 0 {
put!(value.time_running);
}
if d.read_format & super::PERF_FORMAT_ID != 0 {
put!(d.read_entries.first().map_or(0, |entry| entry.id));
}
if d.read_format & super::PERF_FORMAT_LOST != 0 {
put!(value.lost);
}
}
}
if sample_type & PERF_SAMPLE_CALLCHAIN != 0 {
put!(d.callchain.len() as u64);
for &entry in d.callchain {
put!(entry);
}
}
if sample_type & PERF_SAMPLE_REGS_USER != 0 {
if let Some(lr) = d.user_lr {
put!(2u64); put!(lr);
} else {
put!(0u64); }
}
buf[size_off..size_off + 2].copy_from_slice(&(off as u16).to_ne_bytes());
off
}
unsafe fn ring_write_locked(ring: &PerfRingOutput, record: &[u8]) -> bool {
let ring_vaddr = ring.ring_vaddr();
let ring_len = ring.ring_len();
if ring_vaddr == 0 || ring_len < core::mem::size_of::<perf_event_mmap_page>() {
return false;
}
let header = ring_vaddr as *mut perf_event_mmap_page;
let data_offset =
unsafe { core::ptr::addr_of!((*header).data_offset).read_volatile() } as usize;
let data_size = unsafe { core::ptr::addr_of!((*header).data_size).read_volatile() } as usize;
if data_size == 0 || data_offset > ring_len || data_offset + data_size > ring_len {
return false;
}
let len = record.len();
if len > data_size {
return false;
}
let head = unsafe { core::ptr::addr_of!((*header).data_head).read_volatile() };
let tail = unsafe { core::ptr::addr_of!((*header).data_tail).read_volatile() };
if head.wrapping_sub(tail).wrapping_add(len as u64) > data_size as u64 {
return false;
}
let data_base = ring_vaddr + data_offset;
let start = (head % data_size as u64) as usize;
let first = core::cmp::min(len, data_size - start);
unsafe {
core::ptr::copy_nonoverlapping(record.as_ptr(), (data_base + start) as *mut u8, first);
if first < len {
core::ptr::copy_nonoverlapping(
record.as_ptr().add(first),
data_base as *mut u8,
len - first,
);
}
}
core::sync::atomic::fence(Ordering::Release);
unsafe {
core::ptr::addr_of_mut!((*header).data_head).write_volatile(head.wrapping_add(len as u64));
}
true
}
fn write_sample(
ring: &PerfRingOutput,
loss: &LossState,
slot: &SampleSlot,
data: &SampleData<'_>,
sample: &[u8],
) {
let Some(_writer) = ring.try_begin_write() else {
ring.record_contention_drop();
loss.record_drop();
return;
};
let pending = loss.pending.load(Ordering::Relaxed);
if pending != 0 {
let mut record = [0u8; super::sample_id::LOST_RECORD_MAX_LEN];
let identity = super::sample_id::SampleId {
pid: data.pid.map_or(0, TgidNumber::get),
tid: data.tid.map_or(0, TidNumber::get),
time: data.time,
id: data.id,
stream_id: data.stream_id,
cpu: data.cpu,
};
let length =
identity.encode_lost(pending, slot.sample_type, slot.sample_id_all, &mut record);
if !unsafe { ring_write_locked(ring, &record[..length]) } {
loss.record_drop();
return;
}
loss.pending.fetch_sub(pending, Ordering::Relaxed);
}
if unsafe { ring_write_locked(ring, sample) } {
} else {
loss.record_drop();
}
}
pub(crate) unsafe fn ring_write_process(ring: &PerfRingOutput, record: &[u8]) {
let Some(_writer) = ring.try_begin_write() else {
ring.record_contention_drop();
return;
};
let _ = unsafe { ring_write_locked(ring, record) };
}
#[cfg(all(test, axtest))]
mod tests {
#[axtest::axtest]
fn maximum_period_preload_leaves_irq_latency_headroom() {
let count = super::SamplingCount::new();
let _guard = crate::sync::NoPreemptIrqSave::new();
super::super::percpu::ensure_current_cpu_initialized().unwrap();
let slot = super::super::percpu::alloc_current_programmable().unwrap();
crate::perf::hw_owner::on_counter(slot, |pmu, id| pmu.disable(id));
count.preload(slot, u32::MAX);
let raw = crate::perf::hw_owner::on_counter(slot, |pmu, id| pmu.read(id)) as u32;
crate::perf::hw_owner::on_counter(slot, |pmu, id| pmu.write(id, 10));
count.update(slot);
assert!(
!count.period_complete(),
"first hardware chunk is not a full logical sample"
);
count.rearm(slot, u32::MAX);
crate::perf::hw_owner::on_counter(slot, |pmu, id| pmu.write(id, 9));
count.update(slot);
assert!(count.period_complete());
assert_eq!(
count.value(),
u64::from(u32::MAX) + 9,
"IRQ latency must survive the logical period boundary"
);
count.rearm(slot, u32::MAX);
assert!(!count.period_complete());
super::super::percpu::free_current_programmable(slot);
assert_eq!(
raw,
0u32.wrapping_sub(u32::MAX >> 1),
"hardware chunk must leave overflow latency headroom"
);
}
#[axtest::axtest]
fn sampling_delta_counts_partial_wrap_and_reload() {
let mut state = super::SamplingCountState {
previous: 0u32.wrapping_sub(100),
total: 0,
remaining: 100,
period: 100,
};
assert_eq!(state.update(0u32.wrapping_sub(60)), 40);
assert_eq!(state.update(7), 107);
assert_eq!(state.update(7), 107);
state.previous = 0u32.wrapping_sub(200);
assert_eq!(state.update(0u32.wrapping_sub(170)), 137);
}
#[axtest::axtest]
fn read_snapshot_retains_callback_until_registry_removal() {
use super::*;
unsafe fn read_value(context: *const (), _slot: usize, _now: u64) -> SampleReadValue {
let value = unsafe { &*context.cast::<AtomicU64>() }.load(Ordering::Acquire);
SampleReadValue {
value,
..SampleReadValue::default()
}
}
let owner = Arc::new(AtomicU64::new(17));
let weak = Arc::downgrade(&owner);
let entry = SampleReadEntry::owned(Arc::clone(&owner), read_value, 1);
let mut registry = SamplingRegistry::new();
assert!(registry.register(0, 1, entry).is_ok());
drop(owner);
assert!(
weak.upgrade().is_some(),
"closing the fd must retain IRQ callback state"
);
assert_eq!(registry.get_mut(0).unwrap().read(0, 0).value, 17);
drop(registry.unregister(0, 1).unwrap());
assert!(
weak.upgrade().is_none(),
"unregister must release its callback ownership"
);
}
#[cfg(all(test, axtest, target_arch = "aarch64"))]
#[axtest::axtest]
fn kernel_task_sample_ids_are_empty() {
assert!(super::kernel_task_sample_ids_are_empty_for_test());
}
#[axtest::axtest]
fn maximum_sample_record_fits_irq_stack_buffer() {
use super::*;
let entries = [const { SampleReadEntry::EMPTY }; MAX_SAMPLE_READ_EVENTS];
let values = [SampleReadValue::default(); MAX_SAMPLE_READ_EVENTS];
let callchain = [0u64; MAX_CALLCHAIN_ENTRIES];
let data = SampleData {
ip: 1,
pid: None,
tid: None,
time: 2,
addr: 3,
id: 4,
stream_id: 5,
cpu: 6,
period: 7,
read_format: super::super::PERF_FORMAT_GROUP
| super::super::PERF_FORMAT_TOTAL_TIME_ENABLED
| super::super::PERF_FORMAT_TOTAL_TIME_RUNNING
| super::super::PERF_FORMAT_ID
| super::super::PERF_FORMAT_LOST,
read_entries: &entries,
read_values: &values,
callchain: &callchain,
user_lr: Some(8),
};
let mut record = [0u8; SAMPLE_RECORD_MAX_LEN];
let sample_type = PERF_SAMPLE_IDENTIFIER
| PERF_SAMPLE_IP
| PERF_SAMPLE_TID
| PERF_SAMPLE_TIME
| PERF_SAMPLE_ADDR
| PERF_SAMPLE_ID
| PERF_SAMPLE_STREAM_ID
| PERF_SAMPLE_CPU
| PERF_SAMPLE_PERIOD
| PERF_SAMPLE_READ
| PERF_SAMPLE_CALLCHAIN
| PERF_SAMPLE_REGS_USER;
assert_eq!(
build_sample(&mut record, sample_type, PERF_RECORD_MISC_USER, &data),
SAMPLE_RECORD_MAX_LEN
);
}
}