1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-08-02 20:07:47 +00:00

Compare commits

...

2 commits

6 changed files with 184 additions and 179 deletions

View file

@ -53,9 +53,10 @@ impl CpuDelta {
let mut cpus = match &self.for_ { let mut cpus = match &self.for_ {
Some(numbers) => { Some(numbers) => {
let mut cpus = Vec::with_capacity(numbers.len()); let mut cpus = Vec::with_capacity(numbers.len());
let cache = cpu::CpuRescanCache::default();
for &number in numbers { for &number in numbers {
cpus.push(cpu::Cpu::new(number)?); cpus.push(cpu::Cpu::new(number, &cache)?);
} }
cpus cpus

View file

@ -1,11 +1,50 @@
use anyhow::{Context, bail}; use anyhow::{Context, bail};
use yansi::Paint as _; use yansi::Paint as _;
use std::{fmt, string::ToString}; use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString};
use crate::fs; use crate::fs;
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CpuRescanCache {
stat: OnceCell<HashMap<u32, CpuStat>>,
temperatures: OnceCell<HashMap<u32, f64>>,
}
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct CpuStat {
pub user: u64,
pub nice: u64,
pub system: u64,
pub idle: u64,
pub iowait: u64,
pub irq: u64,
pub softirq: u64,
pub steal: u64,
}
impl CpuStat {
pub fn total(&self) -> u64 {
self.user
+ self.nice
+ self.system
+ self.idle
+ self.iowait
+ self.irq
+ self.softirq
+ self.steal
}
pub fn idle(&self) -> u64 {
self.idle + self.iowait
}
pub fn usage(&self) -> f64 {
1.0 - self.idle() as f64 / self.total() as f64
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Cpu { pub struct Cpu {
pub number: u32, pub number: u32,
@ -24,35 +63,9 @@ pub struct Cpu {
pub available_epbs: Vec<String>, pub available_epbs: Vec<String>,
pub epb: Option<String>, pub epb: Option<String>,
pub time_user: u64, pub stat: CpuStat,
pub time_nice: u64,
pub time_system: u64,
pub time_idle: u64,
pub time_iowait: u64,
pub time_irq: u64,
pub time_softirq: u64,
pub time_steal: u64,
}
impl Cpu { pub temperature: Option<f64>,
pub fn time_total(&self) -> u64 {
self.time_user
+ self.time_nice
+ self.time_system
+ self.time_idle
+ self.time_iowait
+ self.time_irq
+ self.time_softirq
+ self.time_steal
}
pub fn time_idle(&self) -> u64 {
self.time_idle + self.time_iowait
}
pub fn usage(&self) -> f64 {
1.0 - self.time_idle() as f64 / self.time_total() as f64
}
} }
impl fmt::Display for Cpu { impl fmt::Display for Cpu {
@ -64,7 +77,7 @@ impl fmt::Display for Cpu {
} }
impl Cpu { impl Cpu {
pub fn new(number: u32) -> anyhow::Result<Self> { pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result<Self> {
let mut cpu = Self { let mut cpu = Self {
number, number,
has_cpufreq: false, has_cpufreq: false,
@ -82,16 +95,20 @@ impl Cpu {
available_epbs: Vec::new(), available_epbs: Vec::new(),
epb: None, epb: None,
time_user: 0, stat: CpuStat {
time_nice: 0, user: 0,
time_system: 0, nice: 0,
time_idle: 0, system: 0,
time_iowait: 0, idle: 0,
time_irq: 0, iowait: 0,
time_softirq: 0, irq: 0,
time_steal: 0, softirq: 0,
steal: 0,
},
temperature: None,
}; };
cpu.rescan()?; cpu.rescan(cache)?;
Ok(cpu) Ok(cpu)
} }
@ -101,11 +118,14 @@ impl Cpu {
const PATH: &str = "/sys/devices/system/cpu"; const PATH: &str = "/sys/devices/system/cpu";
let mut cpus = vec![]; let mut cpus = vec![];
let cache = CpuRescanCache::default();
for entry in fs::read_dir(PATH) for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read contents of '{PATH}'"))? .with_context(|| format!("failed to read CPU entries from '{PATH}'"))?
.flatten() .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{ {
let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_file_name = entry.file_name(); let entry_file_name = entry.file_name();
let Some(name) = entry_file_name.to_str() else { let Some(name) = entry_file_name.to_str() else {
@ -121,13 +141,13 @@ impl Cpu {
continue; continue;
}; };
cpus.push(Self::new(number)?); cpus.push(Self::new(number, &cache)?);
} }
// Fall back if sysfs iteration above fails to find any cpufreq CPUs. // Fall back if sysfs iteration above fails to find any cpufreq CPUs.
if cpus.is_empty() { if cpus.is_empty() {
for number in 0..num_cpus::get() as u32 { for number in 0..num_cpus::get() as u32 {
cpus.push(Self::new(number)?); cpus.push(Self::new(number, &cache)?);
} }
} }
@ -135,7 +155,7 @@ impl Cpu {
} }
/// Rescan CPU, tuning local copy of settings. /// Rescan CPU, tuning local copy of settings.
pub fn rescan(&mut self) -> anyhow::Result<()> { pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
let Self { number, .. } = self; let Self { number, .. } = self;
if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) {
@ -144,8 +164,6 @@ impl Cpu {
self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
self.rescan_times()?;
if self.has_cpufreq { if self.has_cpufreq {
self.rescan_governor()?; self.rescan_governor()?;
self.rescan_frequency()?; self.rescan_frequency()?;
@ -153,66 +171,8 @@ impl Cpu {
self.rescan_epb()?; self.rescan_epb()?;
} }
Ok(()) self.rescan_stat(cache)?;
} self.rescan_temperature(cache)?;
fn rescan_times(&mut self) -> anyhow::Result<()> {
// TODO: Don't read this per CPU. Share the read or
// find something in /sys/.../cpu{N} that does it.
let content = fs::read("/proc/stat")
.context("failed to read CPU stat")?
.context("/proc/stat does not exist")?;
let cpu_name = format!("cpu{number}", number = self.number);
let mut stats = content
.lines()
.find_map(|line| {
line.starts_with(&cpu_name)
.then(|| line.split_whitespace().skip(1))
})
.with_context(|| format!("failed to find {self} in CPU stats"))?;
self.time_user = stats
.next()
.with_context(|| format!("failed to parse {self} user time"))?
.parse()
.with_context(|| format!("failed to find {self} user time"))?;
self.time_nice = stats
.next()
.with_context(|| format!("failed to parse {self} nice time"))?
.parse()
.with_context(|| format!("failed to find {self} nice time"))?;
self.time_system = stats
.next()
.with_context(|| format!("failed to parse {self} system time"))?
.parse()
.with_context(|| format!("failed to find {self} system time"))?;
self.time_idle = stats
.next()
.with_context(|| format!("failed to parse {self} idle time"))?
.parse()
.with_context(|| format!("failed to find {self} idle time"))?;
self.time_iowait = stats
.next()
.with_context(|| format!("failed to parse {self} iowait time"))?
.parse()
.with_context(|| format!("failed to find {self} iowait time"))?;
self.time_irq = stats
.next()
.with_context(|| format!("failed to parse {self} irq time"))?
.parse()
.with_context(|| format!("failed to find {self} irq time"))?;
self.time_softirq = stats
.next()
.with_context(|| format!("failed to parse {self} softirq time"))?
.parse()
.with_context(|| format!("failed to find {self} softirq time"))?;
self.time_steal = stats
.next()
.with_context(|| format!("failed to parse {self} steal time"))?
.parse()
.with_context(|| format!("failed to find {self} steal time"))?;
Ok(()) Ok(())
} }
@ -341,6 +301,106 @@ impl Cpu {
Ok(()) Ok(())
} }
fn rescan_stat(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
// OnceCell::get_or_try_init is unstable. Cope:
let stat = match cache.stat.get() {
Some(stat) => stat,
None => {
let content = fs::read("/proc/stat")
.context("failed to read CPU stat")?
.context("/proc/stat does not exist")?;
cache
.stat
.set(HashMap::from_iter(content.lines().skip(1).filter_map(
|line| {
let mut parts = line.strip_prefix("cpu")?.split_whitespace();
let number = parts.next()?.parse().ok()?;
let stat = CpuStat {
user: parts.next()?.parse().ok()?,
nice: parts.next()?.parse().ok()?,
system: parts.next()?.parse().ok()?,
idle: parts.next()?.parse().ok()?,
iowait: parts.next()?.parse().ok()?,
irq: parts.next()?.parse().ok()?,
softirq: parts.next()?.parse().ok()?,
steal: parts.next()?.parse().ok()?,
};
Some((number, stat))
},
)))
.unwrap();
cache.stat.get().unwrap()
}
};
self.stat = stat
.get(&self.number)
.with_context(|| format!("failed to get stat of {self}"))?
.clone();
Ok(())
}
fn rescan_temperature(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
// OnceCell::get_or_try_init is unstable. Cope:
let temperatures = match cache.temperatures.get() {
Some(temperature) => temperature,
None => {
const PATH: &str = "/sys/class/hwmon";
let temperatures = HashMap::new();
for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read hardware information from '{PATH}'"))?
.with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))?
{
let entry =
entry.with_context(|| format!("failed to read entry of '{PATH}'"))?;
let entry_path = entry.path();
let Some(name) = fs::read(entry_path.join("name")).with_context(|| {
format!(
"failed to read name of hardware entry at '{path}'",
path = entry_path.display(),
)
})?
else {
continue;
};
match &*name {
// Intel CPU temperature driver
"coretemp" => todo!(),
// AMD CPU temperature driver
// TODO: 'zenergy' can also report those stats, I think?
"k10temp" | "zenpower" | "amdgpu" => todo!(),
// Other CPU temperature drivers
_ if name.contains("cpu") || name.contains("temp") => todo!(),
_ => {}
}
}
cache.temperatures.set(temperatures).unwrap();
cache.temperatures.get().unwrap()
}
};
self.temperature = temperatures.get(&self.number).copied();
Ok(())
}
pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> { pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> {
let Self { let Self {
number, number,

View file

@ -8,11 +8,19 @@ pub fn exists(path: impl AsRef<Path>) -> bool {
path.exists() path.exists()
} }
pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<fs::ReadDir> { pub fn read_dir(path: impl AsRef<Path>) -> anyhow::Result<Option<fs::ReadDir>> {
let path = path.as_ref(); let path = path.as_ref();
fs::read_dir(path) match fs::read_dir(path) {
.with_context(|| format!("failed to read directory '{path}'", path = path.display())) Ok(entries) => Ok(Some(entries)),
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => Err(error).context(format!(
"failed to read directory '{path}'",
path = path.display()
)),
}
} }
pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> { pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
@ -23,9 +31,7 @@ pub fn read(path: impl AsRef<Path>) -> anyhow::Result<Option<String>> {
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None),
Err(error) => { Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())),
Err(error).with_context(|| format!("failed to read '{path}", path = path.display()))
}
} }
} }

View file

@ -16,11 +16,7 @@ pub fn get_system_info() -> SystemInfo {
SystemInfo { cpu_model } SystemInfo { cpu_model }
} }
pub fn get_cpu_core_info( pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result<CpuCoreInfo> {
core_id: u32,
prev_times: &CpuTimes,
current_times: &CpuTimes,
) -> anyhow::Result<CpuCoreInfo> {
// Temperature detection. // Temperature detection.
// Should be generic enough to be able to support for multiple hardware sensors // Should be generic enough to be able to support for multiple hardware sensors
// with the possibility of extending later down the road. // with the possibility of extending later down the road.
@ -187,65 +183,6 @@ fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
None None
} }
pub fn get_all_cpu_core_info() -> anyhow::Result<Vec<CpuCoreInfo>> {
let initial_cpu_times = read_all_cpu_times()?;
thread::sleep(Duration::from_millis(250)); // interval for CPU usage calculation
let final_cpu_times = read_all_cpu_times()?;
let num_cores = get_real_cpus()
.map_err(|_| SysMonitorError::ReadError("Could not get the number of cores".to_string()))?;
let mut core_infos = Vec::with_capacity(num_cores as usize);
for core_id in 0..num_cores {
if let (Some(prev), Some(curr)) = (
initial_cpu_times.get(&core_id),
final_cpu_times.get(&core_id),
) {
match get_cpu_core_info(core_id, prev, curr) {
Ok(info) => core_infos.push(info),
Err(e) => {
// Log or handle error for a single core, maybe push a partial info or skip
eprintln!("Error getting info for core {core_id}: {e}");
}
}
} else {
// Log or handle missing times for a core
eprintln!("Missing CPU time data for core {core_id}");
}
}
Ok(core_infos)
}
pub fn get_cpu_global_info(cpu_cores: &[CpuCoreInfo]) -> CpuGlobalInfo {
// Calculate average CPU temperature from the core temperatures
let average_temperature_celsius = if cpu_cores.is_empty() {
None
} else {
// Filter cores with temperature readings, then calculate average
let cores_with_temp: Vec<&CpuCoreInfo> = cpu_cores
.iter()
.filter(|core| core.temperature_celsius.is_some())
.collect();
if cores_with_temp.is_empty() {
None
} else {
// Sum up all temperatures and divide by count
let sum: f32 = cores_with_temp
.iter()
.map(|core| core.temperature_celsius.unwrap())
.sum();
Some(sum / cores_with_temp.len() as f32)
}
};
// Return the constructed CpuGlobalInfo
CpuGlobalInfo {
average_temperature_celsius,
}
}
pub fn get_cpu_model() -> anyhow::Result<String> { pub fn get_cpu_model() -> anyhow::Result<String> {
let path = Path::new("/proc/cpuinfo"); let path = Path::new("/proc/cpuinfo");
let content = fs::read_to_string(path).map_err(|_| { let content = fs::read_to_string(path).map_err(|_| {

View file

@ -155,6 +155,7 @@ impl PowerSupply {
for entry in fs::read_dir(POWER_SUPPLY_PATH) for entry in fs::read_dir(POWER_SUPPLY_PATH)
.with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))? .with_context(|| format!("failed to read '{POWER_SUPPLY_PATH}'"))?
.with_context(|| format!("'{POWER_SUPPLY_PATH}' doesn't exist, are you on linux?"))?
{ {
let entry = match entry { let entry = match entry {
Ok(entry) => entry, Ok(entry) => entry,

View file

@ -99,8 +99,8 @@ impl System {
fn rescan_load_average(&mut self) -> anyhow::Result<()> { fn rescan_load_average(&mut self) -> anyhow::Result<()> {
let content = fs::read("/proc/loadavg") let content = fs::read("/proc/loadavg")
.context("failed to read load average")? .context("failed to read load average from '/proc/loadavg'")?
.context("load average file doesn't exist, are you on linux?")?; .context("'/proc/loadavg' doesn't exist, are you on linux?")?;
let mut parts = content.split_whitespace(); let mut parts = content.split_whitespace();