This commit is contained in:
Michael Mikovsky
2025-12-15 22:28:28 -07:00
parent f7bb26ec1e
commit 10199a83a5
6 changed files with 358 additions and 0 deletions
+5
View File
@@ -12,3 +12,8 @@ Cargo.lock
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
# Added by cargo
/target
+9
View File
@@ -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"
+108
View File
@@ -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<dyn std::error::Error>;
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<f32, Error> {
// 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::<watt>();
// 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::<f32>();
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::<watt>();
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(())
}
View File
+166
View File
@@ -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<Self> {
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::<usize>().ok()?;
Some(RAPLPowerID::Package(cpu_id))
}
2 => {
// intel-rapl:N:X (Subdomain)
let cpu_id = parts[0].parse::<usize>().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<Vec<RAPLPowerID>> {
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<u64> {
let path = power_id.to_path();
let content = fs::read_to_string(&path)?;
content
.trim()
.parse::<u64>()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
pub fn get_cpu_power() -> Result<Vec<(RAPLPowerID, f32)>, 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())
}
+70
View File
@@ -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<u64, std::io::Error> {
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<f32, std::io::Error> {
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<u64, std::io::Error> {
read_msr(cpu, MSR_PKG_ENERGY_STATUS)
}
/// Calculate power consumption over a time interval
/// Returns power in Watts
pub fn calculate_power() -> Result<f32, Error> {
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)
}