From 95e52020f94f694f85bf69bbd043ae63a9eb88df Mon Sep 17 00:00:00 2001 From: "NGnius (Graham)" Date: Fri, 6 Sep 2024 11:12:58 -0400 Subject: [PATCH] Add AMD RAPL power tracking interface --- crates/procbox/Cargo.toml | 3 + crates/procbox/src/cpu/amd.rs | 146 +++++++++++++++++++++++++ crates/procbox/src/cpu/basic_single.rs | 69 +++++++++--- crates/procbox/src/cpu/mod.rs | 4 + 4 files changed, 204 insertions(+), 18 deletions(-) create mode 100644 crates/procbox/src/cpu/amd.rs diff --git a/crates/procbox/Cargo.toml b/crates/procbox/Cargo.toml index 5bb0cba..e966128 100644 --- a/crates/procbox/Cargo.toml +++ b/crates/procbox/Cargo.toml @@ -8,3 +8,6 @@ description = "Power toolbox for processors" [dependencies] powerbox = { version = "0.1", path = "../core" } sysfuss = { version = "0.4", path = "../../../sysfs-nav" } + +[target.'cfg(any(target_arch = "x86_64", target_arch = "x86"))'.dependencies] +x86 = "0.52" diff --git a/crates/procbox/src/cpu/amd.rs b/crates/procbox/src/cpu/amd.rs new file mode 100644 index 0000000..01bdded --- /dev/null +++ b/crates/procbox/src/cpu/amd.rs @@ -0,0 +1,146 @@ +use std::io::{Seek, SeekFrom, Read}; + +use powerbox::{Power, PowerOp}; +use powerbox::primitives::Value; + +use crate::cpu::{CpuPower, CpuPowerOp}; + +use super::BasicCpu; + +const CPUDEV_ROOT: &str = "/dev/cpu/"; +const CPUDEV_MSR: &str = "msr"; + +const MSR_RAPL_POWER_UNIT: u64 = 0xC0010299; + +pub struct AmdCpu { + basic: BasicCpu, +} + +impl AmdCpu { + fn msr_path(&self) -> Option { + self.basic.index.map(|index| std::path::PathBuf::from(CPUDEV_ROOT).join(index.to_string()).join(CPUDEV_MSR)) + } + + fn is_compatible() -> bool { + #[cfg(any(target_arch = "x86_64", target_arch = "x86"))] + if let Some(vendor_info) = x86::cpuid::CpuId::new().get_vendor_info() { + vendor_info.as_str() == "AuthenticAMD" + } else { + false + } + #[cfg(not(any(target_arch = "x86_64", target_arch = "x86")))] + {false} + } +} + +impl core::ops::Deref for AmdCpu { + type Target = BasicCpu; + fn deref(&self) -> &Self::Target { + &self.basic + } +} + +#[derive(Debug)] +pub struct NotAmd; + +impl core::fmt::Display for NotAmd { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "non-AMD CPU") + } +} + +impl std::error::Error for NotAmd {} + +impl std::convert::TryFrom for AmdCpu { + type Error = NotAmd; + + fn try_from(basic: BasicCpu) -> Result { + if Self::is_compatible() { + Ok(Self { basic }) + } else { + Err(NotAmd) + } + } +} + +pub enum AmdPowerUsage { + Get(GetAmdPowerUsage), +} + +impl PowerOp for AmdPowerUsage { + fn is_eq_op(&self, other: &Self) -> bool { + match self { + Self::Get(_) => matches!(other, Self::Get(_)), + } + } +} + +impl CpuPowerOp for AmdPowerUsage {} + +impl Power for AmdCpu { + fn is_on(&self) -> bool { + self.basic.is_online() + } + + fn is_available(&self) -> bool { + self.basic.is_exists() + } + + fn supported_operations(&self) -> Box> { + Box::new( + Power::::supported_operations(self).map(core::convert::Into::into) + ) + } + + fn act(&self, op: AmdPowerUsage) -> Result { + match op { + AmdPowerUsage::Get(get) => self.act(get).map(|x| Value::UInt(x as _)), + } + } +} + +impl CpuPower for AmdCpu {} + +pub struct GetAmdPowerUsage; // number + +impl PowerOp for GetAmdPowerUsage { + fn is_eq_op(&self, _: &Self) -> bool { + true + } +} + +impl CpuPowerOp for GetAmdPowerUsage {} + +impl Power for AmdCpu { + fn is_on(&self) -> bool { + self.basic.is_online() + } + + fn is_available(&self) -> bool { + self.basic.is_exists() + } + + fn supported_operations(&self) -> Box> { + if let Some(msr_path) = self.msr_path() { + if Self::is_compatible() && msr_path.exists() { return Box::new(core::iter::once(GetAmdPowerUsage)); } + } + Box::new(core::iter::empty()) + } + + fn act(&self, _: GetAmdPowerUsage) -> Result { + let mut buf = [0u8; 8]; + let path = self.msr_path().ok_or(powerbox::PowerError::InvalidInput)?; + let mut file = std::fs::File::open(path).map_err(powerbox::PowerError::Io)?; + file.seek(SeekFrom::Start(MSR_RAPL_POWER_UNIT)).map_err(powerbox::PowerError::Io)?; + file.read(&mut buf).map_err(powerbox::PowerError::Io)?; + Ok(u64::from_le_bytes(buf)) + } +} + +impl CpuPower for AmdCpu {} + +impl core::convert::Into for GetAmdPowerUsage { + fn into(self) -> AmdPowerUsage { + AmdPowerUsage::Get(self) + } +} diff --git a/crates/procbox/src/cpu/basic_single.rs b/crates/procbox/src/cpu/basic_single.rs index e51ad09..9cbd020 100644 --- a/crates/procbox/src/cpu/basic_single.rs +++ b/crates/procbox/src/cpu/basic_single.rs @@ -22,31 +22,55 @@ const CPUFREQ_SCALING_DRIVER: &str = "cpufreq/scaling_driver"; /// General single CPU functionality pub struct BasicCpu { - sysfs: BasicEntityPath, + pub(super) sysfs: BasicEntityPath, + pub(super) index: Option, } impl BasicCpu { - fn is_online(&self) -> bool { + pub(super) fn is_online(&self) -> bool { self.sysfs.attribute::(CPU_ONLINE).is_ok_and(|num| num == 1) } - fn is_exists(&self) -> bool { + pub(super) fn is_exists(&self) -> bool { self.sysfs.as_ref().exists() } pub fn new(cpu: usize) -> Self { Self { sysfs: BasicEntityPath::new(std::path::PathBuf::from(super::basic_general::DEFAULT_CPU_ROOT).join(format!("cpu{}", cpu))), + index: Some(cpu), } } pub fn with_root(p: impl AsRef) -> Self { + let index = p.as_ref().to_str().and_then(|s| parse_cpu_index(s)); Self { sysfs: BasicEntityPath::new(p), + index, } } } +fn parse_cpu_index(path: &str) -> Option { + // usually "/sys/devices/system/cpu/cpu" with optional ending / + if let Some(stripped) = path.strip_prefix("/sys/devices/system/cpu/") { + let mut iter = stripped.char_indices(); + if iter.next().is_some_and(|c| c.1 == 'c') + && iter.next().is_some_and(|c| c.1 == 'p') + && iter.next().is_some_and(|c| c.1 == 'u') { + let first_valid = 3; + let mut last_valid = first_valid; + while let Some(maybe_digit) = iter.next() { + if maybe_digit.1.is_ascii_digit() { + last_valid = maybe_digit.0; + } + } + return Some(stripped[first_valid..last_valid+1].parse().unwrap()); // a string of digits is always a valid number + } + } + None +} + impl Power for BasicCpu { fn is_on(&self) -> bool { self.is_online() @@ -57,11 +81,15 @@ impl Power for BasicCpu { } fn supported_operations(&self) -> Box> { - todo!() + Box::new(Power::::supported_operations(self).map(core::convert::Into::::into) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into))) } - fn act(&self, _op: BasicCpuOp) -> Result { - todo!() + fn act(&self, op: BasicCpuOp) -> Result { + match op { + BasicCpuOp::Online(online) => Power::::act(self, online), + BasicCpuOp::CpuFreq(cpu_freq) => Power::::act(self, cpu_freq), + } } } @@ -303,7 +331,7 @@ impl PowerOp for CpuFreq { impl CpuPowerOp for CpuFreq {} -impl Power>> for BasicCpu { +impl Power for BasicCpu { fn is_on(&self) -> bool { self.is_online() } @@ -316,24 +344,29 @@ impl Power>> for BasicCpu { Box::new( Power::::supported_operations(self).map(core::convert::Into::::into) .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) - //.chain(Power::::supported_operations(self).map(core::convert::Into::::into)) - //.chain(Power::::supported_operations(self).map(core::convert::Into::::into)) - //.chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) + .chain(Power::::supported_operations(self).map(core::convert::Into::::into)) ) } - fn act(&self, op: CpuFreq) -> Result>, powerbox::PowerError> { + fn act(&self, op: CpuFreq) -> Result { match op { - CpuFreq::AffectedCpus(affected) => Power::::act(self, affected).map(Value::Custom), - CpuFreq::RelatedCpus(related) => Power::::act(self, related).map(Value::Custom), - //CpuFreq::CpuInfo(cpu_info) => if let Self::CpuInfo(other) = other { cpu_info.is_eq_op(other) } else { false }, - //CpuFreq::Energy(en) => if let Self::Energy(other) = other { en.is_eq_op(other) } else { false }, - //CpuFreq::Scaling(scaling) => if let Self::Scaling(other) = other { scaling.is_eq_op(other) } else { false }, - _ => todo!(), + CpuFreq::AffectedCpus(affected) => Power::::act(self, affected).map(|x| Value::Custom(Box::new(x) as _)), + CpuFreq::RelatedCpus(related) => Power::::act(self, related).map(|x| Value::Custom(Box::new(x) as _)), + CpuFreq::CpuInfo(cpu_info) => Power::::act(self, cpu_info).map(|int| Value::UInt(int as _)), + CpuFreq::Energy(en) => Power::::act(self, en).map(Value::into_any), + CpuFreq::Scaling(scaling) => Power::::act(self, scaling).map(Value::into_any), } } } +impl core::convert::Into for CpuFreq { + fn into(self) -> BasicCpuOp { + BasicCpuOp::CpuFreq(self) + } +} + pub struct GetCpuInfoMinFreq; // number impl PowerOp for GetCpuInfoMinFreq { @@ -529,7 +562,7 @@ impl Power for BasicCpu { fn act(&self, op: EnergyPerf) -> Result { match op { EnergyPerf::AvailablePreferences(avail) => self.act(avail).map(|x| Value::Custom(Box::new(x) as _)), - EnergyPerf::Preference(pref) => self.act(pref).map(Value::into_any), + EnergyPerf::Preference(pref) => self.act(pref), } } } diff --git a/crates/procbox/src/cpu/mod.rs b/crates/procbox/src/cpu/mod.rs index aad8f0a..a50b52b 100644 --- a/crates/procbox/src/cpu/mod.rs +++ b/crates/procbox/src/cpu/mod.rs @@ -9,3 +9,7 @@ pub use basic_general::BasicCpus; #[allow(missing_docs)] pub mod basic_single; pub use basic_single::BasicCpu; + +#[allow(missing_docs)] +pub mod amd; +pub use amd::AmdCpu;