mirror of
https://github.com/Astatin3/linux-power-meter-rs.git
synced 2026-06-09 00:28:03 -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
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
*.pdb
|
*.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