redbear-power: per-core governor display and Ctrl+g control

This commit is contained in:
2026-07-07 22:16:27 +03:00
parent 596407a52d
commit 6e808741ea
5 changed files with 139 additions and 0 deletions
@@ -51,6 +51,53 @@ pub fn read_cpu_freq_khz_sysfs(cpu: u32) -> Option<u32> {
None
}
/// Read the current cpufreq governor for a specific CPU (Linux sysfs).
pub fn read_cpu_governor_sysfs(cpu: u32) -> Option<String> {
let path = format!(
"/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor",
cpu
);
fs::read_to_string(&path)
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
}
/// Read the list of available cpufreq governors for a specific CPU (Linux sysfs).
pub fn read_cpu_available_governors_sysfs(cpu: u32) -> Vec<String> {
let path = format!(
"/sys/devices/system/cpu/cpu{}/cpufreq/scaling_available_governors",
cpu
);
fs::read_to_string(&path)
.ok()
.map(|s| {
s.split_whitespace()
.map(|g| g.to_string())
.collect::<Vec<_>>()
})
.unwrap_or_default()
}
/// Read the min/max scaling frequencies for a specific CPU (Linux sysfs).
pub fn read_cpu_min_max_freq_sysfs(cpu: u32) -> (Option<u32>, Option<u32>) {
let min_path = format!(
"/sys/devices/system/cpu/cpu{}/cpufreq/scaling_min_freq",
cpu
);
let max_path = format!(
"/sys/devices/system/cpu/cpu{}/cpufreq/scaling_max_freq",
cpu
);
let min = fs::read_to_string(&min_path)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok());
let max = fs::read_to_string(&max_path)
.ok()
.and_then(|s| s.trim().parse::<u32>().ok());
(min, max)
}
/// Read package power in microwatts from RAPL MSR or powercap sysfs.
/// Tries MSR first (direct rdmsr via /dev/cpu/*/msr or /scheme/sys/msr),
/// then falls back to /sys/class/powercap/ (Linux sysfs).
@@ -94,6 +94,11 @@ pub struct CpuRow {
pub load_history: VecDeque<u8>,
pub core_type: CoreType,
pub hwp: Option<crate::msr::HwpInfo>,
/// Current cpufreq governor for this specific CPU (Linux sysfs),
/// or the global governor when no per-core source is available.
pub governor: String,
/// List of governors reported by the kernel for this CPU.
pub available_governors: Vec<String>,
}
impl CpuRow {
@@ -402,6 +407,8 @@ impl App {
load_history: VecDeque::with_capacity(LOAD_HISTORY_LEN),
core_type: type_for(id),
hwp: None,
governor: String::new(),
available_governors: Vec::new(),
}
})
.collect();
@@ -783,6 +790,8 @@ impl App {
.map(|row| self.sensors.pkg_temp_c(row.id))
.collect();
let global_governor = self.cpufreq.active.clone();
let global_available = self.cpufreq.available.clone();
self.collector_stats = crate::collector::collect(&mut self.cpus, |i, row| {
if let Some(status) = read_thermal_status(row.id) {
row.temp_c = if status & THERM_STATUS_READOUT_VALID != 0 {
@@ -830,6 +839,12 @@ impl App {
row.load_history
.push_back(row.load_pct.clamp(0.0, 100.0) as u8);
row.hwp = crate::msr::HwpInfo::read(row.id);
row.governor = crate::acpi::read_cpu_governor_sysfs(row.id)
.unwrap_or_else(|| global_governor.clone());
row.available_governors = crate::acpi::read_cpu_available_governors_sysfs(row.id);
if row.available_governors.is_empty() && !global_available.is_empty() {
row.available_governors = global_available.clone();
}
});
// Read package power from RAPL powercap (Intel/AMD). Requires
// kernel CONFIG_POWERCAP and intel_rapl/amd_energy driver.
@@ -952,6 +967,43 @@ impl App {
}
}
/// Cycle the governor of the currently selected CPU. On Linux this
/// writes the per-core sysfs file; on Redox cpufreqd is global so it
/// falls back to the system-wide governor.
pub fn cycle_selected_cpu_governor(&mut self) {
let Some(selected_idx) = self.table_state.selected() else {
self.flash_status("select a CPU first (arrow keys / jk)");
return;
};
let cpu = match self.cpus.get(selected_idx) {
Some(c) => c,
None => {
self.flash_status("no CPU selected");
return;
}
};
let cpu_id = cpu.id;
let current = cpu.governor.clone();
let available = cpu.available_governors.clone();
if available.len() < 2 {
self.flash_status(format!("CPU{} has no other governor available", cpu_id));
return;
}
let cur_pos = available.iter().position(|g| *g == current);
let start = cur_pos.unwrap_or(0);
for offset in 1..=available.len() {
let candidate = &available[(start + offset) % available.len()];
if crate::cpufreq::write_governor_for_cpu(cpu_id, candidate) {
self.flash_status(format!("CPU{} governor → {}", cpu_id, candidate));
return;
}
}
self.flash_status(format!(
"CPU{}: governor cycle failed (permission denied)",
cpu_id
));
}
/// Set the selected CPU to a specific P-state index (clamped to
/// the valid range). Used by D-Bus `set_pstate(target)`.
pub fn set_selected_pstate(&mut self, target: i32) {
@@ -305,3 +305,25 @@ pub fn write_governor_hint(governor: &str) -> bool {
false
}
}
/// Write a governor for a specific CPU. On Linux this targets the per-core
/// sysfs file; on Redox cpufreqd is system-wide, so this falls back to the
/// global governor file. Returns true if the write succeeded.
pub fn write_governor_for_cpu(cpu: u32, governor: &str) -> bool {
if std::path::Path::new("/scheme").exists() {
// Redox cpufreqd has a single system-wide governor.
return write_governor_hint(governor);
}
let path = format!(
"/sys/devices/system/cpu/cpu{}/cpufreq/scaling_governor",
cpu
);
if fs::write(&path, governor).is_err() {
return false;
}
// Verify by reading back.
fs::read_to_string(&path)
.ok()
.map(|s| s.trim() == governor)
.unwrap_or(false)
}
@@ -997,6 +997,7 @@ fn main() -> io::Result<()> {
Key::Char('P') => app.step_selected_pstate(1),
Key::Char('m') => app.force_min_pstate(),
Key::Char('M') => app.force_max_pstate(),
Key::Ctrl('g') => app.cycle_selected_cpu_governor(),
Key::Char('t') => app.toggle_throttle_mode(),
Key::Char('e') => {
app.expanded = !app.expanded;
@@ -2007,6 +2007,7 @@ pub fn render_cpu_table<'a>(
) -> Table<'a> {
let header = Row::new(vec![
"CPU".set_style(theme.label),
"Gov".set_style(theme.label),
"Freq/MHz".set_style(theme.label),
"PkgW".set_style(theme.label),
"Temp°C bar".set_style(theme.label),
@@ -2020,6 +2021,16 @@ pub fn render_cpu_table<'a>(
let rows: Vec<Row> = cpus
.iter()
.map(|cpu| {
let gov_abbrev = match cpu.governor.as_str() {
"performance" => "perf",
"powersave" => "pwr",
"ondemand" => "ondm",
"conservative" => "cons",
"schedutil" => "sched",
"userspace" => "user",
"" => "?",
other => other,
};
let freq_mhz = cpu.freq_khz / 1000;
let freq_style = if !cpu.pstates.is_empty() {
let max_f = cpu.pstates.first().map(|p| p.freq_khz).unwrap_or(0) / 1000;
@@ -2099,6 +2110,7 @@ pub fn render_cpu_table<'a>(
format!("{}{}", cpuid::core_type_label(cpu.core_type), cpu.id)
.set_style(theme.value),
),
Cell::from(gov_abbrev.set_style(theme.value)),
Cell::from(freq.set_style(freq_style)),
Cell::from(
pkg_power_w
@@ -2158,6 +2170,7 @@ pub fn render_cpu_table<'a>(
let power_w = pstate.power_mw as f64 / 1000.0;
rows.push(Row::new(vec![
label.set_style(s),
"".set_style(s),
format!("{freq_mhz} MHz").set_style(s),
format!("{power_w:.1} W").set_style(s),
format!("0x{:02x}", (pstate.ctl >> 8) & 0x7f).set_style(s),
@@ -2172,6 +2185,7 @@ pub fn render_cpu_table<'a>(
let s = sub_style;
rows.push(Row::new(vec![
" HWP caps".set_style(s),
"".set_style(s),
format!(
"eff={} guar={} max={}",
hwp.efficient_perf, hwp.guaranteed_perf, hwp.max_perf
@@ -2193,6 +2207,7 @@ pub fn render_cpu_table<'a>(
]));
rows.push(Row::new(vec![
" HWP req".set_style(s),
"".set_style(s),
format!(
"min={} max={} des={} EPP={} ({})",
hwp.min_request,
@@ -2216,6 +2231,7 @@ pub fn render_cpu_table<'a>(
rows,
[
Constraint::Length(7),
Constraint::Length(5),
Constraint::Length(10),
Constraint::Length(7),
Constraint::Length(8),
@@ -2349,6 +2365,7 @@ INTERACTIVE CONTROLS:
──────── POWER & CPU ────────
[g] cycle governor (Performance / Ondemand / Powersave)
[^g] cycle governor of selected CPU (Linux per-core)
[p/P] step selected CPU P-state down / up
[m/M] force selected CPU to min / max P-state
[t] toggle throttle mode (Auto / User / ForcedMin)