From 6e808741ea31c386167ba9c39d95189bdf3c4e2d Mon Sep 17 00:00:00 2001 From: vasilito Date: Tue, 7 Jul 2026 22:16:27 +0300 Subject: [PATCH] redbear-power: per-core governor display and Ctrl+g control --- .../system/redbear-power/source/src/acpi.rs | 47 +++++++++++++++++ .../system/redbear-power/source/src/app.rs | 52 +++++++++++++++++++ .../redbear-power/source/src/cpufreq.rs | 22 ++++++++ .../system/redbear-power/source/src/main.rs | 1 + .../system/redbear-power/source/src/render.rs | 17 ++++++ 5 files changed, 139 insertions(+) diff --git a/local/recipes/system/redbear-power/source/src/acpi.rs b/local/recipes/system/redbear-power/source/src/acpi.rs index 87bcf4db33..52d4bc7033 100644 --- a/local/recipes/system/redbear-power/source/src/acpi.rs +++ b/local/recipes/system/redbear-power/source/src/acpi.rs @@ -51,6 +51,53 @@ pub fn read_cpu_freq_khz_sysfs(cpu: u32) -> Option { None } +/// Read the current cpufreq governor for a specific CPU (Linux sysfs). +pub fn read_cpu_governor_sysfs(cpu: u32) -> Option { + 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 { + 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::>() + }) + .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, Option) { + 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::().ok()); + let max = fs::read_to_string(&max_path) + .ok() + .and_then(|s| s.trim().parse::().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). diff --git a/local/recipes/system/redbear-power/source/src/app.rs b/local/recipes/system/redbear-power/source/src/app.rs index 538d97a449..07505d1715 100644 --- a/local/recipes/system/redbear-power/source/src/app.rs +++ b/local/recipes/system/redbear-power/source/src/app.rs @@ -94,6 +94,11 @@ pub struct CpuRow { pub load_history: VecDeque, pub core_type: CoreType, pub hwp: Option, + /// 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, } 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) { diff --git a/local/recipes/system/redbear-power/source/src/cpufreq.rs b/local/recipes/system/redbear-power/source/src/cpufreq.rs index 2b800d1a34..ac4b6ce614 100644 --- a/local/recipes/system/redbear-power/source/src/cpufreq.rs +++ b/local/recipes/system/redbear-power/source/src/cpufreq.rs @@ -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) +} diff --git a/local/recipes/system/redbear-power/source/src/main.rs b/local/recipes/system/redbear-power/source/src/main.rs index ac352c6408..2f98de0cd2 100644 --- a/local/recipes/system/redbear-power/source/src/main.rs +++ b/local/recipes/system/redbear-power/source/src/main.rs @@ -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; diff --git a/local/recipes/system/redbear-power/source/src/render.rs b/local/recipes/system/redbear-power/source/src/render.rs index bb96c0dec7..2cda7d2116 100644 --- a/local/recipes/system/redbear-power/source/src/render.rs +++ b/local/recipes/system/redbear-power/source/src/render.rs @@ -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 = 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)