mirror of
https://github.com/RGBCube/superfreq
synced 2025-07-28 09:27:44 +00:00
697 lines
18 KiB
Rust
697 lines
18 KiB
Rust
use std::{
|
|
cell::OnceCell,
|
|
collections::HashMap,
|
|
fmt,
|
|
mem,
|
|
rc::Rc,
|
|
string::ToString,
|
|
};
|
|
|
|
use anyhow::{
|
|
Context,
|
|
bail,
|
|
};
|
|
use yansi::Paint as _;
|
|
|
|
use crate::fs;
|
|
|
|
#[derive(Default, Debug, Clone, PartialEq)]
|
|
pub struct CpuRescanCache {
|
|
stat: OnceCell<HashMap<u32, CpuStat>>,
|
|
info: OnceCell<HashMap<u32, Rc<HashMap<String, String>>>>,
|
|
}
|
|
|
|
#[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,
|
|
|
|
pub has_cpufreq: bool,
|
|
|
|
pub available_governors: Vec<String>,
|
|
pub governor: Option<String>,
|
|
|
|
pub frequency_mhz: Option<u64>,
|
|
pub frequency_mhz_minimum: Option<u64>,
|
|
pub frequency_mhz_maximum: Option<u64>,
|
|
|
|
pub available_epps: Vec<String>,
|
|
pub epp: Option<String>,
|
|
|
|
pub available_epbs: Vec<String>,
|
|
pub epb: Option<String>,
|
|
|
|
pub stat: CpuStat,
|
|
pub info: Option<Rc<HashMap<String, String>>>,
|
|
|
|
pub temperature: Option<f64>,
|
|
}
|
|
|
|
impl fmt::Display for Cpu {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
let number = self.number.cyan();
|
|
|
|
write!(f, "CPU {number}")
|
|
}
|
|
}
|
|
|
|
impl Cpu {
|
|
pub fn new(number: u32, cache: &CpuRescanCache) -> anyhow::Result<Self> {
|
|
let mut cpu = Self {
|
|
number,
|
|
has_cpufreq: false,
|
|
|
|
available_governors: Vec::new(),
|
|
governor: None,
|
|
|
|
frequency_mhz: None,
|
|
frequency_mhz_minimum: None,
|
|
frequency_mhz_maximum: None,
|
|
|
|
available_epps: Vec::new(),
|
|
epp: None,
|
|
|
|
available_epbs: Vec::new(),
|
|
epb: None,
|
|
|
|
stat: CpuStat {
|
|
user: 0,
|
|
nice: 0,
|
|
system: 0,
|
|
idle: 0,
|
|
iowait: 0,
|
|
irq: 0,
|
|
softirq: 0,
|
|
steal: 0,
|
|
},
|
|
info: None,
|
|
|
|
temperature: None,
|
|
};
|
|
cpu.rescan(cache)?;
|
|
|
|
Ok(cpu)
|
|
}
|
|
|
|
/// Get all CPUs.
|
|
pub fn all() -> anyhow::Result<Vec<Cpu>> {
|
|
const PATH: &str = "/sys/devices/system/cpu";
|
|
|
|
let mut cpus = vec![];
|
|
let cache = CpuRescanCache::default();
|
|
|
|
for entry in fs::read_dir(PATH)
|
|
.context("failed to read CPU entries")?
|
|
.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 {
|
|
continue;
|
|
};
|
|
|
|
let Some(cpu_prefix_removed) = name.strip_prefix("cpu") else {
|
|
continue;
|
|
};
|
|
|
|
// Has to match "cpu{N}".
|
|
let Ok(number) = cpu_prefix_removed.parse() else {
|
|
continue;
|
|
};
|
|
|
|
cpus.push(Self::new(number, &cache)?);
|
|
}
|
|
|
|
// 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)?);
|
|
}
|
|
}
|
|
|
|
Ok(cpus)
|
|
}
|
|
|
|
/// Rescan CPU, tuning local copy of settings.
|
|
pub fn rescan(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
|
|
let Self { number, .. } = self;
|
|
|
|
if !fs::exists(format!("/sys/devices/system/cpu/cpu{number}")) {
|
|
bail!("{self} does not exist");
|
|
}
|
|
|
|
self.has_cpufreq =
|
|
fs::exists(format!("/sys/devices/system/cpu/cpu{number}/cpufreq"));
|
|
|
|
if self.has_cpufreq {
|
|
self.rescan_governor()?;
|
|
self.rescan_frequency()?;
|
|
self.rescan_epp()?;
|
|
self.rescan_epb()?;
|
|
}
|
|
|
|
self.rescan_stat(cache)?;
|
|
self.rescan_info(cache)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rescan_governor(&mut self) -> anyhow::Result<()> {
|
|
let Self { number, .. } = *self;
|
|
|
|
self.available_governors = 'available_governors: {
|
|
let Some(content) = fs::read(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
|
scaling_available_governors"
|
|
))
|
|
.with_context(|| format!("failed to read {self} available governors"))?
|
|
else {
|
|
break 'available_governors Vec::new();
|
|
};
|
|
|
|
content
|
|
.split_whitespace()
|
|
.map(ToString::to_string)
|
|
.collect()
|
|
};
|
|
|
|
self.governor = Some(
|
|
fs::read(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"
|
|
))
|
|
.with_context(|| format!("failed to read {self} scaling governor"))?
|
|
.with_context(|| format!("failed to find {self} scaling governor"))?,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rescan_frequency(&mut self) -> anyhow::Result<()> {
|
|
let Self { number, .. } = *self;
|
|
|
|
let frequency_khz = fs::read_n::<u64>(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_cur_freq"
|
|
))
|
|
.with_context(|| format!("failed to parse {self} frequency"))?
|
|
.with_context(|| format!("failed to find {self} frequency"))?;
|
|
let frequency_khz_minimum = fs::read_n::<u64>(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
|
))
|
|
.with_context(|| format!("failed to parse {self} frequency minimum"))?
|
|
.with_context(|| format!("failed to find {self} frequency"))?;
|
|
let frequency_khz_maximum = fs::read_n::<u64>(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"
|
|
))
|
|
.with_context(|| format!("failed to parse {self} frequency maximum"))?
|
|
.with_context(|| format!("failed to find {self} frequency"))?;
|
|
|
|
self.frequency_mhz = Some(frequency_khz / 1000);
|
|
self.frequency_mhz_minimum = Some(frequency_khz_minimum / 1000);
|
|
self.frequency_mhz_maximum = Some(frequency_khz_maximum / 1000);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rescan_epp(&mut self) -> anyhow::Result<()> {
|
|
let Self { number, .. } = *self;
|
|
|
|
self.available_epps = 'available_epps: {
|
|
let Some(content) = fs::read(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
|
energy_performance_available_preferences"
|
|
))
|
|
.with_context(|| format!("failed to read {self} available EPPs"))?
|
|
else {
|
|
break 'available_epps Vec::new();
|
|
};
|
|
|
|
content
|
|
.split_whitespace()
|
|
.map(ToString::to_string)
|
|
.collect()
|
|
};
|
|
|
|
self.epp = Some(
|
|
fs::read(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
|
energy_performance_preference"
|
|
))
|
|
.with_context(|| format!("failed to read {self} EPP"))?
|
|
.with_context(|| format!("failed to find {self} EPP"))?,
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rescan_epb(&mut self) -> anyhow::Result<()> {
|
|
let Self { number, .. } = self;
|
|
|
|
self.available_epbs = if self.has_cpufreq {
|
|
vec![
|
|
"1".to_owned(),
|
|
"2".to_owned(),
|
|
"3".to_owned(),
|
|
"4".to_owned(),
|
|
"5".to_owned(),
|
|
"6".to_owned(),
|
|
"7".to_owned(),
|
|
"8".to_owned(),
|
|
"9".to_owned(),
|
|
"10".to_owned(),
|
|
"11".to_owned(),
|
|
"12".to_owned(),
|
|
"13".to_owned(),
|
|
"14".to_owned(),
|
|
"15".to_owned(),
|
|
"performance".to_owned(),
|
|
"balance-performance".to_owned(),
|
|
"balance_performance".to_owned(), // Alternative form with underscore.
|
|
"balance-power".to_owned(),
|
|
"balance_power".to_owned(), // Alternative form with underscore.
|
|
"power".to_owned(),
|
|
]
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
self.epb = Some(
|
|
fs::read(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"
|
|
))
|
|
.with_context(|| format!("failed to read {self} EPB"))?
|
|
.with_context(|| format!("failed to find {self} 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))
|
|
},
|
|
)))
|
|
.unwrap();
|
|
|
|
cache.stat.get().unwrap()
|
|
},
|
|
};
|
|
|
|
self.stat = stat
|
|
.get(&self.number)
|
|
.with_context(|| format!("failed to get stat of {self}"))?
|
|
.clone();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn rescan_info(&mut self, cache: &CpuRescanCache) -> anyhow::Result<()> {
|
|
// OnceCell::get_or_try_init is unstable. Cope:
|
|
let info = match cache.info.get() {
|
|
Some(stat) => stat,
|
|
|
|
None => {
|
|
let content = fs::read("/proc/cpuinfo")
|
|
.context("failed to read CPU info")?
|
|
.context("/proc/cpuinfo does not exist")?;
|
|
|
|
let mut info = HashMap::new();
|
|
let mut current_number = None;
|
|
let mut current_data = HashMap::new();
|
|
|
|
macro_rules! try_save_data {
|
|
() => {
|
|
if let Some(number) = current_number.take() {
|
|
info.insert(number, Rc::new(mem::take(&mut current_data)));
|
|
}
|
|
};
|
|
}
|
|
|
|
for line in content.lines() {
|
|
let parts = line.splitn(2, ':').collect::<Vec<_>>();
|
|
|
|
if parts.len() == 2 {
|
|
let key = parts[0].trim();
|
|
let value = parts[1].trim();
|
|
|
|
if key == "processor" {
|
|
try_save_data!();
|
|
|
|
current_number = value.parse::<u32>().ok();
|
|
} else {
|
|
current_data.insert(key.to_owned(), value.to_owned());
|
|
}
|
|
}
|
|
}
|
|
|
|
try_save_data!();
|
|
|
|
cache.info.set(info).unwrap();
|
|
cache.info.get().unwrap()
|
|
},
|
|
};
|
|
|
|
self.info = info.get(&self.number).cloned();
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_governor(&mut self, governor: &str) -> anyhow::Result<()> {
|
|
let Self {
|
|
number,
|
|
available_governors: ref governors,
|
|
..
|
|
} = *self;
|
|
|
|
if !governors
|
|
.iter()
|
|
.any(|avail_governor| avail_governor == governor)
|
|
{
|
|
bail!(
|
|
"governor '{governor}' is not available for {self}. available \
|
|
governors: {governors}",
|
|
governors = governors.join(", "),
|
|
);
|
|
}
|
|
|
|
fs::write(
|
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_governor"),
|
|
governor,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"this probably means that {self} doesn't exist or doesn't support \
|
|
changing governors"
|
|
)
|
|
})?;
|
|
|
|
self.governor = Some(governor.to_owned());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_epp(&mut self, epp: &str) -> anyhow::Result<()> {
|
|
let Self {
|
|
number,
|
|
available_epps: ref epps,
|
|
..
|
|
} = *self;
|
|
|
|
if !epps.iter().any(|avail_epp| avail_epp == epp) {
|
|
bail!(
|
|
"EPP value '{epp}' is not available for {self}. available EPP values: \
|
|
{epps}",
|
|
epps = epps.join(", "),
|
|
);
|
|
}
|
|
|
|
fs::write(
|
|
format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/\
|
|
energy_performance_preference"
|
|
),
|
|
epp,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"this probably means that {self} doesn't exist or doesn't support \
|
|
changing EPP"
|
|
)
|
|
})?;
|
|
|
|
self.epp = Some(epp.to_owned());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_epb(&mut self, epb: &str) -> anyhow::Result<()> {
|
|
let Self {
|
|
number,
|
|
available_epbs: ref epbs,
|
|
..
|
|
} = *self;
|
|
|
|
if !epbs.iter().any(|avail_epb| avail_epb == epb) {
|
|
bail!(
|
|
"EPB value '{epb}' is not available for {self}. available EPB values: \
|
|
{valid}",
|
|
valid = epbs.join(", "),
|
|
);
|
|
}
|
|
|
|
fs::write(
|
|
format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/energy_performance_bias"
|
|
),
|
|
epb,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"this probably means that {self} doesn't exist or doesn't support \
|
|
changing EPB"
|
|
)
|
|
})?;
|
|
|
|
self.epb = Some(epb.to_owned());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_frequency_mhz_minimum(
|
|
&mut self,
|
|
frequency_mhz: u64,
|
|
) -> anyhow::Result<()> {
|
|
let Self { number, .. } = *self;
|
|
|
|
self.validate_frequency_mhz_minimum(frequency_mhz)?;
|
|
|
|
// We use u64 for the intermediate calculation to prevent overflow
|
|
let frequency_khz = frequency_mhz * 1000;
|
|
let frequency_khz = frequency_khz.to_string();
|
|
|
|
fs::write(
|
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"),
|
|
&frequency_khz,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"this probably means that {self} doesn't exist or doesn't support \
|
|
changing minimum frequency"
|
|
)
|
|
})?;
|
|
|
|
self.frequency_mhz_minimum = Some(frequency_mhz);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_frequency_mhz_minimum(
|
|
&self,
|
|
new_frequency_mhz: u64,
|
|
) -> anyhow::Result<()> {
|
|
let Self { number, .. } = self;
|
|
|
|
let Some(minimum_frequency_khz) = fs::read_n::<u64>(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_min_freq"
|
|
))
|
|
.with_context(|| format!("failed to read {self} minimum frequency"))?
|
|
else {
|
|
// Just let it pass if we can't find anything.
|
|
return Ok(());
|
|
};
|
|
|
|
if new_frequency_mhz * 1000 < minimum_frequency_khz {
|
|
bail!(
|
|
"new minimum frequency ({new_frequency_mhz} MHz) cannot be lower than \
|
|
the minimum frequency ({} MHz) for {self}",
|
|
minimum_frequency_khz / 1000,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_frequency_mhz_maximum(
|
|
&mut self,
|
|
frequency_mhz: u64,
|
|
) -> anyhow::Result<()> {
|
|
let Self { number, .. } = *self;
|
|
|
|
self.validate_frequency_mhz_maximum(frequency_mhz)?;
|
|
|
|
// We use u64 for the intermediate calculation to prevent overflow
|
|
let frequency_khz = frequency_mhz * 1000;
|
|
let frequency_khz = frequency_khz.to_string();
|
|
|
|
fs::write(
|
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"),
|
|
&frequency_khz,
|
|
)
|
|
.with_context(|| {
|
|
format!(
|
|
"this probably means that {self} doesn't exist or doesn't support \
|
|
changing maximum frequency"
|
|
)
|
|
})?;
|
|
|
|
self.frequency_mhz_maximum = Some(frequency_mhz);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn validate_frequency_mhz_maximum(
|
|
&self,
|
|
new_frequency_mhz: u64,
|
|
) -> anyhow::Result<()> {
|
|
let Self { number, .. } = self;
|
|
|
|
let Some(maximum_frequency_khz) = fs::read_n::<u64>(format!(
|
|
"/sys/devices/system/cpu/cpu{number}/cpufreq/scaling_max_freq"
|
|
))
|
|
.with_context(|| format!("failed to read {self} maximum frequency"))?
|
|
else {
|
|
// Just let it pass if we can't find anything.
|
|
return Ok(());
|
|
};
|
|
|
|
if new_frequency_mhz * 1000 > maximum_frequency_khz {
|
|
bail!(
|
|
"new maximum frequency ({new_frequency_mhz} MHz) cannot be higher \
|
|
than the maximum frequency ({} MHz) for {self}",
|
|
maximum_frequency_khz / 1000,
|
|
);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_turbo(on: bool) -> anyhow::Result<()> {
|
|
let value_boost = match on {
|
|
true => "1", // boost = 1 means turbo is enabled.
|
|
false => "0", // boost = 0 means turbo is disabled.
|
|
};
|
|
|
|
let value_boost_negated = match on {
|
|
true => "0", // no_turbo = 0 means turbo is enabled.
|
|
false => "1", // no_turbo = 1 means turbo is disabled.
|
|
};
|
|
|
|
// AMD specific paths
|
|
let amd_boost_path = "/sys/devices/system/cpu/amd_pstate/cpufreq/boost";
|
|
let msr_boost_path =
|
|
"/sys/devices/system/cpu/cpufreq/amd_pstate_enable_boost";
|
|
|
|
// Path priority (from most to least specific)
|
|
let intel_boost_path_negated =
|
|
"/sys/devices/system/cpu/intel_pstate/no_turbo";
|
|
let generic_boost_path = "/sys/devices/system/cpu/cpufreq/boost";
|
|
|
|
// Try each boost control path in order of specificity
|
|
if fs::write(intel_boost_path_negated, value_boost_negated).is_ok() {
|
|
return Ok(());
|
|
}
|
|
if fs::write(amd_boost_path, value_boost).is_ok() {
|
|
return Ok(());
|
|
}
|
|
if fs::write(msr_boost_path, value_boost).is_ok() {
|
|
return Ok(());
|
|
}
|
|
if fs::write(generic_boost_path, value_boost).is_ok() {
|
|
return Ok(());
|
|
}
|
|
|
|
// Also try per-core cpufreq boost for some AMD systems.
|
|
if Self::all()?.iter().any(|cpu| {
|
|
let Cpu { number, .. } = cpu;
|
|
|
|
fs::write(
|
|
format!("/sys/devices/system/cpu/cpu{number}/cpufreq/boost"),
|
|
value_boost,
|
|
)
|
|
.is_ok()
|
|
}) {
|
|
return Ok(());
|
|
}
|
|
|
|
bail!("no supported CPU boost control mechanism found");
|
|
}
|
|
|
|
pub fn turbo() -> anyhow::Result<Option<bool>> {
|
|
if let Some(content) =
|
|
fs::read_n::<u64>("/sys/devices/system/cpu/intel_pstate/no_turbo")
|
|
.context("failed to read CPU turbo boost status")?
|
|
{
|
|
return Ok(Some(content == 0));
|
|
}
|
|
|
|
if let Some(content) =
|
|
fs::read_n::<u64>("/sys/devices/system/cpu/cpufreq/boost")
|
|
.context("failed to read CPU turbo boost status")?
|
|
{
|
|
return Ok(Some(content == 1));
|
|
}
|
|
|
|
Ok(None)
|
|
}
|
|
}
|