use crate::config::AppMatch;
use crate::platform::counter::{Interval, WrappingCounter, repeat_failure, warn_unavailable};
use crate::platform::cpu_usage::AppMonitor;
use crate::platform::gpu::VendorGpu;
use crate::platform::procfs::{self, MAX_KERNEL_FILE_BYTES, read_capped};
use crate::sensor::{
AppCpuUtilization, CpuUtilization, Platform, PowerSensor, ProcessCpuUtilization,
};
use crate::{Error, Result};
use std::fs;
use std::path::{Path, PathBuf};
use std::time::Duration;
const POWERCAP_PATH: &str = "/sys/class/powercap";
const SENSOR: &str = "Intel RAPL";
#[derive(Debug, Default)]
pub(crate) struct LinuxPlatform;
impl Platform for LinuxPlatform {
fn cpu(&self) -> Box<dyn PowerSensor> {
Box::new(RaplSensor::new(Rapl::open()))
}
fn gpu(&self) -> Box<dyn PowerSensor> {
Box::new(VendorGpu)
}
fn cpu_usage(&self) -> Box<dyn CpuUtilization> {
Box::new(procfs::cpu_usage())
}
fn process_cpu_usage(&self) -> Option<Box<dyn ProcessCpuUtilization>> {
Some(procfs::process_tracker())
}
fn app_cpu_usage(
&self,
refresh_interval: Duration,
app_match: AppMatch,
) -> Option<Box<dyn AppCpuUtilization>> {
Some(AppMonitor::boxed(
procfs::process_tracker,
refresh_interval,
app_match,
))
}
}
#[derive(Debug)]
struct RaplDomain {
energy_path: PathBuf,
counter: WrappingCounter,
}
impl RaplDomain {
fn energy_delta(&mut self) -> Result<f64> {
let current = read_joules(&self.energy_path)?;
Ok(self.counter.delta(current))
}
}
#[derive(Debug)]
struct Rapl {
domains: Vec<RaplDomain>,
}
impl Rapl {
fn open() -> Result<Self> {
Self::open_under(Path::new(POWERCAP_PATH))
}
fn open_under(root: &Path) -> Result<Self> {
let mut domains = Vec::new();
let mut permission_error = None;
for entry in package_domain_paths(root) {
let energy_path = entry.join("energy_uj");
match read_joules(&energy_path) {
Ok(last_energy) => {
let max_energy = read_joules(&entry.join("max_energy_range_uj"))?;
domains.push(RaplDomain {
energy_path,
counter: WrappingCounter::new(max_energy, last_energy),
});
}
Err(e @ Error::PermissionDenied { .. }) => permission_error = Some(e),
Err(e) => {
log::debug!("skipping RAPL domain {}: {e}", energy_path.display());
}
}
}
if let Some(e) = permission_error.filter(|_| domains.is_empty()) {
return Err(e);
}
if domains.is_empty() {
return Err(Error::sensor(
SENSOR,
format!("no readable package domain under {}", root.display()),
));
}
Ok(Self { domains })
}
}
struct RaplSensor {
rapl: Result<Rapl>,
interval: Interval,
}
impl RaplSensor {
fn new(rapl: Result<Rapl>) -> Self {
warn_unavailable(SENSOR, &rapl);
Self {
rapl,
interval: Interval::default(),
}
}
}
impl PowerSensor for RaplSensor {
fn power(&mut self) -> Result<f64> {
let rapl = match &mut self.rapl {
Ok(rapl) => rapl,
Err(e) => return Err(repeat_failure(SENSOR, e)),
};
let mut joules = 0.0;
for domain in &mut rapl.domains {
joules += domain.energy_delta()?;
}
let Some(seconds) = self.interval.tick() else {
return Ok(0.0);
};
Ok(joules / seconds)
}
}
fn package_domain_paths(root: &Path) -> Vec<PathBuf> {
let Ok(entries) = fs::read_dir(root) else {
return Vec::new();
};
let mut paths: Vec<PathBuf> = entries
.flatten()
.map(|entry| entry.path())
.filter(|path| {
let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
return false;
};
let Some(suffix) = name.strip_prefix("intel-rapl:") else {
return false;
};
suffix.chars().all(|c| c.is_ascii_digit())
&& read_capped(&path.join("name"), MAX_KERNEL_FILE_BYTES)
.is_ok_and(|name| name.trim().starts_with("package-"))
})
.collect();
paths.sort();
paths
}
fn read_joules(path: &Path) -> Result<f64> {
let raw = read_capped(path, MAX_KERNEL_FILE_BYTES).map_err(|e| {
if e.kind() == std::io::ErrorKind::PermissionDenied {
Error::permission(
SENSOR,
format!(
"{} is not readable; run as root or grant read access \
(kernels since 5.10 restrict RAPL counters)",
path.display()
),
)
} else {
Error::sensor(SENSOR, format!("{}: {e}", path.display()))
}
})?;
let microjoules: f64 = raw.trim().parse().map_err(|e| {
Error::sensor(
SENSOR,
format!("{} does not hold a number: {e}", path.display()),
)
})?;
Ok(microjoules / 1_000_000.0)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn powercap(entries: &[(&str, &str, Option<&str>, &str)]) -> TempDir {
let root = TempDir::new().expect("temp dir");
for (dir, name, energy, max_energy) in entries {
let domain = root.path().join(dir);
fs::create_dir(&domain).expect("create domain");
fs::write(domain.join("name"), name).expect("write name");
if let Some(energy) = energy {
fs::write(domain.join("energy_uj"), energy).expect("write energy");
}
fs::write(domain.join("max_energy_range_uj"), max_energy).expect("write max");
}
root
}
#[test]
fn only_top_level_package_domains_are_summed() {
let root = powercap(&[
("intel-rapl:0", "package-0", Some("1000000"), "60000000"),
("intel-rapl:1", "package-1", Some("2000000"), "60000000"),
("intel-rapl:0:0", "core", Some("500000"), "60000000"),
("intel-rapl:2", "psys", Some("900000"), "60000000"),
("not-a-rapl-domain", "package-9", Some("100000"), "60000000"),
]);
let paths = package_domain_paths(root.path());
let names: Vec<_> = paths
.iter()
.map(|p| p.file_name().unwrap().to_string_lossy().into_owned())
.collect();
assert_eq!(names, ["intel-rapl:0", "intel-rapl:1"]);
}
#[test]
fn a_tree_with_no_package_domain_is_rejected() {
let root = powercap(&[("intel-rapl:0", "psys", Some("1000000"), "60000000")]);
let error = Rapl::open_under(root.path()).unwrap_err();
assert!(
error.to_string().contains("no readable package domain"),
"unexpected error: {error}"
);
}
#[test]
fn a_missing_powercap_tree_is_rejected_rather_than_panicking() {
let error = Rapl::open_under(Path::new("/definitely/not/here")).unwrap_err();
assert!(matches!(error, Error::SensorUnavailable { .. }));
}
fn sensor(root: &Path) -> RaplSensor {
RaplSensor::new(Rapl::open_under(root))
}
#[test]
fn power_is_energy_over_time_across_every_package() {
let root = powercap(&[
("intel-rapl:0", "package-0", Some("1000000"), "60000000"),
("intel-rapl:1", "package-1", Some("2000000"), "60000000"),
]);
let mut sensor = sensor(root.path());
assert_eq!(sensor.power().unwrap(), 0.0);
fs::write(root.path().join("intel-rapl:0/energy_uj"), "2000000").unwrap();
fs::write(root.path().join("intel-rapl:1/energy_uj"), "3000000").unwrap();
let watts = sensor.power().unwrap();
assert!(watts > 0.0, "expected positive power, got {watts}");
}
#[test]
fn a_wrapped_counter_does_not_report_negative_power() {
let root = powercap(&[("intel-rapl:0", "package-0", Some("59000000"), "60000000")]);
let mut sensor = sensor(root.path());
assert_eq!(sensor.power().unwrap(), 0.0);
fs::write(root.path().join("intel-rapl:0/energy_uj"), "1000000").unwrap();
assert!(sensor.power().unwrap() > 0.0);
}
#[test]
fn a_junk_wrap_point_is_reported_rather_than_guessed() {
let root = powercap(&[("intel-rapl:0", "package-0", Some("1000000"), "not a number")]);
let error = Rapl::open_under(root.path()).unwrap_err();
assert!(
error.to_string().contains("does not hold a number"),
"unexpected error: {error}"
);
}
#[test]
fn an_unreadable_domain_is_skipped_rather_than_failing_the_others() {
let root = powercap(&[
("intel-rapl:0", "package-0", None, "60000000"),
("intel-rapl:1", "package-1", Some("junk"), "60000000"),
("intel-rapl:2", "package-2", Some("2000000"), "60000000"),
]);
assert!(Rapl::open_under(root.path()).is_ok());
}
}