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::{
output::PerfRingOutput,
sampling_lifecycle::SampleRegistration,
sampling_registry::{RegisterError, SamplingRegistry, UnregisterError},
target::PerfCpuId,
};
use crate::{
sync::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;
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 SAMPLE_RECORD_MAX_LEN: usize = 8 + 9 * 8;
const PERF_SAMPLE_IP: u64 = 1 << 0;
const PERF_SAMPLE_TID: u64 = 1 << 1;
const PERF_SAMPLE_TIME: u64 = 1 << 2;
const PERF_SAMPLE_ADDR: u64 = 1 << 3;
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_IDENTIFIER: u64 = 1 << 16;
pub const SUPPORTED_SAMPLE_TYPE: u64 = PERF_SAMPLE_IP
| PERF_SAMPLE_TID
| PERF_SAMPLE_TIME
| PERF_SAMPLE_ADDR
| PERF_SAMPLE_ID
| PERF_SAMPLE_CPU
| PERF_SAMPLE_PERIOD
| PERF_SAMPLE_STREAM_ID
| PERF_SAMPLE_IDENTIFIER;
#[derive(Clone)]
pub struct SampleOutput {
ring: Option<PerfRingOutput>,
notify: Option<Arc<IrqNotify>>,
}
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>>) -> Self {
Self { ring, notify }
}
}
pub struct SampleSlot {
output: SampleOutput,
pub period: u32,
pub sample_type: u64,
pub id: u64,
pub observer: PidNamespaceId,
pub freq: bool,
pub target_freq: u32,
pub last_time: u64,
}
pub struct SampleSlotConfig {
pub period: u32,
pub sample_type: u64,
pub id: u64,
pub observer: PidNamespaceId,
pub freq: bool,
pub target_freq: u32,
pub last_time: u64,
}
impl SampleSlot {
pub fn new(output: SampleOutput, config: SampleSlotConfig) -> Self {
Self {
output,
period: config.period,
sample_type: config.sample_type,
id: config.id,
observer: config.observer,
freq: config.freq,
target_freq: config.target_freq,
last_time: config.last_time,
}
}
}
#[ax_percpu::def_percpu]
static REGISTRY: SamplingRegistry<SampleSlot> = 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}"))
}
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))
}
#[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| {
registry.unregister(registration.counter(), registration.generation())
})
}
.map_err(SamplingUnregisterError::Registry)?
};
drop(removed);
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: u64,
misc: u16,
ip: u64,
) -> u64 {
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;
let time = ax_runtime::hal::time::monotonic_time_nanos();
let cpu = ax_hal::percpu::this_cpu_id() as u32;
let (pid, tid) = current.as_ref().map_or((None, None), |task| {
(
task.visible_tgid(slot.observer),
task.visible_tid(slot.observer),
)
});
let mut record = [0u8; SAMPLE_RECORD_MAX_LEN];
let data = SampleData {
ip,
pid,
tid,
time,
addr: 0,
id,
stream_id: 0,
cpu,
period: cur_period as u64,
};
let len = build_sample(&mut record, sample_type, misc, &data);
if let Some(ring) = &slot.output.ring {
unsafe { ring_write(ring, &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
};
crate::perf::hw_owner::on_counter(n, |pmu, id| pmu.preload(id, u64::from(next_period)));
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 ip = context.pc as u64;
let is_user = context.privilege == ax_cpu::trap::InterruptedPrivilege::User;
let ovf = crate::perf::hw_owner::on_pmu(|pmu| pmu.overflow_status());
if ovf == 0 {
return IrqReturn::Unhandled;
}
let misc = if is_user {
PERF_RECORD_MISC_USER
} else {
PERF_RECORD_MISC_KERNEL
};
let handled =
unsafe { with_registry_mut(|registry| service_overflowed_slots(registry, ovf, misc, ip)) };
crate::perf::hw_owner::on_pmu(|pmu| pmu.clear_overflow(handled));
IrqReturn::Handled
}
#[cfg(all(test, axtest))]
fn kernel_task_sample_ids_are_empty_for_test() -> bool {
try_current_user_irq_view().is_none()
}
struct SampleData {
ip: u64,
pid: Option<TgidNumber>,
tid: Option<TidNumber>,
time: u64,
addr: u64,
id: u64,
stream_id: u64,
cpu: u32,
period: 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);
}
buf[size_off..size_off + 2].copy_from_slice(&(off as u16).to_ne_bytes());
off
}
unsafe fn ring_write(ring: &PerfRingOutput, record: &[u8]) {
let Some(_writer) = ring.try_begin_write() else {
ring.record_contention_drop();
return;
};
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;
}
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;
}
let len = record.len();
if len > data_size {
return;
}
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;
}
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));
}
}
pub(crate) unsafe fn ring_write_process(ring: &PerfRingOutput, record: &[u8]) {
unsafe { ring_write(ring, record) };
}
#[cfg(all(test, axtest))]
mod tests {
#[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());
}
}