diff --git a/.gitignore b/.gitignore index 6985cf1..196e176 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,8 @@ Cargo.lock # MSVC Windows builds of rustc generate these, which store debugging information *.pdb + + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7479b25 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "linux-power-meter-rs" +version = "0.1.0" +edition = "2024" + +[dependencies] +battery = "0.7.8" +nvml-wrapper = "0.11.0" +x86 = "0.52.0" diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..340b3ec --- /dev/null +++ b/src/main.rs @@ -0,0 +1,108 @@ +use battery::{Manager, units::power::watt}; +use nvml_wrapper::Nvml; + +mod nvml; +mod rapl; +mod rapl_power_direct; + +type Error = Box; +const TIME_DELTA_SECS: f32 = 0.1; + +fn main() -> Result<(), Error> { + let manager = Manager::new()?; + let batteries = manager.batteries()?; + + let mut total_usage: f32 = 0.; + + println!("Batteries:"); + // let total_usage = batteries + // .enumerate() + // .map(|(i, bat)| -> Result { + // let bat = bat?; + + // print!("-- Bat{i} = "); + + // match bat.state() { + // battery::State::Unknown => println!("Unknown"), + // battery::State::Charging => println!("Charging (No power draw)"), + // battery::State::Discharging => { + // let rate = bat.energy_rate().get::(); + // println!("Discharging, {rate}W"); + // return Ok(rate); + // } + // battery::State::Empty => println!("Empty"), + // battery::State::Full => println!("Full"), + // _ => todo!(), + // } + + // Err("Could not get battery discharge rate".into()) + // }) + // .map(|res| match res { + // Ok(rate) => rate, + // Err(e) => { + // println!("{e}:?"); + // panic!("Not all batteries could be metered!") + // } + // }) + // .sum::(); + + for (i, bat) in batteries.enumerate() { + let bat = bat?; + + print!("-- Bat{i} = "); + + match bat.state() { + battery::State::Unknown => println!("Unknown"), + battery::State::Charging => println!("Charging (No power draw)"), + battery::State::Discharging => { + let rate = bat.energy_rate().get::(); + println!("Discharging, {rate}W"); + // return Ok(rate); + total_usage += rate; + continue; + } + battery::State::Empty => println!("Empty"), + battery::State::Full => println!("Full"), + _ => todo!(), + } + + println!("All batteries are not in the discharging state!"); + // panic!() + } + + if let Ok(nvml) = Nvml::init() { + println!("NVML Devices:"); + let device_count = nvml.device_count()?; + for n in 0..device_count { + let device = nvml.device_by_index(n)?; + // device.power + let power_w = device.power_usage()? as f32 / 1000.; + + print!("-- Dev {n} = {power_w}W"); + if let Ok(limit) = device.power_management_limit() { + let limit_w = limit as f32 / 1000.; + print!(" (Limited to {limit_w}W)") + } + print!("\n"); + + total_usage -= power_w; + } + } else { + println!("No NVML Devices"); + } + + println!("RAPL:"); + for (cpu_power_type, wattage) in rapl::get_cpu_power()? { + println!("-- {cpu_power_type:?}, {wattage}W"); + total_usage -= wattage; + } + + if let Ok(power) = rapl_power_direct::calculate_power() { + // let power = ; + println!("RAPL Package Power: {power}W",); + } + + println!("## Unaccounted: {total_usage}"); + + Ok(()) +} diff --git a/src/nvml.rs b/src/nvml.rs new file mode 100644 index 0000000..e69de29 diff --git a/src/rapl.rs b/src/rapl.rs new file mode 100644 index 0000000..aa6cf7e --- /dev/null +++ b/src/rapl.rs @@ -0,0 +1,166 @@ +use std::fs; +use std::io; +use std::path::PathBuf; +use std::thread; +use std::time::Duration; + +use crate::TIME_DELTA_SECS; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum RAPLPowerID { + Package(usize), // intel-rapl:N + Core(usize), // intel-rapl:N:0 + Uncore(usize), // intel-rapl:N:1 + DRAM(usize), // intel-rapl:N:2 + Other(usize, String), // intel-rapl:N:X (unknown subdomains) +} + +impl RAPLPowerID { + fn from_path(path_str: &str) -> Option { + if !path_str.starts_with("intel-rapl:") { + return None; + } + + let parts: Vec<&str> = path_str.strip_prefix("intel-rapl:")?.split(':').collect(); + + match parts.len() { + 1 => { + // intel-rapl:N (Package) + let cpu_id = parts[0].parse::().ok()?; + Some(RAPLPowerID::Package(cpu_id)) + } + 2 => { + // intel-rapl:N:X (Subdomain) + let cpu_id = parts[0].parse::().ok()?; + let subdomain = parts[1]; + + match subdomain { + "0" => Some(RAPLPowerID::Core(cpu_id)), + "1" => Some(RAPLPowerID::Uncore(cpu_id)), + "2" => Some(RAPLPowerID::DRAM(cpu_id)), + _ => Some(RAPLPowerID::Other(cpu_id, subdomain.to_string())), + } + } + _ => None, + } + } + + fn to_path(&self) -> PathBuf { + let path_str = match self { + RAPLPowerID::Package(id) => format!("intel-rapl:{}", id), + RAPLPowerID::Core(id) => format!("intel-rapl:{}:0", id), + RAPLPowerID::Uncore(id) => format!("intel-rapl:{}:1", id), + RAPLPowerID::DRAM(id) => format!("intel-rapl:{}:2", id), + RAPLPowerID::Other(id, sub) => format!("intel-rapl:{}:{}", id, sub), + }; + PathBuf::from("/sys/class/powercap") + .join(path_str) + .join("energy_uj") + } + + fn display_name(&self) -> String { + match self { + RAPLPowerID::Package(id) => format!("Package {}", id), + RAPLPowerID::Core(id) => format!("Core {} (Package {})", id, id), + RAPLPowerID::Uncore(id) => format!("Uncore {} (Package {})", id, id), + RAPLPowerID::DRAM(id) => format!("DRAM {} (Package {})", id, id), + RAPLPowerID::Other(id, sub) => format!("Unknown-{} (Package {})", sub, id), + } + } +} + +fn list_rapl_domains() -> io::Result> { + let powercap_dir = "/sys/class/powercap"; + let mut domains = Vec::new(); + + let entries = fs::read_dir(powercap_dir)?; + + for entry in entries { + let entry = entry?; + let filename = entry.file_name(); + let filename_str = filename.to_string_lossy(); + + if let Some(power_id) = RAPLPowerID::from_path(&filename_str) { + let energy_file = entry.path().join("energy_uj"); + if energy_file.exists() { + domains.push(power_id); + } + } + } + + domains.sort_by_key(|d| match d { + RAPLPowerID::Package(id) => (*id, 0), + RAPLPowerID::Core(id) => (*id, 1), + RAPLPowerID::Uncore(id) => (*id, 2), + RAPLPowerID::DRAM(id) => (*id, 3), + RAPLPowerID::Other(id, _) => (*id, 99), + }); + + Ok(domains) +} + +fn read_cpu_energy(power_id: &RAPLPowerID) -> io::Result { + let path = power_id.to_path(); + let content = fs::read_to_string(&path)?; + content + .trim() + .parse::() + .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) +} + +pub fn get_cpu_power() -> Result, crate::Error> { + // println!("Reading CPU power consumption from RAPL interface...\n"); + + let domains = list_rapl_domains()?; + + if domains.is_empty() { + println!("No RAPL energy readings found."); + println!("This may indicate:"); + println!(" - No Intel RAPL support on this system"); + println!(" - Insufficient permissions (try running with sudo)"); + println!(" - The intel_rapl kernel module is not loaded"); + panic!() + } + + // Take initial readings + let mut initial_readings = Vec::new(); + for domain in &domains { + match read_cpu_energy(domain) { + Ok(energy) => initial_readings.push((domain.clone(), energy)), + Err(e) => { + eprintln!("Warning: Failed to read {}: {}", domain.display_name(), e); + panic!() + } + } + } + + // Wait 1 second + thread::sleep(Duration::from_secs_f32(TIME_DELTA_SECS)); + + // Take final readings and calculate power + // for in initial_readings { + Ok(initial_readings + .into_iter() + .map(|(domain, initial_energy)| match read_cpu_energy(&domain) { + Ok(final_energy) => { + let energy_diff = if final_energy >= initial_energy { + final_energy - initial_energy + } else { + (u64::MAX - initial_energy) + final_energy + }; + + let power_watts = energy_diff as f32 / (TIME_DELTA_SECS * 1_000_000.0); + + (domain, power_watts) + } + Err(e) => { + eprintln!( + "Error reading final energy for {}: {}", + domain.display_name(), + e + ); + panic!(); + } + }) + .collect()) +} diff --git a/src/rapl_power_direct.rs b/src/rapl_power_direct.rs new file mode 100644 index 0000000..1617a5b --- /dev/null +++ b/src/rapl_power_direct.rs @@ -0,0 +1,70 @@ +use std::fs::File; +use std::io::{Read, Seek, SeekFrom}; +use std::thread; +use std::time::Duration; +// use x86::msr::{MSR_PKG_ENERGY_STATUS, MSR_RAPL_POWER_UNIT, rdmsr}; + +use crate::{Error, TIME_DELTA_SECS}; + +const MSR_RAPL_POWER_UNIT: u64 = 0x606; +const MSR_PKG_ENERGY_STATUS: u64 = 0x611; + +/// Read an MSR register via /dev/cpu/*/msr +fn read_msr(cpu: usize, msr: u64) -> Result { + let path = format!("/dev/cpu/{}/msr", cpu); + let mut file = File::open(&path)?; + + // Seek to the MSR offset + file.seek(SeekFrom::Start(msr))?; + + // Read 8 bytes (u64) + let mut buffer = [0u8; 8]; + file.read_exact(&mut buffer)?; + + Ok(u64::from_ne_bytes(buffer)) +} + +/// Calculate the energy unit from MSR_RAPL_POWER_UNIT +fn get_energy_unit(cpu: usize) -> Result { + let power_unit = read_msr(cpu, MSR_RAPL_POWER_UNIT)?; + // Energy unit is in bits 12:8 + let energy_unit_bits = (power_unit >> 8) & 0x1F; + // Energy unit = 1 / 2^(energy_unit_bits) Joules + Ok(1.0 / (1u64 << energy_unit_bits) as f32) +} + +/// Read the current energy counter value +fn read_energy_counter(cpu: usize) -> Result { + read_msr(cpu, MSR_PKG_ENERGY_STATUS) +} + +/// Calculate power consumption over a time interval +/// Returns power in Watts +pub fn calculate_power() -> Result { + let cpu = 0; + + let energy_unit = get_energy_unit(cpu)?; + + // Read initial energy + let energy_start = read_energy_counter(cpu)?; + + // Wait for the specified duration + thread::sleep(Duration::from_secs_f32(TIME_DELTA_SECS)); + + // Read final energy + let energy_end = read_energy_counter(cpu)?; + + // Handle counter wraparound (32-bit counter) + let energy_diff = if energy_end >= energy_start { + energy_end - energy_start + } else { + // Counter wrapped around + (u32::MAX as u64 - energy_start) + energy_end + 1 + }; + + // Convert to Joules + let energy_joules = energy_diff as f32 * energy_unit; + + // Calculate power (Watts = Joules / seconds) + Ok(energy_joules / TIME_DELTA_SECS) +}