mirror of
https://github.com/Astatin3/linux-power-meter-rs.git
synced 2026-06-08 16:18:02 -06:00
Add code
This commit is contained in:
@@ -12,3 +12,8 @@ Cargo.lock
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
|
||||
# Added by cargo
|
||||
|
||||
/target
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
+166
@@ -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())
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user