use std::fs::File;
use std::io::{self, Read};
use std::mem;
use std::os::fd::{FromRawFd, OwnedFd};
use anyhow::{Context, Result};
#[derive(Clone, Copy)]
pub struct PerfCounts {
pub instructions: u64,
pub cycles: u64,
pub cache_misses: u64,
pub branch_misses: u64,
}
#[repr(C)]
#[derive(Default)]
struct PerfEventAttr {
type_: u32,
size: u32,
config: u64,
sample_period: u64,
sample_type: u64,
read_format: u64,
flags: u64,
wakeup_events: u32,
bp_type: u32,
bp_addr: u64,
bp_len: u64,
}
const INHERIT: u64 = 1 << 1;
const EXCLUDE_KERNEL: u64 = 1 << 5;
const EXCLUDE_HV: u64 = 1 << 6;
const HW_CPU_CYCLES: u64 = 0;
const HW_INSTRUCTIONS: u64 = 1;
const HW_CACHE_MISSES: u64 = 3;
const HW_BRANCH_MISSES: u64 = 5;
const EVENTS: [u64; 4] = [
HW_INSTRUCTIONS,
HW_CPU_CYCLES,
HW_CACHE_MISSES,
HW_BRANCH_MISSES,
];
const PERF_FLAG_FD_CLOEXEC: libc::c_ulong = 1 << 3;
const FORMAT_TOTAL_TIME_ENABLED: u64 = 1 << 0;
const FORMAT_TOTAL_TIME_RUNNING: u64 = 1 << 1;
fn open(config: u64, pid: i32) -> io::Result<OwnedFd> {
let attr = PerfEventAttr {
type_: 0, size: mem::size_of::<PerfEventAttr>() as u32,
config,
read_format: FORMAT_TOTAL_TIME_ENABLED | FORMAT_TOTAL_TIME_RUNNING,
flags: INHERIT | EXCLUDE_KERNEL | EXCLUDE_HV,
..Default::default()
};
let fd = unsafe {
libc::syscall(
libc::SYS_perf_event_open,
&attr,
pid,
-1 as libc::c_int, -1 as libc::c_int, PERF_FLAG_FD_CLOEXEC,
)
};
if fd < 0 {
return Err(io::Error::last_os_error());
}
Ok(unsafe { OwnedFd::from_raw_fd(fd as i32) })
}
pub fn probe() -> Result<()> {
let Err(e) = open(HW_INSTRUCTIONS, 0) else {
return Ok(());
};
let paranoid = std::fs::read_to_string("/proc/sys/kernel/perf_event_paranoid")
.ok()
.and_then(|s| s.trim().parse::<i64>().ok());
let hint = match (e.kind(), paranoid) {
(io::ErrorKind::PermissionDenied, Some(p)) if p > 2 => format!(
"kernel.perf_event_paranoid is {p}, which blocks unprivileged perf events; \
run `sudo sysctl kernel.perf_event_paranoid=2`"
),
(io::ErrorKind::PermissionDenied, _) => {
"perf events are denied even though kernel.perf_event_paranoid allows them \
(container seccomp profile or LSM policy?)"
.to_string()
}
(io::ErrorKind::NotFound, _) => {
"the hardware 'instructions' event is unsupported: no PMU is available \
(VM without PMU passthrough?)"
.to_string()
}
_ => "perf_event_open(2) failed".to_string(),
};
Err(e).context(format!("--counters is unavailable: {hint}"))
}
pub struct Counters([File; 4]);
pub fn attach(pid: i32) -> io::Result<Counters> {
let mut fds = Vec::with_capacity(EVENTS.len());
for event in EVENTS {
fds.push(File::from(open(event, pid)?));
}
Ok(Counters(fds.try_into().expect("one fd per event")))
}
impl Counters {
pub fn read(&self) -> io::Result<PerfCounts> {
let mut values = [0u64; 4];
for (mut fd, out) in self.0.iter().zip(&mut values) {
let mut buf = [0u8; 3 * 8]; fd.read_exact(&mut buf)?;
let [value, enabled, running] =
std::array::from_fn(|i| u64::from_ne_bytes(buf[i * 8..][..8].try_into().unwrap()));
*out = if running > 0 {
(value as f64 * enabled as f64 / running as f64) as u64
} else {
value
};
}
Ok(PerfCounts {
instructions: values[0],
cycles: values[1],
cache_misses: values[2],
branch_misses: values[3],
})
}
}