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:
parent
fd3ae29dc5
commit
421d4aaacc
5 changed files with 149 additions and 142 deletions
203
src/cpu.rs
203
src/cpu.rs
|
@ -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,
|
||||
|
|
18
src/fs.rs
18
src/fs.rs
|
@ -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())),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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(|_| {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue