use crate::observation::{
BindContext, Perturbation, Sensor, SensorDescriptor, SensorId, SensorOutcome, StateWriter,
Uncertainty,
};
use corescout_core::error::{Error, Result};
#[cfg_attr(not(target_os = "linux"), allow(unused_imports))]
use corescout_mirror::state::{ChannelId, Semantics, Unit};
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
const EVENTS: [(&str, u64); 4] = [
("cpu.pmu.cycles", 0), ("cpu.pmu.instructions", 1), ("cpu.pmu.cache_misses", 3), ("cpu.pmu.branch_misses", 5), ];
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub struct CounterSensor {
counters: Vec<(u32, usize, RawFd)>,
channels: Vec<ChannelId>,
}
#[cfg(target_os = "linux")]
type RawFd = std::os::unix::io::RawFd;
#[cfg(not(target_os = "linux"))]
type RawFd = i32;
impl CounterSensor {
pub fn new() -> CounterSensor {
CounterSensor {
counters: Vec::new(),
channels: Vec::new(),
}
}
}
impl Default for CounterSensor {
fn default() -> Self {
Self::new()
}
}
impl Drop for CounterSensor {
fn drop(&mut self) {
#[cfg(target_os = "linux")]
for (_, _, fd) in &self.counters {
unsafe {
libc::close(*fd);
}
}
}
}
impl Sensor for CounterSensor {
fn descriptor(&self) -> SensorDescriptor {
SensorDescriptor {
id: SensorId(7),
key: "counters",
physical_fact: "cycles, retired instructions, cache misses and branch \
mispredictions executed by each core since the counter was opened",
source: "perf_event_open(2), PERF_TYPE_HARDWARE, per-CPU counting mode",
max_rate_hz: 1000.0,
perturbation: Perturbation::Low,
uncertainty: Uncertainty::unknown(
"counts are exact for the event as the microarchitecture defines it, but \
event definitions differ between vendors and generations, so absolute \
comparison across machines is not meaningful",
),
requires_privilege: true,
}
}
fn bind(&mut self, ctx: &mut BindContext<'_>) -> Result<()> {
#[cfg(not(target_os = "linux"))]
{
let _ = ctx;
Err(Error::unsupported(
"hardware performance counters (perf_event_open is Linux only)",
))
}
#[cfg(target_os = "linux")]
{
let cpus: Vec<u32> = ctx
.substrate()
.topology
.logical_cpus
.iter()
.filter(|c| c.online)
.map(|c| c.id)
.collect();
let mut counters = Vec::new();
for cpu in cpus {
let Some(row) = ctx.row_of(&corescout_mirror::entity::keys::logical_cpu(cpu))
else {
continue;
};
for (index, (_, config)) in EVENTS.iter().enumerate() {
match perf::open_counting_event(*config, cpu as i32) {
Ok(fd) => counters.push((row, index, fd)),
Err(_) => continue,
}
}
}
if counters.is_empty() {
return Err(Error::unsupported(
"perf_event_open returned nothing usable (perf_event_paranoid may be \
above 0, or this machine has no accessible PMU)",
));
}
self.counters = counters;
for (key, _) in EVENTS {
self.channels
.push(ctx.declare_channel(key, Unit::Count, Semantics::Cumulative));
}
Ok(())
}
}
fn observe(&mut self, out: &mut StateWriter<'_>) -> SensorOutcome {
#[allow(unused_mut)]
let mut outcome = SensorOutcome::default();
#[cfg(target_os = "linux")]
for (row, event, fd) in &self.counters {
match perf::read_counter(*fd) {
Some(value) => {
if let Some(channel) = self.channels.get(*event) {
out.set(*row, *channel, value as f64);
outcome.sample();
}
}
None => outcome.error(),
}
}
#[cfg(not(target_os = "linux"))]
let _ = out;
outcome
}
}
#[cfg(target_os = "linux")]
mod perf {
const PERF_TYPE_HARDWARE: u32 = 0;
const PERF_FLAG_FD_CLOEXEC: u64 = 8;
#[allow(dead_code)]
pub const ATTR_SIZE: usize = std::mem::size_of::<PerfEventAttrV0>();
#[repr(C)]
#[derive(Default)]
struct PerfEventAttrV0 {
type_: u32,
size: u32,
config: u64,
sample_period_or_freq: u64,
sample_type: u64,
read_format: u64,
flags: u64,
wakeup: u32,
bp_type: u32,
config1: u64,
}
pub fn open_counting_event(config: u64, cpu: i32) -> Result<i32, i32> {
let attr = PerfEventAttrV0 {
type_: PERF_TYPE_HARDWARE,
size: std::mem::size_of::<PerfEventAttrV0>() as u32,
config,
..Default::default()
};
debug_assert_eq!(std::mem::size_of::<PerfEventAttrV0>(), 64);
let fd = unsafe {
libc::syscall(
libc::SYS_perf_event_open,
&attr as *const PerfEventAttrV0,
-1i32, cpu, -1i32, PERF_FLAG_FD_CLOEXEC,
)
};
if fd < 0 {
return Err(std::io::Error::last_os_error().raw_os_error().unwrap_or(0));
}
Ok(fd as i32)
}
pub fn read_counter(fd: i32) -> Option<u64> {
let mut value: u64 = 0;
let read = unsafe {
libc::read(
fd,
&mut value as *mut u64 as *mut libc::c_void,
std::mem::size_of::<u64>(),
)
};
if read == std::mem::size_of::<u64>() as isize {
Some(value)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_event_set_is_distinct_and_named() {
let mut keys: Vec<&str> = EVENTS.iter().map(|(k, _)| *k).collect();
let mut configs: Vec<u64> = EVENTS.iter().map(|(_, c)| *c).collect();
keys.sort_unstable();
configs.sort_unstable();
let unique_keys = {
let mut v = keys.clone();
v.dedup();
v.len()
};
let unique_configs = {
let mut v = configs.clone();
v.dedup();
v.len()
};
assert_eq!(unique_keys, EVENTS.len());
assert_eq!(unique_configs, EVENTS.len());
}
#[test]
fn the_event_set_fits_in_the_general_purpose_counters() {
assert!(EVENTS.len() <= 4, "the PMU budget was exceeded");
}
#[test]
#[cfg(target_os = "linux")]
fn the_attr_struct_matches_the_kernel_abi_size() {
assert_eq!(super::perf::ATTR_SIZE, 64);
}
}