diff --git a/src/config.rs b/src/config.rs index 2560806..4de0ac3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -53,10 +53,9 @@ impl CpuDelta { let mut cpus = match &self.for_ { Some(numbers) => { let mut cpus = Vec::with_capacity(numbers.len()); - let cache = cpu::CpuRescanCache::default(); for &number in numbers { - cpus.push(cpu::Cpu::new(number, &cache)?); + cpus.push(cpu::Cpu::new(number)?); } cpus diff --git a/src/cpu.rs b/src/cpu.rs index 3c0f332..6712cdf 100644 --- a/src/cpu.rs +++ b/src/cpu.rs @@ -1,50 +1,11 @@ use anyhow::{Context, bail}; use yansi::Paint as _; -use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString}; +use std::{fmt, string::ToString}; use crate::fs; -#[derive(Default, Debug, Clone, PartialEq)] -pub struct CpuRescanCache { - stat: OnceCell>, - temperatures: OnceCell>, -} - #[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 number: u32, @@ -63,9 +24,35 @@ pub struct Cpu { pub available_epbs: Vec, pub epb: Option, - pub stat: CpuStat, + pub time_user: u64, + 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, +} - pub temperature: Option, +impl Cpu { + 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 { @@ -77,7 +64,7 @@ impl fmt::Display for Cpu { } impl Cpu { - pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result { + pub fn new(number: u32) -> anyhow::Result { let mut cpu = Self { number, has_cpufreq: false, @@ -95,20 +82,16 @@ impl Cpu { available_epbs: Vec::new(), epb: None, - stat: CpuStat { - user: 0, - nice: 0, - system: 0, - idle: 0, - iowait: 0, - irq: 0, - softirq: 0, - steal: 0, - }, - - temperature: None, + time_user: 0, + time_nice: 0, + time_system: 0, + time_idle: 0, + time_iowait: 0, + time_irq: 0, + time_softirq: 0, + time_steal: 0, }; - cpu.rescan(cache)?; + cpu.rescan()?; Ok(cpu) } @@ -118,14 +101,11 @@ impl Cpu { const PATH: &str = "/sys/devices/system/cpu"; let mut cpus = vec![]; - let cache = CpuRescanCache::default(); for entry in fs::read_dir(PATH) - .with_context(|| format!("failed to read CPU entries from '{PATH}'"))? - .with_context(|| format!("'{PATH}' doesn't exist, are you on linux?"))? + .with_context(|| format!("failed to read contents of '{PATH}'"))? + .flatten() { - let entry = entry.with_context(|| format!("failed to read entry of '{PATH}'"))?; - let entry_file_name = entry.file_name(); let Some(name) = entry_file_name.to_str() else { @@ -141,13 +121,13 @@ impl Cpu { continue; }; - cpus.push(Self::new(number, &cache)?); + cpus.push(Self::new(number)?); } // Fall back if sysfs iteration above fails to find any cpufreq CPUs. if cpus.is_empty() { for number in 0..num_cpus::get() as u32 { - cpus.push(Self::new(number, &cache)?); + cpus.push(Self::new(number)?); } } @@ -155,7 +135,7 @@ impl Cpu { } /// Rescan CPU, tuning local copy of settings. - pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> { + pub fn rescan(&mut self) -> anyhow::Result<()> { let Self { number, .. } = self; if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) { @@ -164,6 +144,8 @@ impl Cpu { self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq")); + self.rescan_times()?; + if self.has_cpufreq { self.rescan_governor()?; self.rescan_frequency()?; @@ -171,8 +153,66 @@ impl Cpu { self.rescan_epb()?; } - self.rescan_stat(cache)?; - self.rescan_temperature(cache)?; + Ok(()) + } + + 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(()) } @@ -301,106 +341,6 @@ impl Cpu { 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<()> { let Self { number, diff --git a/src/fs.rs b/src/fs.rs index 3192e4d..526856d 100644 --- a/src/fs.rs +++ b/src/fs.rs @@ -8,19 +8,11 @@ pub fn exists(path: impl AsRef) -> bool { path.exists() } -pub fn read_dir(path: impl AsRef) -> anyhow::Result> { +pub fn read_dir(path: impl AsRef) -> anyhow::Result { let path = path.as_ref(); - match fs::read_dir(path) { - 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() - )), - } + fs::read_dir(path) + .with_context(|| format!("failed to read directory '{path}'", path = path.display())) } pub fn read(path: impl AsRef) -> anyhow::Result> { @@ -31,7 +23,9 @@ pub fn read(path: impl AsRef) -> anyhow::Result> { Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(None), - Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())), + Err(error) => { + Err(error).with_context(|| format!("failed to read '{path}", path = path.display())) + } } } diff --git a/src/monitor.rs b/src/monitor.rs index d4534ba..88195c8 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -16,7 +16,11 @@ pub fn get_system_info() -> SystemInfo { SystemInfo { cpu_model } } -pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result { +pub fn get_cpu_core_info( + core_id: u32, + prev_times: &CpuTimes, + current_times: &CpuTimes, +) -> anyhow::Result { // Temperature detection. // Should be generic enough to be able to support for multiple hardware sensors // with the possibility of extending later down the road. @@ -183,6 +187,65 @@ fn get_fallback_temperature(hw_path: &Path) -> Option { None } +pub fn get_all_cpu_core_info() -> anyhow::Result> { + 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 { let path = Path::new("/proc/cpuinfo"); let content = fs::read_to_string(path).map_err(|_| { diff --git a/src/power_supply.rs b/src/power_supply.rs index 2155c29..f213e5b 100644 --- a/src/power_supply.rs +++ b/src/power_supply.rs @@ -155,7 +155,6 @@ impl PowerSupply { for entry in fs::read_dir(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 { Ok(entry) => entry, diff --git a/src/system.rs b/src/system.rs index 57d5ce2..02b7b80 100644 --- a/src/system.rs +++ b/src/system.rs @@ -99,8 +99,8 @@ impl System { fn rescan_load_average(&mut self) -> anyhow::Result<()> { let content = fs::read("/proc/loadavg") - .context("failed to read load average from '/proc/loadavg'")? - .context("'/proc/loadavg' doesn't exist, are you on linux?")?; + .context("failed to read load average")? + .context("load average file doesn't exist, are you on linux?")?; let mut parts = content.split_whitespace();