redbear-power: per-core governor display and Ctrl+g control
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user