use std::path::{Path, PathBuf};
use std::time::Instant;
use super::{SysRoots, read_trimmed, read_u64, sorted_entries};
#[derive(Debug, Default)]
pub struct RaplMeter {
zones: Option<Vec<Zone>>,
last_sample: Option<Instant>,
}
#[derive(Debug)]
struct Zone {
energy_path: PathBuf,
max_range_uj: u64,
last_uj: Option<u64>,
}
impl RaplMeter {
pub fn new() -> Self {
Self::default()
}
pub fn sample(&mut self, roots: &SysRoots, now: Instant) -> Option<f32> {
let zones = self.zones.get_or_insert_with(|| discover_zones(roots));
if zones.is_empty() {
return None;
}
let elapsed = self
.last_sample
.map(|prev| now.saturating_duration_since(prev).as_secs_f64());
self.last_sample = Some(now);
let mut total_w = 0.0;
let mut complete = true;
for zone in zones.iter_mut() {
let current = read_u64(&zone.energy_path);
match (zone.last_uj, current, elapsed) {
(Some(prev), Some(cur), Some(secs)) => {
match power_from_delta(prev, cur, zone.max_range_uj, secs) {
Some(w) => total_w += w,
None => complete = false,
}
}
_ => complete = false,
}
zone.last_uj = current;
}
complete.then_some(total_w as f32)
}
}
fn discover_zones(roots: &SysRoots) -> Vec<Zone> {
let class_dir = roots.sys.join("class/powercap");
sorted_entries(&class_dir)
.into_iter()
.filter(|entry| {
entry.matches(':').count() == 1 && entry.ends_with(|c: char| c.is_ascii_digit())
})
.map(|entry| class_dir.join(entry))
.filter(|dir| is_package_zone(dir))
.filter_map(|dir| {
let energy_path = dir.join("energy_uj");
read_u64(&energy_path)?;
Some(Zone {
max_range_uj: read_u64(&dir.join("max_energy_range_uj")).unwrap_or(0),
energy_path,
last_uj: None,
})
})
.collect()
}
fn is_package_zone(dir: &Path) -> bool {
read_trimmed(&dir.join("name")).is_some_and(|name| name.starts_with("package"))
}
pub(crate) fn power_from_delta(
prev_uj: u64,
cur_uj: u64,
max_range_uj: u64,
secs: f64,
) -> Option<f64> {
if secs < 0.01 {
return None;
}
let delta = if cur_uj >= prev_uj {
cur_uj - prev_uj
} else if max_range_uj > prev_uj {
(max_range_uj - prev_uj) + cur_uj
} else {
return None;
};
Some(delta as f64 / 1_000_000.0 / secs)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::linux::testutil::write;
use std::time::Duration;
#[test]
fn power_from_plain_delta() {
let w = power_from_delta(1_000_000, 51_000_000, 0, 2.0).unwrap();
assert!((w - 25.0).abs() < 1e-9);
}
#[test]
fn power_across_wrap_around() {
let w = power_from_delta(90_000_000, 5_000_000, 100_000_000, 1.0).unwrap();
assert!((w - 15.0).abs() < 1e-9);
}
#[test]
fn power_rejects_tiny_interval_and_unknown_wrap() {
assert!(power_from_delta(0, 10, 0, 0.001).is_none());
assert!(power_from_delta(10, 5, 0, 1.0).is_none());
}
#[test]
fn meter_sums_packages_and_skips_subzones() {
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
write(root, "sys/class/powercap/intel-rapl:0/name", "package-0\n");
write(
root,
"sys/class/powercap/intel-rapl:0/energy_uj",
"1000000\n",
);
write(
root,
"sys/class/powercap/intel-rapl:0/max_energy_range_uj",
"65532610987\n",
);
write(root, "sys/class/powercap/intel-rapl:0:0/name", "core\n");
write(
root,
"sys/class/powercap/intel-rapl:0:0/energy_uj",
"500000\n",
);
let roots = SysRoots {
sys: root.join("sys"),
proc: root.join("proc"),
};
let mut meter = RaplMeter::new();
let t0 = Instant::now();
assert_eq!(meter.sample(&roots, t0), None);
write(
root,
"sys/class/powercap/intel-rapl:0/energy_uj",
"31000000\n",
);
write(
root,
"sys/class/powercap/intel-rapl:0:0/energy_uj",
"99000000\n",
);
let w = meter.sample(&roots, t0 + Duration::from_secs(1)).unwrap();
assert!((w - 30.0).abs() < 1e-3);
}
}