use anyhow::{anyhow, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{self, File};
use std::io::{Read, Seek, SeekFrom};
use std::path::Path;
use std::sync::Mutex;
const MSR_RAPL_POWER_UNIT: u32 = 0x606;
const MSR_PKG_ENERGY_STATUS: u32 = 0x611; const MSR_DRAM_ENERGY_STATUS: u32 = 0x619; const MSR_PP0_ENERGY_STATUS: u32 = 0x639; const MSR_PP1_ENERGY_STATUS: u32 = 0x641; const MSR_PSYS_ENERGY_STATUS: u32 = 0x64D;
const MSR_PKG_POWER_LIMIT: u32 = 0x610; const MSR_DRAM_POWER_LIMIT: u32 = 0x618; const MSR_PP0_POWER_LIMIT: u32 = 0x638; const MSR_PP1_POWER_LIMIT: u32 = 0x640; const MSR_PSYS_POWER_LIMIT: u32 = 0x64C;
const MSR_PKG_POWER_INFO: u32 = 0x614; const MSR_DRAM_POWER_INFO: u32 = 0x61C; const MSR_PP0_POWER_INFO: u32 = 0x63C; const MSR_PP1_POWER_INFO: u32 = 0x644; const MSR_PSYS_POWER_INFO: u32 = 0x650;
const MSR_PKG_PERF_STATUS: u32 = 0x613; const MSR_DRAM_PERF_STATUS: u32 = 0x61B; const MSR_PP0_PERF_STATUS: u32 = 0x63B; const MSR_PP1_PERF_STATUS: u32 = 0x643;
#[allow(dead_code)]
const MSR_PLATFORM_POWER_LIMIT: u32 = 0x65C;
const MSR_AMD_RAPL_POWER_UNIT: u32 = 0xC0010299;
const MSR_AMD_CORE_ENERGY_STAT: u32 = 0xC001029A; const MSR_AMD_PKG_ENERGY_STAT: u32 = 0xC001029B; const MSR_AMD_L3_ENERGY_STAT: u32 = 0xC001029C;
const MSR_AMD_CORE_POWER_LIMIT: u32 = 0xC0010290; const MSR_AMD_PKG_POWER_LIMIT: u32 = 0xC0010291;
const MSR_AMD_CORE_POWER_INFO: u32 = 0xC0010292; const MSR_AMD_PKG_POWER_INFO: u32 = 0xC0010293;
#[allow(dead_code)]
const MSR_AMD_RAPL_PWR_UNIT: u32 = 0xC0010299; #[allow(dead_code)]
const MSR_AMD_PWR_REPORTING: u32 = 0xC001007A;
#[derive(Debug, Clone, PartialEq)]
enum CpuVendor {
Intel,
Amd,
Unknown,
}
#[derive(Debug, Clone)]
struct RaplUnits {
energy_unit: f64, power_unit: f64, #[allow(dead_code)]
time_unit: f64, }
impl Default for RaplUnits {
fn default() -> Self {
Self {
energy_unit: 1.0 / 65536.0, power_unit: 1.0 / 8.0, time_unit: 1.0 / 1024.0, }
}
}
#[derive(Debug, Clone, Default)]
struct RaplReading {
timestamp: u64,
package_energy_uj: u64, core_energy_uj: u64, dram_energy_uj: u64, gpu_energy_uj: u64, psys_energy_uj: u64, l3_energy_uj: u64,
package_power_limit: f64, dram_power_limit: f64, core_power_limit: f64, gpu_power_limit: f64, psys_power_limit: f64,
package_tdp: f64, dram_tdp: f64, core_tdp: f64, gpu_tdp: f64, psys_tdp: f64,
package_throttled_pct: f64, dram_throttled_pct: f64, core_throttled_pct: f64, gpu_throttled_pct: f64, }
struct MsrReader {
msr_files: Mutex<HashMap<u32, File>>, cpu_vendor: CpuVendor,
rapl_units: HashMap<u32, RaplUnits>, }
impl MsrReader {
pub fn new() -> Result<Self> {
let cpu_vendor = Self::detect_cpu_vendor()?;
let mut reader = Self {
msr_files: Mutex::new(HashMap::new()),
cpu_vendor,
rapl_units: HashMap::new(),
};
reader.init_rapl_units()?;
Ok(reader)
}
fn detect_cpu_vendor() -> Result<CpuVendor> {
if let Ok(cpuinfo) = fs::read_to_string("/proc/cpuinfo") {
for line in cpuinfo.lines() {
if line.starts_with("vendor_id") {
if line.contains("GenuineIntel") {
return Ok(CpuVendor::Intel);
} else if line.contains("AuthenticAMD") {
return Ok(CpuVendor::Amd);
}
break;
}
}
}
Ok(CpuVendor::Unknown)
}
fn init_rapl_units(&mut self) -> Result<()> {
let package_count = self.get_package_count()?;
for package_id in 0..package_count {
if let Some(cpu_id) = self.find_first_cpu_in_package(package_id)? {
if let Ok(units) = self.read_rapl_units(cpu_id) {
self.rapl_units.insert(package_id, units);
}
}
}
Ok(())
}
fn get_package_count(&self) -> Result<u32> {
let mut max_package = 0;
for cpu_id in 0..256 {
let topology_path =
format!("/sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id");
if let Ok(package_str) = fs::read_to_string(&topology_path) {
if let Ok(package_id) = package_str.trim().parse::<u32>() {
max_package = max_package.max(package_id);
}
} else {
break; }
}
Ok(max_package + 1)
}
fn find_first_cpu_in_package(&self, package_id: u32) -> Result<Option<u32>> {
for cpu_id in 0..256 {
let topology_path =
format!("/sys/devices/system/cpu/cpu{cpu_id}/topology/physical_package_id");
if let Ok(package_str) = fs::read_to_string(&topology_path) {
if let Ok(cpu_package_id) = package_str.trim().parse::<u32>() {
if cpu_package_id == package_id {
return Ok(Some(cpu_id));
}
}
} else {
break;
}
}
Ok(None)
}
fn read_rapl_units(&self, cpu_id: u32) -> Result<RaplUnits> {
let power_unit_msr = match self.cpu_vendor {
CpuVendor::Intel => MSR_RAPL_POWER_UNIT,
CpuVendor::Amd => MSR_AMD_RAPL_POWER_UNIT,
CpuVendor::Unknown => return Ok(RaplUnits::default()),
};
if let Ok(raw_value) = self.read_msr(cpu_id, power_unit_msr) {
let energy_unit = 1.0 / (1u64 << ((raw_value >> 8) & 0x1F)) as f64;
let power_unit = 1.0 / (1u64 << (raw_value & 0xF)) as f64;
let time_unit = 1.0 / (1u64 << ((raw_value >> 16) & 0xF)) as f64;
Ok(RaplUnits {
energy_unit,
power_unit,
time_unit,
})
} else {
Ok(RaplUnits::default())
}
}
fn read_msr(&self, cpu_id: u32, msr: u32) -> Result<u64> {
let mut files = self.msr_files.lock().unwrap();
if let std::collections::hash_map::Entry::Vacant(e) = files.entry(cpu_id) {
let file = match File::open(format!("/dev/cpu/{cpu_id}/msr")) {
Ok(file) => file,
Err(_) => {
let _ = std::process::Command::new("modprobe").arg("msr").output();
match File::open(format!("/dev/cpu/{cpu_id}/msr")) {
Ok(file) => file,
Err(e) => {
return Err(anyhow!("Cannot access MSR device /dev/cpu/{cpu_id}/msr: {e}. MSR access requires elevated privileges or proper kernel module configuration."));
}
}
}
};
e.insert(file);
}
let file = files.get_mut(&cpu_id).unwrap();
file.seek(SeekFrom::Start(msr as u64))?;
let mut buffer = [0u8; 8];
file.read_exact(&mut buffer)?;
Ok(u64::from_le_bytes(buffer))
}
pub fn read_rapl_energy(&self, package_id: u32) -> Result<RaplReading> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
let cpu_id = self
.find_first_cpu_in_package(package_id)?
.ok_or_else(|| anyhow!("No CPU found in package {}", package_id))?;
let mut reading = RaplReading {
timestamp,
..Default::default()
};
let default_units = RaplUnits::default();
let units = self.rapl_units.get(&package_id).unwrap_or(&default_units);
match self.cpu_vendor {
CpuVendor::Intel => {
if let Ok(pkg_energy) = self.read_msr(cpu_id, MSR_PKG_ENERGY_STATUS) {
reading.package_energy_uj =
((pkg_energy & 0xFFFFFFFF) as f64 * units.energy_unit * 1_000_000.0) as u64;
}
if let Ok(core_energy) = self.read_msr(cpu_id, MSR_PP0_ENERGY_STATUS) {
reading.core_energy_uj = ((core_energy & 0xFFFFFFFF) as f64
* units.energy_unit
* 1_000_000.0) as u64;
}
if let Ok(dram_energy) = self.read_msr(cpu_id, MSR_DRAM_ENERGY_STATUS) {
reading.dram_energy_uj = ((dram_energy & 0xFFFFFFFF) as f64
* units.energy_unit
* 1_000_000.0) as u64;
}
if let Ok(gpu_energy) = self.read_msr(cpu_id, MSR_PP1_ENERGY_STATUS) {
reading.gpu_energy_uj =
((gpu_energy & 0xFFFFFFFF) as f64 * units.energy_unit * 1_000_000.0) as u64;
}
if let Ok(psys_energy) = self.read_msr(cpu_id, MSR_PSYS_ENERGY_STATUS) {
reading.psys_energy_uj = ((psys_energy & 0xFFFFFFFF) as f64
* units.energy_unit
* 1_000_000.0) as u64;
}
if let Ok(pkg_power_limit) = self.read_msr(cpu_id, MSR_PKG_POWER_LIMIT) {
let pl1 = (pkg_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.package_power_limit = pl1;
}
if let Ok(dram_power_limit) = self.read_msr(cpu_id, MSR_DRAM_POWER_LIMIT) {
let dram_limit = (dram_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.dram_power_limit = dram_limit;
}
if let Ok(pp0_power_limit) = self.read_msr(cpu_id, MSR_PP0_POWER_LIMIT) {
let pp0_limit = (pp0_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.core_power_limit = pp0_limit;
}
if let Ok(pp1_power_limit) = self.read_msr(cpu_id, MSR_PP1_POWER_LIMIT) {
let pp1_limit = (pp1_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.gpu_power_limit = pp1_limit;
}
if let Ok(psys_power_limit) = self.read_msr(cpu_id, MSR_PSYS_POWER_LIMIT) {
let psys_limit = (psys_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.psys_power_limit = psys_limit;
}
if let Ok(pkg_power_info) = self.read_msr(cpu_id, MSR_PKG_POWER_INFO) {
let tdp = (pkg_power_info & 0x7FFF) as f64 * units.power_unit;
reading.package_tdp = tdp;
}
if let Ok(dram_power_info) = self.read_msr(cpu_id, MSR_DRAM_POWER_INFO) {
let dram_tdp = (dram_power_info & 0x7FFF) as f64 * units.power_unit;
reading.dram_tdp = dram_tdp;
}
if let Ok(pp0_power_info) = self.read_msr(cpu_id, MSR_PP0_POWER_INFO) {
let pp0_tdp = (pp0_power_info & 0x7FFF) as f64 * units.power_unit;
reading.core_tdp = pp0_tdp;
}
if let Ok(pp1_power_info) = self.read_msr(cpu_id, MSR_PP1_POWER_INFO) {
let pp1_tdp = (pp1_power_info & 0x7FFF) as f64 * units.power_unit;
reading.gpu_tdp = pp1_tdp;
}
if let Ok(psys_power_info) = self.read_msr(cpu_id, MSR_PSYS_POWER_INFO) {
let psys_tdp = (psys_power_info & 0x7FFF) as f64 * units.power_unit;
reading.psys_tdp = psys_tdp;
}
if let Ok(pkg_perf_status) = self.read_msr(cpu_id, MSR_PKG_PERF_STATUS) {
let throttle_cycles = (pkg_perf_status >> 32) & 0xFFFFFFFF;
let total_cycles = pkg_perf_status & 0xFFFFFFFF;
if total_cycles > 0 {
reading.package_throttled_pct =
(throttle_cycles as f64 / total_cycles as f64) * 100.0;
}
}
if let Ok(dram_perf_status) = self.read_msr(cpu_id, MSR_DRAM_PERF_STATUS) {
let throttle_cycles = (dram_perf_status >> 32) & 0xFFFFFFFF;
let total_cycles = dram_perf_status & 0xFFFFFFFF;
if total_cycles > 0 {
reading.dram_throttled_pct =
(throttle_cycles as f64 / total_cycles as f64) * 100.0;
}
}
if let Ok(pp0_perf_status) = self.read_msr(cpu_id, MSR_PP0_PERF_STATUS) {
let throttle_cycles = (pp0_perf_status >> 32) & 0xFFFFFFFF;
let total_cycles = pp0_perf_status & 0xFFFFFFFF;
if total_cycles > 0 {
reading.core_throttled_pct =
(throttle_cycles as f64 / total_cycles as f64) * 100.0;
}
}
if let Ok(pp1_perf_status) = self.read_msr(cpu_id, MSR_PP1_PERF_STATUS) {
let throttle_cycles = (pp1_perf_status >> 32) & 0xFFFFFFFF;
let total_cycles = pp1_perf_status & 0xFFFFFFFF;
if total_cycles > 0 {
reading.gpu_throttled_pct =
(throttle_cycles as f64 / total_cycles as f64) * 100.0;
}
}
}
CpuVendor::Amd => {
if let Ok(pkg_energy) = self.read_msr(cpu_id, MSR_AMD_PKG_ENERGY_STAT) {
reading.package_energy_uj =
((pkg_energy & 0xFFFFFFFF) as f64 * units.energy_unit * 1_000_000.0) as u64;
}
if let Ok(core_energy) = self.read_msr(cpu_id, MSR_AMD_CORE_ENERGY_STAT) {
reading.core_energy_uj = ((core_energy & 0xFFFFFFFF) as f64
* units.energy_unit
* 1_000_000.0) as u64;
}
if let Ok(l3_energy) = self.read_msr(cpu_id, MSR_AMD_L3_ENERGY_STAT) {
reading.l3_energy_uj =
((l3_energy & 0xFFFFFFFF) as f64 * units.energy_unit * 1_000_000.0) as u64;
}
if let Ok(core_power_limit) = self.read_msr(cpu_id, MSR_AMD_CORE_POWER_LIMIT) {
let core_limit = (core_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.core_power_limit = core_limit;
}
if let Ok(pkg_power_limit) = self.read_msr(cpu_id, MSR_AMD_PKG_POWER_LIMIT) {
let pkg_limit = (pkg_power_limit & 0x7FFF) as f64 * units.power_unit;
reading.package_power_limit = pkg_limit;
}
if let Ok(core_power_info) = self.read_msr(cpu_id, MSR_AMD_CORE_POWER_INFO) {
let core_tdp = (core_power_info & 0x7FFF) as f64 * units.power_unit;
reading.core_tdp = core_tdp;
}
if let Ok(pkg_power_info) = self.read_msr(cpu_id, MSR_AMD_PKG_POWER_INFO) {
let pkg_tdp = (pkg_power_info & 0x7FFF) as f64 * units.power_unit;
reading.package_tdp = pkg_tdp;
}
}
CpuVendor::Unknown => {
return Err(anyhow!("Unknown CPU vendor - cannot read RAPL MSRs"));
}
}
Ok(reading)
}
pub fn get_cpu_vendor(&self) -> &CpuVendor {
&self.cpu_vendor
}
}
struct RaplPowerMonitor {
msr_reader: MsrReader,
previous_readings: HashMap<u32, RaplReading>, package_count: u32,
}
impl RaplPowerMonitor {
pub fn new() -> Result<Self> {
let msr_reader = MsrReader::new()?;
let package_count = msr_reader.get_package_count()?;
Ok(Self {
msr_reader,
previous_readings: HashMap::new(),
package_count,
})
}
pub fn collect_power_data(&mut self) -> Result<HashMap<u32, f64>> {
let mut package_power = HashMap::new();
for package_id in 0..self.package_count {
if let Ok(current_reading) = self.msr_reader.read_rapl_energy(package_id) {
if let Some(previous_reading) = self.previous_readings.get(&package_id) {
let time_delta_seconds =
(current_reading.timestamp - previous_reading.timestamp) as f64
/ 1_000_000.0;
if time_delta_seconds > 0.0 {
let energy_delta_joules = (current_reading.package_energy_uj
- previous_reading.package_energy_uj)
as f64
/ 1_000_000.0;
let power_watts = energy_delta_joules / time_delta_seconds;
if (0.0..=1000.0).contains(&power_watts) {
package_power.insert(package_id, power_watts);
}
}
}
self.previous_readings.insert(package_id, current_reading);
}
}
Ok(package_power)
}
pub fn get_cpu_vendor(&self) -> &CpuVendor {
self.msr_reader.get_cpu_vendor()
}
pub fn is_available(&self) -> bool {
for package_id in 0..self.package_count {
if self.msr_reader.read_rapl_energy(package_id).is_ok() {
return true;
}
}
false
}
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CStateInfo {
pub name: String,
pub latency: u64, pub residency: u64, pub usage: u64, }
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct CorePowerData {
pub core_id: u32,
pub frequency_mhz: f64,
pub temperature_celsius: f64,
pub power_watts: f64,
pub c_states: HashMap<String, CStateInfo>,
pub package_id: u32,
pub tdp: f64, pub power_limit: f64, pub throttle_percent: f64, pub dram_energy_uj: u64, pub gpu_energy_uj: u64, pub psys_energy_uj: u64, pub l3_energy_uj: u64, }
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct SystemPowerData {
pub timestamp: u64,
pub cores: HashMap<u32, CorePowerData>,
pub total_power_watts: f64,
pub battery_level_percent: Option<f64>,
pub battery_charging: Option<bool>,
pub battery_remaining_time_minutes: Option<u32>,
pub package_power: HashMap<u32, f64>, }
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct PowerHistoryPoint {
pub timestamp: u64,
pub total_power_watts: f64,
pub avg_power_per_core: f64,
}
#[derive(Clone, Debug, Default)]
pub struct PowerSnapshot {
pub current: SystemPowerData,
pub previous: Option<SystemPowerData>,
pub history: Vec<PowerHistoryPoint>,
max_history_points: usize,
pub cstate_deltas: HashMap<u32, HashMap<String, CStateInfo>>,
power_min: f64,
power_max: f64,
temperature_min: f64,
temperature_max: f64,
frequency_min: f64,
frequency_max: f64,
has_data: bool,
}
impl PowerSnapshot {
pub fn new() -> Self {
Self {
max_history_points: 120,
cstate_deltas: HashMap::new(),
..Default::default()
}
}
pub fn update(&mut self, new_data: SystemPowerData) {
self.calculate_cstate_deltas(&new_data);
self.previous = Some(self.current.clone());
self.update_min_max_values(&new_data);
let avg_power_per_core = if !new_data.cores.is_empty() {
new_data.total_power_watts / new_data.cores.len() as f64
} else {
0.0
};
let history_point = PowerHistoryPoint {
timestamp: new_data.timestamp,
total_power_watts: new_data.total_power_watts,
avg_power_per_core,
};
self.history.push(history_point);
if self.history.len() > self.max_history_points {
self.history.remove(0);
}
self.current = new_data;
}
pub fn set_max_history_points(&mut self, max_points: usize) {
self.max_history_points = max_points;
while self.history.len() > self.max_history_points {
self.history.remove(0);
}
}
pub fn get_time_bounded_data(
&self,
tick_interval_ms: u64,
chart_width: u16,
) -> &[PowerHistoryPoint] {
if self.history.is_empty() {
return &self.history;
}
let optimal_points = ((chart_width as f64) / 1.2).floor() as usize;
let max_points = optimal_points.min(self.max_history_points).max(30);
let time_window_seconds = (max_points as u64 * tick_interval_ms) / 1000;
let current_time = self.history.last().unwrap().timestamp;
let start_time = current_time.saturating_sub(time_window_seconds);
let start_idx = self
.history
.iter()
.position(|point| point.timestamp >= start_time)
.unwrap_or(0);
&self.history[start_idx..]
}
pub fn get_adaptive_chart_data(
&self,
tick_interval_ms: u64,
chart_width: u16,
) -> Vec<&PowerHistoryPoint> {
let bounded_data = self.get_time_bounded_data(tick_interval_ms, chart_width);
if bounded_data.is_empty() {
return Vec::new();
}
let target_points = ((chart_width as f64) / 1.2).floor() as usize;
if bounded_data.len() <= target_points {
return bounded_data.iter().collect();
}
let mut sampled_points = Vec::new();
sampled_points.push(&bounded_data[0]);
if target_points > 2 {
let step = bounded_data.len() as f64 / target_points as f64;
for i in 1..(target_points - 1) {
let idx = (i as f64 * step).floor() as usize;
if idx < bounded_data.len() && idx > 0 {
sampled_points.push(&bounded_data[idx]);
}
}
}
if let Some(last_point) = bounded_data.last() {
if sampled_points.last() != Some(&last_point) {
sampled_points.push(last_point);
}
}
sampled_points
}
pub fn get_high_density_chart_data(&self, chart_width: u16) -> Vec<&PowerHistoryPoint> {
if self.history.is_empty() {
return Vec::new();
}
let max_points = ((chart_width as f64) / 0.8).floor() as usize;
let points_to_show = max_points.min(self.history.len());
let start_idx = self.history.len().saturating_sub(points_to_show);
self.history[start_idx..].iter().collect()
}
fn calculate_cstate_deltas(&mut self, new_data: &SystemPowerData) {
if let Some(prev_data) = &self.previous {
self.cstate_deltas.clear();
for (core_id, new_core) in &new_data.cores {
if let Some(prev_core) = prev_data.cores.get(core_id) {
let mut core_deltas = HashMap::new();
for (cstate_name, new_cstate) in &new_core.c_states {
if let Some(prev_cstate) = prev_core.c_states.get(cstate_name) {
let delta_residency =
new_cstate.residency.saturating_sub(prev_cstate.residency);
let delta_usage = new_cstate.usage.saturating_sub(prev_cstate.usage);
core_deltas.insert(
cstate_name.clone(),
CStateInfo {
name: cstate_name.clone(),
latency: new_cstate.latency,
residency: delta_residency,
usage: delta_usage,
},
);
}
}
self.cstate_deltas.insert(*core_id, core_deltas);
}
}
}
}
pub fn get_cstate_percentage(&self, core_id: u32, cstate_name: &str) -> f64 {
if let Some(core_deltas) = self.cstate_deltas.get(&core_id) {
if let Some(cstate_delta) = core_deltas.get(cstate_name) {
let total_residency_delta: u64 = core_deltas.values().map(|cs| cs.residency).sum();
if total_residency_delta > 0 {
return (cstate_delta.residency as f64 / total_residency_delta as f64) * 100.0;
}
}
}
if let Some(core_data) = self.current.cores.get(&core_id) {
if let Some(cstate_info) = core_data.c_states.get(cstate_name) {
let total_residency: u64 = core_data.c_states.values().map(|cs| cs.residency).sum();
if total_residency > 0 {
return (cstate_info.residency as f64 / total_residency as f64) * 100.0;
}
}
}
0.0
}
pub fn get_power_delta(&self) -> HashMap<u32, f64> {
let mut deltas = HashMap::new();
if let Some(prev) = &self.previous {
for (core_id, current_core) in &self.current.cores {
if let Some(prev_core) = prev.cores.get(core_id) {
let delta = current_core.power_watts - prev_core.power_watts;
deltas.insert(*core_id, delta);
}
}
}
deltas
}
pub fn get_chart_data(&self) -> &[PowerHistoryPoint] {
&self.history
}
pub fn get_time_range(&self) -> (f64, f64) {
if self.history.is_empty() {
return (0.0, 1.0);
}
let min_time = self.history.first().unwrap().timestamp as f64;
let max_time = self.history.last().unwrap().timestamp as f64;
let range = max_time - min_time;
let buffer = range * 0.1;
(min_time - buffer, max_time + buffer)
}
pub fn get_total_power_range(&self) -> (f64, f64) {
if self.history.is_empty() {
return (0.0, 100.0);
}
let mut min_power = f64::MAX;
let mut max_power = f64::MIN;
for point in &self.history {
if point.total_power_watts < min_power {
min_power = point.total_power_watts;
}
if point.total_power_watts > max_power {
max_power = point.total_power_watts;
}
}
let range = max_power - min_power;
let buffer = range * 0.1;
((min_power - buffer).max(0.0), max_power + buffer)
}
pub fn get_avg_power_range(&self) -> (f64, f64) {
if self.history.is_empty() {
return (0.0, 10.0);
}
let mut min_power = f64::MAX;
let mut max_power = f64::MIN;
for point in &self.history {
if point.avg_power_per_core < min_power {
min_power = point.avg_power_per_core;
}
if point.avg_power_per_core > max_power {
max_power = point.avg_power_per_core;
}
}
let range = max_power - min_power;
let buffer = range * 0.1;
((min_power - buffer).max(0.0), max_power + buffer)
}
fn update_min_max_values(&mut self, new_data: &SystemPowerData) {
if !self.has_data {
self.power_min = new_data.total_power_watts;
self.power_max = new_data.total_power_watts;
if let Some(first_core) = new_data.cores.values().next() {
self.temperature_min = first_core.temperature_celsius;
self.temperature_max = first_core.temperature_celsius;
self.frequency_min = first_core.frequency_mhz;
self.frequency_max = first_core.frequency_mhz;
}
self.has_data = true;
}
self.power_min = self.power_min.min(new_data.total_power_watts);
self.power_max = self.power_max.max(new_data.total_power_watts);
for core_data in new_data.cores.values() {
if core_data.temperature_celsius > 0.0 {
self.temperature_min = self.temperature_min.min(core_data.temperature_celsius);
self.temperature_max = self.temperature_max.max(core_data.temperature_celsius);
}
if core_data.frequency_mhz > 0.0 {
self.frequency_min = self.frequency_min.min(core_data.frequency_mhz);
self.frequency_max = self.frequency_max.max(core_data.frequency_mhz);
}
}
}
pub fn get_power_thresholds(&self) -> (f64, f64) {
if !self.has_data || self.power_max <= self.power_min {
return (5.0, 15.0); }
let range = self.power_max - self.power_min;
let low_threshold = self.power_min + (range * 0.33); let high_threshold = self.power_min + (range * 0.67);
(low_threshold, high_threshold)
}
pub fn get_temperature_thresholds(&self) -> (f64, f64) {
if !self.has_data || self.temperature_max <= self.temperature_min {
return (60.0, 80.0); }
let range = self.temperature_max - self.temperature_min;
let low_threshold = self.temperature_min + (range * 0.4);
let high_threshold = self.temperature_min + (range * 0.75);
let low_threshold = low_threshold.clamp(40.0, 70.0);
let high_threshold = high_threshold.clamp(60.0, 90.0);
(low_threshold, high_threshold)
}
pub fn get_frequency_thresholds(&self) -> (f64, f64) {
if !self.has_data || self.frequency_max <= self.frequency_min {
return (1000.0, 3000.0); }
let range = self.frequency_max - self.frequency_min;
let low_threshold = self.frequency_min + (range * 0.33); let high_threshold = self.frequency_min + (range * 0.67);
(low_threshold, high_threshold)
}
pub fn get_observed_ranges(
&self,
) -> (
(f64, f64), // power range
(f64, f64), // temperature range
(f64, f64), // frequency range
) {
if !self.has_data {
return ((0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
}
(
(self.power_min, self.power_max),
(self.temperature_min, self.temperature_max),
(self.frequency_min, self.frequency_max),
)
}
}
pub struct PowerDataCollector {
cpu_count: u32,
sysfs_power_path: String,
sysfs_cpufreq_path: String,
sysfs_thermal_path: String,
sysfs_cpuidle_path: String,
rapl_monitor: Option<RaplPowerMonitor>,
}
impl PowerDataCollector {
pub fn new() -> Result<Self> {
let cpu_count = Self::detect_cpu_count()?;
let rapl_monitor = match RaplPowerMonitor::new() {
Ok(monitor) => {
if monitor.is_available() {
log::info!(
"MSR-based RAPL power monitoring enabled for {:?} CPU",
monitor.get_cpu_vendor()
);
Some(monitor)
} else {
log::warn!("MSR-based RAPL not available, falling back to sysfs");
None
}
}
Err(e) => {
log::warn!("Failed to initialize RAPL monitor: {e}, falling back to sysfs");
None
}
};
Ok(Self {
cpu_count,
sysfs_power_path: "/sys/class/power_supply".to_string(),
sysfs_cpufreq_path: "/sys/devices/system/cpu".to_string(),
sysfs_thermal_path: "/sys/class/thermal".to_string(),
sysfs_cpuidle_path: "/sys/devices/system/cpu".to_string(),
rapl_monitor,
})
}
fn detect_cpu_count() -> Result<u32> {
let cpus_online = fs::read_to_string("/sys/devices/system/cpu/online")
.map_err(|e| anyhow!("Failed to read CPU online info: {}", e))?;
let trimmed = cpus_online.trim();
if let Some(dash_pos) = trimmed.rfind('-') {
let max_cpu_str = &trimmed[dash_pos + 1..];
let max_cpu: u32 = max_cpu_str
.parse()
.map_err(|e| anyhow!("Failed to parse max CPU number: {}", e))?;
Ok(max_cpu + 1)
} else {
Err(anyhow!("Unexpected CPU online format: {}", trimmed))
}
}
pub fn collect(&mut self) -> Result<SystemPowerData> {
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let mut system_data = SystemPowerData {
timestamp,
..Default::default()
};
for core_id in 0..self.cpu_count {
if let Ok(core_data) = self.collect_core_data(core_id) {
system_data.cores.insert(core_id, core_data);
}
}
if let Ok((battery_level, charging, remaining_time)) = self.collect_battery_info() {
system_data.battery_level_percent = Some(battery_level);
system_data.battery_charging = Some(charging);
system_data.battery_remaining_time_minutes = remaining_time;
}
system_data.package_power = self.collect_package_power()?;
system_data.total_power_watts = if !system_data.package_power.is_empty() {
system_data.package_power.values().sum()
} else {
system_data
.cores
.values()
.map(|core| core.power_watts)
.sum()
};
Ok(system_data)
}
fn collect_core_data(&self, core_id: u32) -> Result<CorePowerData> {
let mut core_data = CorePowerData {
core_id,
..Default::default()
};
core_data.frequency_mhz = self.read_cpu_frequency(core_id)?;
core_data.temperature_celsius = self.read_cpu_temperature(core_id)?;
core_data.power_watts = 0.0;
core_data.c_states = self.read_c_states(core_id)?;
core_data.package_id = self.get_package_id(core_id)?;
if let Some(ref rapl_monitor) = self.rapl_monitor {
if let Ok(rapl_reading) = rapl_monitor
.msr_reader
.read_rapl_energy(core_data.package_id)
{
core_data.tdp = rapl_reading.core_tdp.max(rapl_reading.package_tdp);
core_data.power_limit = rapl_reading
.core_power_limit
.max(rapl_reading.package_power_limit);
core_data.throttle_percent = rapl_reading
.core_throttled_pct
.max(rapl_reading.package_throttled_pct);
core_data.dram_energy_uj = rapl_reading.dram_energy_uj;
core_data.gpu_energy_uj = rapl_reading.gpu_energy_uj;
core_data.psys_energy_uj = rapl_reading.psys_energy_uj;
core_data.l3_energy_uj = rapl_reading.l3_energy_uj;
}
}
Ok(core_data)
}
fn read_cpu_frequency(&self, core_id: u32) -> Result<f64> {
let freq_path = format!(
"{}/cpu{core_id}/cpufreq/scaling_cur_freq",
self.sysfs_cpufreq_path,
);
if !Path::new(&freq_path).exists() {
let alt_path = format!(
"{}/cpu{core_id}/cpufreq/cpuinfo_cur_freq",
self.sysfs_cpufreq_path,
);
if Path::new(&alt_path).exists() {
let freq_khz_str = fs::read_to_string(&alt_path)
.map_err(|e| anyhow!("Failed to read CPU frequency: {e}"))?;
let freq_khz: f64 = freq_khz_str
.trim()
.parse()
.map_err(|e| anyhow!("Failed to parse CPU frequency: {e}"))?;
return Ok(freq_khz / 1000.0); }
return Ok(0.0); }
let freq_khz_str = fs::read_to_string(&freq_path)
.map_err(|e| anyhow!("Failed to read CPU frequency: {e}"))?;
let freq_khz: f64 = freq_khz_str
.trim()
.parse()
.map_err(|e| anyhow!("Failed to parse CPU frequency: {e}"))?;
Ok(freq_khz / 1000.0) }
fn read_cpu_temperature(&self, core_id: u32) -> Result<f64> {
for i in 0..20 {
let thermal_path = format!("{}/thermal_zone{i}/type", self.sysfs_thermal_path);
if let Ok(thermal_type) = fs::read_to_string(&thermal_path) {
let thermal_type = thermal_type.trim().to_lowercase();
if thermal_type.contains("cpu")
|| thermal_type.contains(&format!("core {core_id}"))
|| thermal_type.contains(&format!("core{core_id}"))
|| thermal_type.contains("x86_pkg_temp")
|| thermal_type.contains("coretemp")
{
let temp_path = format!("{}/thermal_zone{i}/temp", self.sysfs_thermal_path);
if let Ok(temp_str) = fs::read_to_string(&temp_path) {
if let Ok(temp_millicelsius) = temp_str.trim().parse::<f64>() {
let temp_celsius = temp_millicelsius / 1000.0;
if (0.0..=150.0).contains(&temp_celsius) {
return Ok(temp_celsius);
}
}
}
}
}
}
let hwmon_paths = [
format!(
"/sys/devices/platform/coretemp.0/hwmon/hwmon0/temp{}_input",
core_id + 2
),
format!(
"/sys/devices/platform/coretemp.0/hwmon/hwmon1/temp{}_input",
core_id + 2
),
format!(
"/sys/devices/platform/coretemp.0/hwmon/hwmon2/temp{}_input",
core_id + 2
),
format!(
"/sys/devices/platform/coretemp.0/hwmon/hwmon3/temp{}_input",
core_id + 2
),
format!("/sys/class/hwmon/hwmon0/temp{}_input", core_id + 2),
format!("/sys/class/hwmon/hwmon1/temp{}_input", core_id + 2),
format!("/sys/class/hwmon/hwmon2/temp{}_input", core_id + 2),
format!("/sys/class/hwmon/hwmon3/temp{}_input", core_id + 2),
];
for path in &hwmon_paths {
if let Ok(temp_str) = fs::read_to_string(path) {
if let Ok(temp_millicelsius) = temp_str.trim().parse::<f64>() {
let temp_celsius = temp_millicelsius / 1000.0;
if (0.0..=150.0).contains(&temp_celsius) {
return Ok(temp_celsius);
}
}
}
}
let alt_core_paths = [
format!("/sys/devices/platform/coretemp.0/temp{}_input", core_id + 1),
format!("/sys/devices/platform/coretemp.0/temp{}_input", core_id + 2),
format!("/sys/devices/platform/coretemp.0/temp{core_id}_input"),
];
for path in &alt_core_paths {
if let Ok(temp_str) = fs::read_to_string(path) {
if let Ok(temp_millicelsius) = temp_str.trim().parse::<f64>() {
let temp_celsius = temp_millicelsius / 1000.0;
if (0.0..=150.0).contains(&temp_celsius) {
return Ok(temp_celsius);
}
}
}
}
let package_id = self.get_package_id(core_id).unwrap_or(0);
let package_temp_paths = [
format!("/sys/devices/platform/coretemp.{package_id}/temp1_input"),
format!(
"/sys/devices/platform/coretemp.{package_id}/hwmon/hwmon{package_id}/temp1_input"
),
"/sys/devices/platform/coretemp.0/temp1_input".to_string(),
"/sys/class/hwmon/hwmon0/temp1_input".to_string(),
"/sys/class/hwmon/hwmon1/temp1_input".to_string(),
];
for path in &package_temp_paths {
if let Ok(temp_str) = fs::read_to_string(path) {
if let Ok(temp_millicelsius) = temp_str.trim().parse::<f64>() {
let temp_celsius = temp_millicelsius / 1000.0;
if (0.0..=150.0).contains(&temp_celsius) {
return Ok(temp_celsius);
}
}
}
}
if let Ok(hwmon_dir) = fs::read_dir("/sys/class/hwmon") {
for entry in hwmon_dir.flatten() {
let hwmon_path = entry.path();
let name_path = hwmon_path.join("name");
if let Ok(name) = fs::read_to_string(&name_path) {
let name = name.trim().to_lowercase();
if name.contains("coretemp") || name.contains("cpu") || name.contains("k10temp")
{
for temp_input in 1..=10 {
let temp_path = hwmon_path.join(format!("temp{temp_input}_input"));
if let Ok(temp_str) = fs::read_to_string(&temp_path) {
if let Ok(temp_millicelsius) = temp_str.trim().parse::<f64>() {
let temp_celsius = temp_millicelsius / 1000.0;
if (0.0..=150.0).contains(&temp_celsius) {
return Ok(temp_celsius);
}
}
}
}
}
}
}
}
Ok(0.0)
}
fn read_c_states(&self, core_id: u32) -> Result<HashMap<String, CStateInfo>> {
let mut c_states = HashMap::new();
let cpuidle_path = format!("{}/cpu{core_id}/cpuidle", self.sysfs_cpuidle_path);
if !Path::new(&cpuidle_path).exists() {
return Ok(c_states);
}
for i in 0..10 {
let state_path = format!("{cpuidle_path}/state{i}");
if !Path::new(&state_path).exists() {
break;
}
let name_path = format!("{state_path}/name");
let latency_path = format!("{state_path}/latency");
let usage_path = format!("{state_path}/usage");
let time_path = format!("{state_path}/time");
if let (Ok(name), Ok(latency_str), Ok(usage_str), Ok(time_str)) = (
fs::read_to_string(&name_path),
fs::read_to_string(&latency_path),
fs::read_to_string(&usage_path),
fs::read_to_string(&time_path),
) {
let name = name.trim().to_string();
let latency: u64 = latency_str.trim().parse().unwrap_or(0);
let usage: u64 = usage_str.trim().parse().unwrap_or(0);
let residency: u64 = time_str.trim().parse().unwrap_or(0);
c_states.insert(
name.clone(),
CStateInfo {
name,
latency,
residency,
usage,
},
);
}
}
Ok(c_states)
}
fn get_package_id(&self, core_id: u32) -> Result<u32> {
let topology_path =
format!("/sys/devices/system/cpu/cpu{core_id}/topology/physical_package_id");
if let Ok(package_str) = fs::read_to_string(&topology_path) {
return Ok(package_str.trim().parse().unwrap_or(0));
}
Ok(0) }
fn collect_battery_info(&self) -> Result<(f64, bool, Option<u32>)> {
let power_supply_dir = Path::new(&self.sysfs_power_path);
if !power_supply_dir.exists() {
return Err(anyhow!("Power supply directory not found"));
}
for entry in fs::read_dir(power_supply_dir)? {
let entry = entry?;
let supply_path = entry.path();
let type_path = supply_path.join("type");
if let Ok(supply_type) = fs::read_to_string(&type_path) {
if supply_type.trim() == "Battery" {
let capacity_path = supply_path.join("capacity");
let status_path = supply_path.join("status");
let time_to_empty_path = supply_path.join("time_to_empty_now");
let capacity = if let Ok(cap_str) = fs::read_to_string(&capacity_path) {
cap_str.trim().parse::<f64>().unwrap_or(0.0)
} else {
0.0
};
let charging = if let Ok(status_str) = fs::read_to_string(&status_path) {
status_str.trim() == "Charging"
} else {
false
};
let remaining_time =
if let Ok(time_str) = fs::read_to_string(&time_to_empty_path) {
time_str.trim().parse::<u32>().ok().map(|s| s / 60) } else {
None
};
return Ok((capacity, charging, remaining_time));
}
}
}
Err(anyhow!("No battery found"))
}
fn collect_package_power(&mut self) -> Result<HashMap<u32, f64>> {
if let Some(ref mut rapl_monitor) = self.rapl_monitor {
if let Ok(msr_power_data) = rapl_monitor.collect_power_data() {
if !msr_power_data.is_empty() {
return Ok(msr_power_data);
}
}
}
let mut package_power = HashMap::new();
let intel_rapl_path = "/sys/class/powercap/intel-rapl";
if Path::new(intel_rapl_path).exists() {
for entry in fs::read_dir(intel_rapl_path)? {
let entry = entry?;
let package_path = entry.path();
let package_name = package_path
.file_name()
.and_then(|name| name.to_str())
.unwrap_or("");
if package_name.starts_with("intel-rapl:") {
let energy_path = package_path.join("energy_uj");
let name_path = package_path.join("name");
if let (Ok(energy_str), Ok(name_str)) = (
fs::read_to_string(&energy_path),
fs::read_to_string(&name_path),
) {
if name_str.trim().starts_with("package-") {
if let Ok(energy_uj) = energy_str.trim().parse::<u64>() {
let package_id = package_name
.chars()
.last()
.and_then(|c| c.to_digit(10))
.unwrap_or(0);
let power_watts = energy_uj as f64 / 1_000_000.0; package_power.insert(package_id, power_watts);
}
}
}
}
}
}
Ok(package_power)
}
}
impl Default for PowerDataCollector {
fn default() -> Self {
Self::new().unwrap_or(Self {
cpu_count: 1,
sysfs_power_path: "/sys/class/power_supply".to_string(),
sysfs_cpufreq_path: "/sys/devices/system/cpu".to_string(),
sysfs_thermal_path: "/sys/class/thermal".to_string(),
sysfs_cpuidle_path: "/sys/devices/system/cpu".to_string(),
rapl_monitor: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_power_snapshot_update() {
let mut snapshot = PowerSnapshot::new();
let data = SystemPowerData {
timestamp: 1000,
total_power_watts: 50.0,
..Default::default()
};
snapshot.update(data.clone());
assert_eq!(snapshot.current.timestamp, 1000);
assert_eq!(snapshot.current.total_power_watts, 50.0);
let data2 = SystemPowerData {
timestamp: 2000,
total_power_watts: 60.0,
..Default::default()
};
snapshot.update(data2);
assert_eq!(snapshot.current.timestamp, 2000);
assert_eq!(snapshot.current.total_power_watts, 60.0);
assert!(snapshot.previous.is_some());
assert_eq!(snapshot.previous.as_ref().unwrap().total_power_watts, 50.0);
}
#[test]
fn test_c_state_info() {
let c_state = CStateInfo {
name: "C1".to_string(),
latency: 10,
residency: 1000,
usage: 50,
};
assert_eq!(c_state.name, "C1");
assert_eq!(c_state.latency, 10);
assert_eq!(c_state.residency, 1000);
assert_eq!(c_state.usage, 50);
}
}