1
Fork 0
mirror of https://github.com/RGBCube/superfreq synced 2025-07-27 17:07:44 +00:00

cpu: wip temperature scanning, waiting for raf to stand up from his desk and open his laptop on the other side of the room

This commit is contained in:
RGBCube 2025-05-28 22:35:31 +03:00
parent fd3ae29dc5
commit 421d4aaacc
Signed by: RGBCube
SSH key fingerprint: SHA256:CzqbPcfwt+GxFYNnFVCqoN5Itn4YFrshg1TrnACpA5M
5 changed files with 149 additions and 142 deletions

View file

@ -5,9 +5,10 @@ use std::{cell::OnceCell, collections::HashMap, fmt, string::ToString};
use crate::fs;
#[derive(Default, Debug, Clone, PartialEq, Eq)]
#[derive(Default, Debug, Clone, PartialEq)]
pub struct CpuRescanCache {
stat: OnceCell<HashMap<u32, CpuStat>>,
temperatures: OnceCell<HashMap<u32, f64>>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@ -22,7 +23,28 @@ pub struct CpuStat {
pub steal: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
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,
@ -42,27 +64,8 @@ pub struct Cpu {
pub epb: Option<String>,
pub stat: CpuStat,
}
impl Cpu {
pub fn time_total(&self) -> u64 {
self.stat.user
+ self.stat.nice
+ self.stat.system
+ self.stat.idle
+ self.stat.iowait
+ self.stat.irq
+ self.stat.softirq
+ self.stat.steal
}
pub fn time_idle(&self) -> u64 {
self.stat.idle + self.stat.iowait
}
pub fn usage(&self) -> f64 {
1.0 - self.time_idle() as f64 / self.time_total() as f64
}
pub temperature: Option<f64>,
}
impl fmt::Display for Cpu {
@ -102,6 +105,8 @@ impl Cpu {
softirq: 0,
steal: 0,
},
temperature: None,
};
cpu.rescan(cache)?;
@ -116,9 +121,11 @@ impl Cpu {
let cache = CpuRescanCache::default();
for entry in fs::read_dir(PATH)
.with_context(|| format!("failed to read contents of '{PATH}'"))?
.flatten()
.with_context(|| format!("failed to read CPU entries 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_file_name = entry.file_name();
let Some(name) = entry_file_name.to_str() else {
@ -157,8 +164,6 @@ impl Cpu {
self.has_cpufreq = fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
self.rescan_stat(cache)?;
if self.has_cpufreq {
self.rescan_governor()?;
self.rescan_frequency()?;
@ -166,50 +171,8 @@ impl Cpu {
self.rescan_epb()?;
}
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))
},
)));
cache.stat.get().unwrap()
}
};
self.stat = stat
.get(&self.number)
.with_context(|| format!("failed to get stat of {self}"))?
.clone();
self.rescan_stat(cache)?;
self.rescan_temperature(cache)?;
Ok(())
}
@ -338,6 +301,106 @@ 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,

View file

@ -8,11 +8,19 @@ pub fn exists(path: impl AsRef<Path>) -> bool {
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();
fs::read_dir(path)
.with_context(|| format!("failed to read directory '{path}'", path = path.display()))
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()
)),
}
}
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) => {
Err(error).with_context(|| format!("failed to read '{path}", path = path.display()))
}
Err(error) => Err(error).context(format!("failed to read '{path}", path = path.display())),
}
}

View file

@ -16,11 +16,7 @@ pub fn get_system_info() -> SystemInfo {
SystemInfo { cpu_model }
}
pub fn get_cpu_core_info(
core_id: u32,
prev_times: &CpuTimes,
current_times: &CpuTimes,
) -> anyhow::Result<CpuCoreInfo> {
pub fn get_cpu_core_info(core_id: u32) -> anyhow::Result<CpuCoreInfo> {
// Temperature detection.
// Should be generic enough to be able to support for multiple hardware sensors
// with the possibility of extending later down the road.
@ -187,65 +183,6 @@ fn get_fallback_temperature(hw_path: &Path) -> Option<f32> {
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> {
let path = Path::new("/proc/cpuinfo");
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)
.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,

View file

@ -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")?
.context("load average file doesn't exist, are you on linux?")?;
.context("failed to read load average from '/proc/loadavg'")?
.context("'/proc/loadavg' doesn't exist, are you on linux?")?;
let mut parts = content.split_whitespace();