ryzen_reader/
lib.rs

1const AMD_TIME_UNIT_MASK: u64 = 0xF0000;
2const AMD_ENERGY_UNIT_MASK: u64 = 0x1F00;
3const AMD_POWER_UNIT_MASK: u64 = 0xF;
4
5const MAX_CPUS: u32 = 1024;
6
7use std::fs::{File, OpenOptions};
8use std::io::{Read, Seek, SeekFrom};
9use std::mem::size_of;
10use std::str;
11use std::sync::Mutex;
12use std::thread::sleep;
13use std::time::Duration;
14use thiserror::Error;
15
16#[repr(u64)]
17enum MsrValue {
18    PowerUnit = 0xC0010299,
19    CoreEnergy = 0xC001029A,
20    PackageEnergy = 0xC001029B,
21}
22
23#[derive(Error, Debug)]
24#[non_exhaustive]
25pub enum Error {
26    #[error("Permission denied when trying to open msr, are you running as root?")]
27    PermissionDenied,
28    #[error("Core with id not found")]
29    CoreNotFound,
30    #[error("IO error when trying to open msr: {0}")]
31    IO(#[source] std::io::Error),
32    #[error("No cores detected")]
33    NoCores,
34    #[error("Invalid package data")]
35    InvalidPackage,
36}
37
38impl From<std::io::Error> for Error {
39    fn from(err: std::io::Error) -> Self {
40        match err.kind() {
41            std::io::ErrorKind::PermissionDenied => Self::PermissionDenied,
42            std::io::ErrorKind::NotFound => Self::CoreNotFound,
43            _ => Error::IO(err),
44        }
45    }
46}
47
48struct Core {
49    handle: Mutex<File>,
50    package: u32,
51}
52
53impl Core {
54    pub fn open(cpu_id: u32) -> Result<Self, Error> {
55        let mut data = [0; 4];
56        let mut package_handle = OpenOptions::new().read(true).open(&format!(
57            "/sys/devices/system/cpu/cpu{}/topology/physical_package_id",
58            cpu_id
59        ))?;
60        package_handle.read(&mut data)?;
61        let package: u32 = str::from_utf8(&data)
62            .map_err(|_| Error::InvalidPackage)?
63            .trim_end_matches('\u{0}')
64            .trim()
65            .parse()
66            .map_err(|_| Error::InvalidPackage)?;
67
68        let handle = OpenOptions::new()
69            .read(true)
70            .write(false)
71            .open(&format!("/dev/cpu/{}/msr", cpu_id))?;
72
73        Ok(Core {
74            handle: Mutex::new(handle),
75            package,
76        })
77    }
78
79    pub fn read(&self, value: MsrValue) -> Result<u64, Error> {
80        let mut handle = self.handle.lock().unwrap();
81        handle.seek(SeekFrom::Start(value as u64))?;
82
83        let mut data = [0; size_of::<u64>()];
84        handle.read(&mut data)?;
85        Ok(u64::from_le_bytes(data))
86    }
87}
88
89#[derive(Debug, Clone)]
90struct CorePower {
91    core_power: f64,
92    package_power: f64,
93    package: u32,
94}
95
96#[derive(Debug, Clone)]
97pub struct CpuPower {
98    cores: Vec<CorePower>,
99}
100
101impl CpuPower {
102    /// Get an iterator for all cpu cores in the system and their power draw in watt
103    pub fn cores<'a>(&'a self) -> impl Iterator<Item = f64> + 'a {
104        self.cores.iter().map(|core| core.core_power)
105    }
106
107    /// Get an iterator for all cpu packages in the system and their power draw in watt
108    pub fn packages<'a>(&'a self) -> impl Iterator<Item = f64> + 'a {
109        let mut last_package = u32::max_value();
110
111        let mut packages = Vec::new();
112
113        for core in self.cores.iter() {
114            if core.package != last_package {
115                last_package = core.package;
116                packages.push(core.package_power)
117            }
118        }
119
120        packages.into_iter()
121    }
122}
123
124#[derive(Debug)]
125struct PowerUnits {
126    time_unit: f64,
127    energy_unit: f64,
128    power_unit: f64,
129}
130
131pub struct CpuInfo {
132    cores: Vec<Core>,
133    units: PowerUnits,
134}
135
136/// Struct that allows reading of cpu power info
137///
138/// # Example
139///
140/// ```rust
141/// # use ryzen_reader::{CpuInfo, Error};
142/// #
143/// # fn main() -> Result<(), Error> {
144///     let cpu = CpuInfo::new()?;
145///     let power = cpu.read()?;
146///
147///     println!("Package power:");
148///     for (package, usage) in power.packages().enumerate() {
149///         println!("\t#{}: {:.2}W", package, usage);
150///     }
151///     println!("Core power:");
152///     for (core, usage) in power.cores().enumerate() {
153///         println!("\t#{}: {:.2}W", core, usage);
154///     }
155/// #     Ok(())
156/// # }
157///```
158impl CpuInfo {
159    pub fn new() -> Result<Self, Error> {
160        let mut cores = Vec::with_capacity(8);
161
162        for i in 0..MAX_CPUS {
163            match Core::open(i) {
164                Ok(core) => cores.push(core),
165                Err(Error::CoreNotFound) => break,
166                Err(e) => return Err(e),
167            }
168        }
169
170        if cores.is_empty() {
171            return Err(Error::NoCores);
172        }
173
174        let units = cores[0].read(MsrValue::PowerUnit)?;
175        let time_unit = (units & AMD_TIME_UNIT_MASK) >> 16;
176        let energy_unit = (units & AMD_ENERGY_UNIT_MASK) >> 8;
177        let power_unit = units & AMD_POWER_UNIT_MASK;
178
179        let time_unit = 0.5f64.powi(time_unit as i32);
180        let energy_unit = 0.5f64.powi(energy_unit as i32);
181        let power_unit = 0.5f64.powi(power_unit as i32);
182
183        let units = PowerUnits {
184            time_unit,
185            energy_unit,
186            power_unit,
187        };
188
189        Ok(CpuInfo { cores, units })
190    }
191
192    /// Read the cpu power levels
193    ///
194    /// Note that this method will block for ~10ms
195    pub fn read(&self) -> Result<CpuPower, Error> {
196        let start = self.read_raw()?;
197        sleep(Duration::from_millis(10));
198        let end = self.read_raw()?;
199
200        let cores = start
201            .into_iter()
202            .zip(end.into_iter())
203            .map(|(start, end)| CorePower {
204                core_power: (end.core_power - start.core_power) * 100.0,
205                package_power: (end.package_power - start.package_power) * 100.0,
206                package: start.package,
207            })
208            .collect();
209
210        Ok(CpuPower { cores })
211    }
212
213    fn read_raw(&self) -> Result<Vec<CorePower>, Error> {
214        self.cores
215            .iter()
216            .map(|core| {
217                let core_power = core.read(MsrValue::CoreEnergy)? as f64 * self.units.energy_unit;
218                let package_power =
219                    core.read(MsrValue::PackageEnergy)? as f64 * self.units.energy_unit;
220                Ok(CorePower {
221                    core_power,
222                    package_power,
223                    package: core.package,
224                })
225            })
226            .collect()
227    }
228}