diff --git a/local/recipes/system/cpufreqd/source/Cargo.toml b/local/recipes/system/cpufreqd/source/Cargo.toml index b8179d7443..21b9bc987f 100644 --- a/local/recipes/system/cpufreqd/source/Cargo.toml +++ b/local/recipes/system/cpufreqd/source/Cargo.toml @@ -8,8 +8,13 @@ name = "cpufreqd" path = "src/main.rs" [dependencies] -redox_syscall = { path = "../../../../../local/sources/syscall" } -log = "0.4" +libc = "0.2" +libredox = { path = "../../../../../local/sources/libredox", features = ["call", "std"] } +log = { version = "0.4", features = ["std"] } +redox-scheme = { path = "../../../../../local/sources/redox-scheme" } +syscall = { path = "../../../../../local/sources/syscall", package = "redox_syscall", features = ["std"] } [patch.crates-io] +redox-scheme = { path = "../../../../../local/sources/redox-scheme" } redox_syscall = { path = "../../../../../local/sources/syscall" } +libredox = { path = "../../../../../local/sources/libredox" } diff --git a/local/recipes/system/cpufreqd/source/src/main.rs b/local/recipes/system/cpufreqd/source/src/main.rs index 56510452f4..c4d328d770 100644 --- a/local/recipes/system/cpufreqd/source/src/main.rs +++ b/local/recipes/system/cpufreqd/source/src/main.rs @@ -1,8 +1,13 @@ +mod scheme; + +use std::collections::HashMap; use std::env; use std::fs; use std::io::{Read, Write}; +use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use log::{info, warn, LevelFilter}; +use scheme::{CpufreqShared, effective_governor}; // MSR addresses — see Intel SDM Vol 3B §14 const IA32_PERF_CTL: u32 = 0x199; // legacy P-state @@ -13,21 +18,53 @@ const IA32_PM_ENABLE: u32 = 0x770; // HWP enable bit // Hysteresis: minimum dwell time (in polls) at the current P-state // before we consider changing. Prevents thrashing at the // Ondemand/Conservative boundaries when load oscillates around the -// threshold. With POLL_MS=100ms and DWELL_POLLS=3 the minimum -// dwell is 300ms — well within the Linux kernel schedutil's +// threshold. With POLL_MS=100ms and DWELL_POLLS=3 the minimum dwell +// is 300ms — well within the Linux kernel schedutil's // typical 4-8ms response time but slow enough to avoid // spurious transitions. On QEMU, dwll is moot because the // PIIX4 doesn't model IA32_PERF_STATUS, but the read_only flag // from detect_virtualization() short-circuits MSR writes // entirely. const POLL_MS: u64 = 100; -const SAMPLE_WINDOW: usize = 10; -const STATE_WRITE_INTERVAL_S: u64 = 1; +pub(crate) const SAMPLE_WINDOW: usize = 10; const MSR_ERROR_SUPPRESS_COUNT: u32 = 1; const THERMAL_CACHE_MS: u64 = 1000; #[derive(Clone, Copy, PartialEq, Debug)] -enum Governor { Performance, Powersave, Ondemand, Conservative, Schedutil } +pub(crate) enum Governor { + Performance, + Powersave, + Ondemand, + Conservative, + Schedutil, +} + +impl Governor { + /// Lowercase Linux cpufreq governor name (e.g. `"ondemand"`). + pub(crate) fn as_str(self) -> &'static str { + match self { + Governor::Performance => "performance", + Governor::Powersave => "powersave", + Governor::Ondemand => "ondemand", + Governor::Conservative => "conservative", + Governor::Schedutil => "schedutil", + } + } + + /// Parse a governor name, case-insensitively. Returns `None` for any + /// name outside the valid Linux cpufreq governor set so callers can + /// reject it with `EINVAL` rather than silently accepting garbage. + pub(crate) fn from_name(name: &str) -> Option { + match name.trim().to_ascii_lowercase().as_str() { + "performance" => Some(Governor::Performance), + "powersave" => Some(Governor::Powersave), + "ondemand" => Some(Governor::Ondemand), + "conservative" => Some(Governor::Conservative), + "schedutil" => Some(Governor::Schedutil), + _ => None, + } + } +} struct StderrLogger; impl log::Log for StderrLogger { @@ -41,13 +78,13 @@ impl log::Log for StderrLogger { /// writes are ignored when HWP is active. We detect HWP via MSR 0x770 /// bit 0 and use IA32_HWP_REQUEST (0x774) with EPP hints. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum PstateMode { LegacyPerfCtl, Hwp } +pub(crate) enum PstateMode { LegacyPerfCtl, Hwp } #[derive(Clone)] -struct PState { - freq_khz: u32, - power_mw: u32, - ctl: u64, +pub(crate) struct PState { + pub(crate) freq_khz: u32, + pub(crate) power_mw: u32, + pub(crate) ctl: u64, } /// Minimum dwell time (in polls) at the current P-state before we @@ -60,51 +97,54 @@ struct PState { const DWELL_POLLS: u32 = 3; #[derive(Clone)] -struct CpuInfo { - id: u32, - pstates: Vec, - current_idx: usize, - load_history: [f64; SAMPLE_WINDOW], - load_idx: usize, - throttle: bool, - msr_errors: u32, - msr_suppressed: bool, - mode: PstateMode, - hwp_min: u8, // from MSR 0x771[15:8] - hwp_max: u8, // from MSR 0x771[7:0] +pub(crate) struct CpuInfo { + pub(crate) id: u32, + pub(crate) pstates: Vec, + pub(crate) current_idx: usize, + pub(crate) load_history: [f64; SAMPLE_WINDOW], + pub(crate) load_idx: usize, + pub(crate) throttle: bool, + pub(crate) msr_errors: u32, + pub(crate) msr_suppressed: bool, + pub(crate) mode: PstateMode, + pub(crate) hwp_min: u8, // from MSR 0x771[15:8] + pub(crate) hwp_max: u8, // from MSR 0x771[7:0] /// Number of consecutive polls at the current_idx. Reset to /// 0 on every state change. The next P-state transition /// only fires when dwell reaches DWELL_POLLS. This is the /// hystersis that stops the P0->P1->P0 oscillation on idle. - dwell: u32, + pub(crate) dwell: u32, /// P-state index the dwell counter is counting toward. Set /// each time choose_pstate returns a different target than /// the previous tick; reset to 0 when it matches the actual /// current_idx (= "no transition was requested"). - dwell_target: usize, + pub(crate) dwell_target: usize, /// When the host is a VM (QEMU, KVM, VMware, etc.) the MSR /// writes are no-ops on the underlying hardware emulation. /// In that case we don't even try to write; the load value is /// still tracked and the governor still logs its choice, but /// the P-state stays where the BIOS/bootloader left it. - read_only: bool, + pub(crate) read_only: bool, } fn detect_cpus() -> Vec { if let Ok(data) = fs::read_to_string("/scheme/sys/cpu") { for line in data.lines() { - if let Some(rest) = line.strip_prefix("CPUs: ") { - if let Ok(n) = rest.trim().parse::() { - if n > 0 { return (0..n).collect(); } - } + if let Some(rest) = line.strip_prefix("CPUs: ") + && let Ok(n) = rest.trim().parse::() + && n > 0 + { + return (0..n).collect(); } } } let mut v = Vec::new(); if let Ok(e) = fs::read_dir("/dev/cpu") { for x in e.flatten() { - if let Ok(n) = x.file_name().into_string() { - if let Ok(id) = n.parse() { v.push(id); } + if let Ok(n) = x.file_name().into_string() + && let Ok(id) = n.parse() + { + v.push(id); } } } @@ -148,14 +188,14 @@ fn read_acpi_pss(cpu: u32) -> Vec { let mut s = Vec::new(); for l in d.lines() { let w: Vec<&str> = l.split_whitespace().collect(); - if w.len() >= 6 { - if let (Ok(f), Ok(pw), Ok(ct)) = ( + if w.len() >= 6 + && let (Ok(f), Ok(pw), Ok(ct)) = ( w[0].parse(), w[2].parse(), u64::from_str_radix(w[5], 16), - ) { - s.push(PState { freq_khz: f, power_mw: pw, ctl: ct }); - } + ) + { + s.push(PState { freq_khz: f, power_mw: pw, ctl: ct }); } } if !s.is_empty() { return s; } @@ -292,20 +332,6 @@ fn check_thermal_raw() -> bool { if let Ok(d) = fs::read_to_string("/scheme/thermal/summary") { d.contains("critical") || d.contains("throttle") } else { false } } -fn write_scheme_state(governor: Governor, cpus: &[CpuInfo]) { - let mut out = format!("governor={:?}\n", governor); - for ci in cpus { - if ci.pstates.is_empty() { continue; } - let p = &ci.pstates[ci.current_idx.min(ci.pstates.len() - 1)]; - let mode_s = match ci.mode { - PstateMode::Hwp => "HWP", - PstateMode::LegacyPerfCtl => "legacy", - }; - out.push_str(&format!("CPU{} [{}]: {} kHz, {} mW, load={:.1}%\n", ci.id, mode_s, p.freq_khz, p.power_mw, avg_load(ci) * 100.0)); - } - let _ = fs::write("/scheme/cpufreq/state", out); -} - fn detect_virtualization() -> bool { // Detect a hypervisor / VM before applying P-state changes. // @@ -385,14 +411,14 @@ fn main() { info!("detected virtualized environment: cpufreqd will run in read-only mode (no MSR writes)"); } - let governor = match env::var("CPUFREQ_GOVERNOR").unwrap_or_default().as_str() { + let initial_governor = match env::var("CPUFREQ_GOVERNOR").unwrap_or_default().as_str() { "performance" => Governor::Performance, "powersave" => Governor::Powersave, "conservative" => Governor::Conservative, "schedutil" => Governor::Schedutil, _ => Governor::Ondemand, }; - let cpus = detect_cpus(); - info!("detected {} CPU(s), governor={:?}", cpus.len(), governor); - let mut ci: Vec = cpus.iter().map(|&id| { + let detected_cpus = detect_cpus(); + info!("detected {} CPU(s), governor={:?}", detected_cpus.len(), initial_governor); + let ci: Vec = detected_cpus.iter().map(|&id| { let mode = detect_pstate_mode(id); let (hwp_min, hwp_max) = if mode == PstateMode::Hwp { let (min, max, _, _) = read_hwp_capabilities(id); @@ -409,30 +435,64 @@ fn main() { info!("CPU{}: {} P-states ({} - {} kHz)", id, ps.len(), ps.first().map_or(0, |p| p.freq_khz), ps.last().map_or(0, |p| p.freq_khz)); CpuInfo { id, pstates: ps, current_idx: 0, load_history: [0.0; SAMPLE_WINDOW], load_idx: 0, throttle: false, msr_errors: 0, msr_suppressed: false, mode, hwp_min, hwp_max, dwell: 0, dwell_target: 0, read_only: virtualized } }).collect(); - let mut prev: Vec<(u64, u64)> = vec![(0, 0); cpus.len()]; + + // Shared state: the global governor, per-CPU governor overrides + // (written via /scheme/cpufreq/cpu/scaling_governor), and the + // live CPU vector are all shared with the scheme server thread. + // Lock order across the process is always governor -> overrides + // -> cpus; the main loop snapshots governor + overrides before + // taking the cpus lock so no lock is ever held while waiting on + // an earlier-ordered lock. + let governor = Arc::new(Mutex::new(initial_governor)); + let overrides: Arc>> = Arc::new(Mutex::new(HashMap::new())); + let cpus = Arc::new(Mutex::new(ci)); + + let shared = CpufreqShared { + governor: Arc::clone(&governor), + overrides: Arc::clone(&overrides), + cpus: Arc::clone(&cpus), + }; + if let Err(err) = scheme::spawn_scheme_server(shared) { + warn!("cpufreqd: scheme server did not start: {err}"); + } + + let mut prev: Vec<(u64, u64)> = vec![(0, 0); detected_cpus.len()]; let mut thermal = ThermalCache::new(); - let mut last_state_write = Instant::now(); + // Set initial P-state. For HWP we leave MSR 0x774 as BIOS-set // (defaults to performance) and just let the governor pick a // starting index. For legacy, write the lowest P-state's IA32_PERF_CTL. - for c in &ci { - if !c.pstates.is_empty() { - match c.mode { - PstateMode::Hwp => {} // HWP starts at the BIOS default - PstateMode::LegacyPerfCtl => { - let _ = write_msr(c.id, IA32_PERF_CTL, c.pstates[0].ctl); + { + let ci_guard = cpus.lock().unwrap(); + for c in ci_guard.iter() { + if !c.pstates.is_empty() { + match c.mode { + PstateMode::Hwp => {} // HWP starts at the BIOS default + PstateMode::LegacyPerfCtl => { + let _ = write_msr(c.id, IA32_PERF_CTL, c.pstates[0].ctl); + } } } } } + loop { std::thread::sleep(Duration::from_millis(POLL_MS)); let tt = thermal.get(); - for (i, c) in ci.iter_mut().enumerate() { + + // Snapshot the governor and overrides before locking cpus so + // the scheme server can always acquire governor/overrides + // without deadlocking against the cpus lock held below. + let global_governor = *governor.lock().unwrap(); + let overrides_snapshot = overrides.lock().unwrap().clone(); + + let mut ci_guard = cpus.lock().unwrap(); + for (i, c) in ci_guard.iter_mut().enumerate() { if c.pstates.is_empty() { continue; } let l = measure_load(c.id, &mut prev[i]); c.load_history[c.load_idx] = l; c.load_idx = (c.load_idx + 1) % SAMPLE_WINDOW; c.throttle = tt; - let n = choose_pstate(governor, c); + let g = effective_governor(global_governor, &overrides_snapshot, c.id); + let n = choose_pstate(g, c); // Dwell-based hysteresis: only transition after DWELL_POLLS // consecutive polls at the same target. This stops the // P0->P1->P0 oscillation on idle systems (QEMU and @@ -478,6 +538,5 @@ fn main() { } } } - if last_state_write.elapsed() >= Duration::from_secs(STATE_WRITE_INTERVAL_S) { write_scheme_state(governor, &ci); last_state_write = Instant::now(); } } } diff --git a/local/recipes/system/cpufreqd/source/src/scheme.rs b/local/recipes/system/cpufreqd/source/src/scheme.rs new file mode 100644 index 0000000000..f01834bf05 --- /dev/null +++ b/local/recipes/system/cpufreqd/source/src/scheme.rs @@ -0,0 +1,1035 @@ +//! `scheme:cpufreq` — Linux cpufreq-sysfs-compatible scheme server. +//! +//! cpufreqd owns the `/scheme/cpufreq` namespace and serves a read/write +//! control surface that mirrors the Linux +//! `/sys/devices/system/cpu/cpu/cpufreq/` layout so that thermal +//! managers (thermald) and monitoring tools (redbear-power) can switch +//! governors and read frequencies through the same path names they use +//! on Linux. +//! +//! # Layout +//! +//! | Path | Mode | Description | +//! |------|------|-------------| +//! | `governor` | rw | Global default governor. Writing switches **all** CPUs and clears per-CPU overrides. | +//! | `state` | ro | Legacy key=value text snapshot (same format `write_scheme_state` produced). | +//! | `scaling_available_governors` | ro | Space-separated list of valid governor names. | +//! | `control/governor` | rw | Alias of the global governor (thermald fallback path). | +//! | `cpu/scaling_governor` | rw | Per-CPU governor (override; falls back to global). | +//! | `cpu/scaling_cur_freq` | ro | Current frequency in kHz. | +//! | `cpu/scaling_min_freq` | ro | Policy minimum frequency in kHz. | +//! | `cpu/scaling_max_freq` | ro | Policy maximum frequency in kHz. | +//! | `cpu/cpuinfo_min_freq` | ro | Hardware minimum frequency in kHz. | +//! | `cpu/cpuinfo_max_freq` | ro | Hardware maximum frequency in kHz. | +//! | `cpu/cpuinfo_cur_freq` | ro | Alias of `scaling_cur_freq`. | +//! | `cpu/scaling_available_governors` | ro | Space-separated list of valid governor names. | +//! +//! # Cross-platform +//! +//! The pure logic helpers (`parse_cpu_index`, `effective_governor`, +//! `HandleKind`, `resolve_path`) compile on every target so they can be +//! unit-tested on the host. The `SchemeSync` implementation and the +//! scheme-server bootstrap are `#[cfg(target_os = "redox")]` only. + +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +#[cfg(target_os = "redox")] +use std::collections::BTreeMap; + +use crate::{CpuInfo, Governor}; + +#[cfg(target_os = "redox")] +use redox_scheme::scheme::{SchemeState, SchemeSync}; +#[cfg(target_os = "redox")] +use redox_scheme::{CallerCtx, OpenResult, SignalBehavior, Socket}; +#[cfg(target_os = "redox")] +use syscall::dirent::{DirEntry, DirentBuf, DirentKind}; +#[cfg(target_os = "redox")] +use syscall::error::{EACCES, EBADF, EINVAL, EIO, EISDIR, ENOENT, ENOTDIR, Error, Result}; +#[cfg(target_os = "redox")] +use syscall::flag::{EventFlags, MODE_DIR, MODE_FILE, O_ACCMODE, O_RDONLY, O_RDWR, O_WRONLY}; +#[cfg(target_os = "redox")] +use syscall::schemev2::NewFdFlags; +#[cfg(target_os = "redox")] +use syscall::Stat; + +/// Shared state between the main cpufreq loop and the scheme server. +/// +/// `governor` is the global default governor; the main loop reads it +/// every tick and the scheme server updates it on writes to +/// `/scheme/cpufreq/governor`. +/// +/// `overrides` carries per-CPU governor overrides set via +/// `cpu/scaling_governor`. An empty map means "every CPU follows the +/// global default". +/// +/// `cpus` is the live `Vec` owned by the main loop. The scheme +/// server reads it (under the mutex) to report frequencies; the main +/// loop mutates it (under the mutex) every poll cycle. +#[cfg_attr(not(target_os = "redox"), allow(dead_code))] +pub struct CpufreqShared { + pub governor: Arc>, + pub overrides: Arc>>, + pub cpus: Arc>>, +} + +/// Fixed, ordered list of governors the scheme accepts. Matches the +/// Linux `cpufreq` governor set plus `schedutil`. +pub const AVAILABLE_GOVERNORS: &[Governor] = &[ + Governor::Performance, + Governor::Powersave, + Governor::Ondemand, + Governor::Conservative, + Governor::Schedutil, +]; + +/// Parse a `cpu` path segment into a CPU index. +pub fn parse_cpu_index(segment: &str) -> Option { + segment.strip_prefix("cpu").and_then(|rest| rest.parse::().ok()) +} + +/// Resolve the effective governor for a CPU: the per-CPU override if +/// one is set, otherwise the global default. Pure function, testable on +/// the host without any Redox types. +pub fn effective_governor( + global: Governor, + overrides: &HashMap, + cpu_id: u32, +) -> Governor { + overrides.get(&cpu_id).copied().unwrap_or(global) +} + +/// Highest hardware frequency (kHz) across a CPU's P-states. +pub fn cpuinfo_max_freq(ci: &CpuInfo) -> u32 { + ci.pstates.iter().map(|p| p.freq_khz).max().unwrap_or(0) +} + +/// Lowest hardware frequency (kHz) across a CPU's P-states. +pub fn cpuinfo_min_freq(ci: &CpuInfo) -> u32 { + ci.pstates.iter().map(|p| p.freq_khz).min().unwrap_or(0) +} + +/// Current frequency (kHz) for a CPU based on its active P-state index. +pub fn current_freq(ci: &CpuInfo) -> u32 { + if ci.pstates.is_empty() { + return 0; + } + let idx = ci.current_idx.min(ci.pstates.len() - 1); + ci.pstates[idx].freq_khz +} + +/// Kind of file handle handed out by the scheme. Defined for every +/// target that builds the resolver or tests; only the `SchemeSync` +/// wiring is Redox-only. +#[cfg(any(test, target_os = "redox"))] +#[derive(Clone, Debug, PartialEq, Eq)] +enum HandleKind { + Root, + Governor, + State, + AvailableGovernors, + ControlDir, + CpuDir(u32), + CpuScalingGovernor(u32), + CpuScalingCurFreq(u32), + CpuScalingMinFreq(u32), + CpuScalingMaxFreq(u32), + CpuCpuinfoMinFreq(u32), + CpuCpuinfoMaxFreq(u32), + CpuCpuinfoCurFreq(u32), + CpuScalingAvailableGovernors(u32), +} + +#[cfg(target_os = "redox")] +impl HandleKind { + fn is_dir(&self) -> bool { + matches!(self, HandleKind::Root | HandleKind::ControlDir | HandleKind::CpuDir(_)) + } + + fn is_writable(&self) -> bool { + matches!( + self, + HandleKind::Governor | HandleKind::CpuScalingGovernor(_) + ) + } +} + +/// Resolve a scheme-relative path against the root into a handle kind. +/// Validates that any `cpu` segment refers to a known CPU id. +#[cfg(any(test, target_os = "redox"))] +fn resolve_path(path: &str, known_cpus: &[u32]) -> Option { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Some(HandleKind::Root); + } + + let segments: Vec<&str> = trimmed.split('/').filter(|s| !s.is_empty()).collect(); + // Fixed top-level entries. cpu/... paths are resolved in the + // `or_else` branch below so the slice patterns stay exhaustive. + match segments.as_slice() { + ["governor"] => Some(HandleKind::Governor), + ["state"] => Some(HandleKind::State), + ["scaling_available_governors"] => Some(HandleKind::AvailableGovernors), + ["control"] => Some(HandleKind::ControlDir), + ["control", "governor"] => Some(HandleKind::Governor), + _ => None, + } + .or_else(|| match segments.as_slice() { + [cpu_seg] => { + let id = parse_cpu_index(cpu_seg)?; + known_cpus.contains(&id).then_some(HandleKind::CpuDir(id)) + } + [cpu_seg, leaf] => { + let id = parse_cpu_index(cpu_seg)?; + if !known_cpus.contains(&id) { + return None; + } + match *leaf { + "scaling_governor" => Some(HandleKind::CpuScalingGovernor(id)), + "scaling_cur_freq" => Some(HandleKind::CpuScalingCurFreq(id)), + "scaling_min_freq" => Some(HandleKind::CpuScalingMinFreq(id)), + "scaling_max_freq" => Some(HandleKind::CpuScalingMaxFreq(id)), + "cpuinfo_min_freq" => Some(HandleKind::CpuCpuinfoMinFreq(id)), + "cpuinfo_max_freq" => Some(HandleKind::CpuCpuinfoMaxFreq(id)), + "cpuinfo_cur_freq" => Some(HandleKind::CpuCpuinfoCurFreq(id)), + "scaling_available_governors" => { + Some(HandleKind::CpuScalingAvailableGovernors(id)) + } + _ => None, + } + } + _ => None, + }) +} + +/// Format the legacy `state` snapshot as a `String`. Kept in sync with +/// the previous `write_scheme_state` output so redbear-power and any +/// other reader sees the same key=value layout. +pub fn format_scheme_state(governor: Governor, cpus: &[CpuInfo]) -> String { + let mut out = format!("governor={}\n", governor.as_str()); + for ci in cpus { + if ci.pstates.is_empty() { + continue; + } + let p = &ci.pstates[ci.current_idx.min(ci.pstates.len() - 1)]; + let mode_s = match ci.mode { + crate::PstateMode::Hwp => "HWP", + crate::PstateMode::LegacyPerfCtl => "legacy", + }; + let load = ci.load_history.iter().sum::() / ci.load_history.len().max(1) as f64; + out.push_str(&format!( + "CPU{} [{}]: {} kHz, {} mW, load={:.1}%\n", + ci.id, + mode_s, + p.freq_khz, + p.power_mw, + load * 100.0 + )); + } + out +} + +// --------------------------------------------------------------------------- +// Redox scheme server +// --------------------------------------------------------------------------- + +#[cfg(target_os = "redox")] +const SCHEME_NAME: &str = "cpufreq"; +#[cfg(target_os = "redox")] +const ROOT_ID: usize = 1; + +#[cfg(target_os = "redox")] +struct CpufreqScheme { + shared: CpufreqShared, + next_id: usize, + handles: BTreeMap, +} + +#[cfg(target_os = "redox")] +impl CpufreqScheme { + fn new(shared: CpufreqShared) -> Self { + Self { + shared, + next_id: ROOT_ID + 1, + handles: BTreeMap::new(), + } + } + + fn alloc_handle(&mut self, kind: HandleKind) -> usize { + let id = self.next_id; + self.next_id += 1; + self.handles.insert(id, kind); + id + } + + fn handle(&self, id: usize) -> Result<&HandleKind> { + self.handles.get(&id).ok_or(Error::new(EBADF)) + } + + fn known_cpus(&self) -> Vec { + match self.shared.cpus.lock() { + Ok(cpus) => cpus.iter().map(|c| c.id).collect(), + Err(_) => Vec::new(), + } + } + + fn open_kind(&mut self, dirfd: usize, path: &str) -> Result { + let known = self.known_cpus(); + let parent = if dirfd == ROOT_ID { + HandleKind::Root + } else { + self.handle(dirfd)?.clone() + }; + + match parent { + HandleKind::Root => resolve_path(path, &known) + .ok_or(Error::new(ENOENT)), + HandleKind::ControlDir => { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + Ok(HandleKind::ControlDir) + } else if trimmed == "governor" { + Ok(HandleKind::Governor) + } else { + Err(Error::new(ENOENT)) + } + } + HandleKind::CpuDir(id) => { + let trimmed = path.trim_matches('/'); + if trimmed.is_empty() { + return Ok(HandleKind::CpuDir(id)); + } + let fake_path = format!("cpu{id}/{trimmed}"); + resolve_path(&fake_path, &known).ok_or(Error::new(ENOENT)) + } + _ => Err(Error::new(EACCES)), + } + } + + fn read_global_governor(&self) -> Result { + let g = self + .shared + .governor + .lock() + .map_err(|_| Error::new(EIO))?; + Ok(format!("{}\n", g.as_str())) + } + + fn read_cpu_governor(&self, cpu_id: u32) -> Result { + let global = self.shared.governor.lock().map_err(|_| Error::new(EIO))?; + let g = match self.shared.overrides.lock() { + Ok(ov) => effective_governor(*global, &ov, cpu_id), + Err(_) => *global, + }; + Ok(format!("{}\n", g.as_str())) + } + + fn read_state(&self) -> Result { + let governor = self.shared.governor.lock().map_err(|_| Error::new(EIO))?; + let cpus = self.shared.cpus.lock().map_err(|_| Error::new(EIO))?; + Ok(format_scheme_state(*governor, &cpus)) + } + + fn cpu_info(&self, cpu_id: u32) -> Result { + let cpus = self.shared.cpus.lock().map_err(|_| Error::new(EIO))?; + cpus + .iter() + .find(|c| c.id == cpu_id) + .cloned() + .ok_or(Error::new(ENOENT)) + } + + fn read_freq(&self, cpu_id: u32, kind: &HandleKind) -> Result { + let ci = self.cpu_info(cpu_id)?; + let khz = match kind { + HandleKind::CpuScalingCurFreq(_) | HandleKind::CpuCpuinfoCurFreq(_) => current_freq(&ci), + HandleKind::CpuScalingMinFreq(_) | HandleKind::CpuCpuinfoMinFreq(_) => { + cpuinfo_min_freq(&ci) + } + HandleKind::CpuScalingMaxFreq(_) | HandleKind::CpuCpuinfoMaxFreq(_) => { + cpuinfo_max_freq(&ci) + } + _ => return Err(Error::new(EINVAL)), + }; + Ok(format!("{khz}\n")) + } + + fn read_available_governors(&self) -> String { + let names: Vec<&str> = AVAILABLE_GOVERNORS.iter().map(|g| g.as_str()).collect(); + format!("{}\n", names.join(" ")) + } + + fn read_handle(&self, kind: &HandleKind) -> Result { + match kind { + HandleKind::Root => { + let cpus = self.known_cpus(); + let mut entries: Vec = [ + "governor", + "state", + "scaling_available_governors", + "control", + ] + .into_iter() + .map(String::from) + .collect(); + for id in &cpus { + entries.push(format!("cpu{id}")); + } + Ok(format!("{}\n", entries.join("\n"))) + } + HandleKind::ControlDir => Ok("governor\n".to_string()), + HandleKind::Governor => self.read_global_governor(), + HandleKind::State => self.read_state(), + HandleKind::AvailableGovernors | HandleKind::CpuScalingAvailableGovernors(_) => { + Ok(self.read_available_governors()) + } + HandleKind::CpuDir(id) => { + let _ = self.cpu_info(*id)?; + Ok( + "scaling_governor\nscaling_cur_freq\nscaling_min_freq\nscaling_max_freq\ncpuinfo_min_freq\ncpuinfo_max_freq\ncpuinfo_cur_freq\nscaling_available_governors\n" + .to_string(), + ) + } + HandleKind::CpuScalingGovernor(id) => self.read_cpu_governor(*id), + freq_kind @ (HandleKind::CpuScalingCurFreq(id) + | HandleKind::CpuScalingMinFreq(id) + | HandleKind::CpuScalingMaxFreq(id) + | HandleKind::CpuCpuinfoMinFreq(id) + | HandleKind::CpuCpuinfoMaxFreq(id) + | HandleKind::CpuCpuinfoCurFreq(id)) => self.read_freq(*id, freq_kind), + } + } + + fn write_handle(&self, kind: &HandleKind, buf: &[u8]) -> Result<()> { + let name = std::str::from_utf8(buf) + .map_err(|_| Error::new(EINVAL))? + .trim() + .to_ascii_lowercase(); + let new_governor = + Governor::from_name(&name).ok_or(Error::new(EINVAL))?; + + match kind { + HandleKind::Governor => { + // Global write: switch the default AND drop every per-CPU + // override so all CPUs follow the new governor. This + // mirrors Linux, where writing the global policy governor + // applies to every CPU in the policy. + let mut governor = self.shared.governor.lock().map_err(|_| Error::new(EIO))?; + *governor = new_governor; + if let Ok(mut overrides) = self.shared.overrides.lock() { + overrides.clear(); + } + Ok(()) + } + HandleKind::CpuScalingGovernor(id) => { + let mut overrides = self.shared.overrides.lock().map_err(|_| Error::new(EIO))?; + overrides.insert(*id, new_governor); + Ok(()) + } + _ => Err(Error::new(EACCES)), + } + } + + fn path_for(&self, kind: &HandleKind) -> String { + match kind { + HandleKind::Root => format!("{SCHEME_NAME}:/"), + HandleKind::Governor => format!("{SCHEME_NAME}:/governor"), + HandleKind::State => format!("{SCHEME_NAME}:/state"), + HandleKind::AvailableGovernors => { + format!("{SCHEME_NAME}:/scaling_available_governors") + } + HandleKind::ControlDir => format!("{SCHEME_NAME}:/control"), + HandleKind::CpuDir(id) => format!("{SCHEME_NAME}:/cpu{id}"), + HandleKind::CpuScalingGovernor(id) => { + format!("{SCHEME_NAME}:/cpu{id}/scaling_governor") + } + HandleKind::CpuScalingCurFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/scaling_cur_freq") + } + HandleKind::CpuScalingMinFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/scaling_min_freq") + } + HandleKind::CpuScalingMaxFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/scaling_max_freq") + } + HandleKind::CpuCpuinfoMinFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/cpuinfo_min_freq") + } + HandleKind::CpuCpuinfoMaxFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/cpuinfo_max_freq") + } + HandleKind::CpuCpuinfoCurFreq(id) => { + format!("{SCHEME_NAME}:/cpu{id}/cpuinfo_cur_freq") + } + HandleKind::CpuScalingAvailableGovernors(id) => { + format!("{SCHEME_NAME}:/cpu{id}/scaling_available_governors") + } + } + } + + fn directory_entries(&self, kind: &HandleKind) -> Result> { + match kind { + HandleKind::Root => { + let mut entries: Vec<(&'static str, DirentKind)> = vec![ + ("governor", DirentKind::Regular), + ("state", DirentKind::Regular), + ("scaling_available_governors", DirentKind::Regular), + ("control", DirentKind::Directory), + ]; + for id in self.known_cpus() { + // Leak a stable name; cpufreqd has a bounded CPU count. + let name: &'static str = Box::leak(format!("cpu{id}").into_boxed_str()); + entries.push((name, DirentKind::Directory)); + } + Ok(entries) + } + HandleKind::ControlDir => Ok(vec![("governor", DirentKind::Regular)]), + HandleKind::CpuDir(_) => Ok(vec![ + ("scaling_governor", DirentKind::Regular), + ("scaling_cur_freq", DirentKind::Regular), + ("scaling_min_freq", DirentKind::Regular), + ("scaling_max_freq", DirentKind::Regular), + ("cpuinfo_min_freq", DirentKind::Regular), + ("cpuinfo_max_freq", DirentKind::Regular), + ("cpuinfo_cur_freq", DirentKind::Regular), + ("scaling_available_governors", DirentKind::Regular), + ]), + _ => Err(Error::new(ENOTDIR)), + } + } +} + +#[cfg(target_os = "redox")] +impl SchemeSync for CpufreqScheme { + fn scheme_root(&mut self) -> Result { + Ok(ROOT_ID) + } + + fn openat( + &mut self, + dirfd: usize, + path: &str, + flags: usize, + _fcntl_flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let accmode = flags & O_ACCMODE; + if accmode != O_RDONLY && accmode != O_WRONLY && accmode != O_RDWR { + return Err(Error::new(EACCES)); + } + + let kind = self.open_kind(dirfd, path)?; + + // Reject write opens on read-only files early so the handle never + // looks writable. Read-only files: everything except governor and + // per-CPU scaling_governor. + if (accmode == O_WRONLY || accmode == O_RDWR) && !kind.is_writable() { + return Err(Error::new(EACCES)); + } + + Ok(OpenResult::ThisScheme { + number: self.alloc_handle(kind), + flags: NewFdFlags::POSITIONED, + }) + } + + fn read( + &mut self, + id: usize, + buf: &mut [u8], + offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = if id == ROOT_ID { + HandleKind::Root + } else { + self.handle(id)?.clone() + }; + if kind.is_dir() { + return Err(Error::new(EISDIR)); + } + let data = self.read_handle(&kind)?; + let bytes = data.as_bytes(); + let start = usize::try_from(offset).map_err(|_| Error::new(EINVAL))?; + if start >= bytes.len() { + return Ok(0); + } + let count = (bytes.len() - start).min(buf.len()); + buf[..count].copy_from_slice(&bytes[start..start + count]); + Ok(count) + } + + fn write( + &mut self, + id: usize, + buf: &[u8], + _offset: u64, + _flags: u32, + _ctx: &CallerCtx, + ) -> Result { + let kind = self.handle(id)?.clone(); + self.write_handle(&kind, buf)?; + Ok(buf.len()) + } + + fn fstat(&mut self, id: usize, stat: &mut Stat, _ctx: &CallerCtx) -> Result<()> { + let kind = if id == ROOT_ID { + HandleKind::Root + } else { + self.handle(id)?.clone() + }; + stat.st_mode = if kind.is_dir() { + MODE_DIR | 0o755 + } else { + MODE_FILE | 0o644 + }; + stat.st_size = if kind.is_dir() { + 0 + } else { + match self.read_handle(&kind) { + Ok(data) => u64::try_from(data.len()).unwrap_or(0), + Err(_) => 0, + } + }; + Ok(()) + } + + fn fpath(&mut self, id: usize, buf: &mut [u8], _ctx: &CallerCtx) -> Result { + let kind = if id == ROOT_ID { + HandleKind::Root + } else { + self.handle(id)?.clone() + }; + let path = self.path_for(&kind); + let bytes = path.as_bytes(); + let count = bytes.len().min(buf.len()); + buf[..count].copy_from_slice(&bytes[..count]); + Ok(count) + } + + fn fsync(&mut self, id: usize, _ctx: &CallerCtx) -> Result<()> { + if id != ROOT_ID { + let _ = self.handle(id)?; + } + Ok(()) + } + + fn fcntl(&mut self, id: usize, _cmd: usize, _arg: usize, _ctx: &CallerCtx) -> Result { + if id != ROOT_ID { + let _ = self.handle(id)?; + } + Ok(0) + } + + fn fevent(&mut self, id: usize, _flags: EventFlags, _ctx: &CallerCtx) -> Result { + if id != ROOT_ID { + let _ = self.handle(id)?; + } + Ok(EventFlags::empty()) + } + + fn getdents<'buf>( + &mut self, + id: usize, + mut buf: DirentBuf<&'buf mut [u8]>, + opaque_offset: u64, + ) -> Result> { + let kind = if id == ROOT_ID { + HandleKind::Root + } else { + self.handle(id)?.clone() + }; + let entries = self.directory_entries(&kind)?; + let start = usize::try_from(opaque_offset).unwrap_or(usize::MAX); + for (i, (name, entry_kind)) in entries.iter().enumerate().skip(start) { + if let Err(err) = buf.entry(DirEntry { + inode: 0, + next_opaque_id: (i as u64) + 1, + name, + kind: *entry_kind, + }) { + if err.errno == EINVAL { + break; + } + return Err(err); + } + } + Ok(buf) + } + + fn on_close(&mut self, id: usize) { + if id != ROOT_ID { + self.handles.remove(&id); + } + } +} + +/// Boot the scheme server. On Redox this registers `scheme:cpufreq` and +/// spawns a dedicated thread that services requests for the lifetime of +/// the daemon. On non-Redox targets (host `cargo test`/`cargo check`) it +/// is a no-op so the rest of cpufreqd still compiles. +#[cfg(target_os = "redox")] +pub fn spawn_scheme_server(shared: CpufreqShared) -> Result<(), String> { + let socket = + Socket::create().map_err(|err| format!("cpufreqd: failed to create scheme socket: {err}"))?; + + let mut scheme = CpufreqScheme::new(shared); + redox_scheme::scheme::register_sync_scheme(&socket, SCHEME_NAME, &mut scheme) + .map_err(|err| format!("cpufreqd: failed to register scheme:{SCHEME_NAME}: {err}"))?; + + log::info!("cpufreqd: registered scheme:{SCHEME_NAME}"); + + std::thread::Builder::new() + .name("cpufreq-scheme".to_string()) + .spawn(move || { + let mut state = SchemeState::new(); + loop { + let request = match socket.next_request(SignalBehavior::Restart) { + Ok(Some(request)) => request, + Ok(None) => { + log::info!("cpufreqd: scheme socket closed, shutting down scheme server"); + break; + } + Err(err) => { + log::error!("cpufreqd: failed to read scheme request: {err}"); + break; + } + }; + + if let redox_scheme::RequestKind::Call(request) = request.kind() { + let response = request.handle_sync(&mut scheme, &mut state); + if let Err(err) = socket.write_response(response, SignalBehavior::Restart) { + log::error!("cpufreqd: failed to write scheme response: {err}"); + break; + } + } + } + }) + .map_err(|err| format!("cpufreqd: failed to spawn scheme server thread: {err}"))?; + + Ok(()) +} + +#[cfg(not(target_os = "redox"))] +pub fn spawn_scheme_server(_shared: CpufreqShared) -> Result<(), String> { + // Host builds have no scheme surface; the daemon's MSR/P-state loop + // is exercised via the host-runnable unit tests instead. + Ok(()) +} + +// --------------------------------------------------------------------------- +// Tests — host-runnable, no Redox runtime required. +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::{CpuInfo, Governor, PState, PstateMode}; + + fn mock_pstates() -> Vec { + vec![ + PState { freq_khz: 2400, power_mw: 35000, ctl: 0x1a00 }, + PState { freq_khz: 2000, power_mw: 25000, ctl: 0x1600 }, + PState { freq_khz: 1600, power_mw: 18000, ctl: 0x1200 }, + PState { freq_khz: 1200, power_mw: 12000, ctl: 0x0e00 }, + ] + } + + fn mock_cpu(id: u32) -> CpuInfo { + CpuInfo { + id, + pstates: mock_pstates(), + current_idx: 0, + load_history: [0.0; crate::SAMPLE_WINDOW], + load_idx: 0, + throttle: false, + msr_errors: 0, + msr_suppressed: false, + mode: PstateMode::LegacyPerfCtl, + hwp_min: 0, + hwp_max: 0, + dwell: 0, + dwell_target: 0, + read_only: true, + } + } + + // --- Governor name parsing (case-insensitive, valid set) --- + + #[test] + fn parse_valid_governor_names() { + assert_eq!(Governor::from_name("performance"), Some(Governor::Performance)); + assert_eq!(Governor::from_name("powersave"), Some(Governor::Powersave)); + assert_eq!(Governor::from_name("ondemand"), Some(Governor::Ondemand)); + assert_eq!(Governor::from_name("conservative"), Some(Governor::Conservative)); + assert_eq!(Governor::from_name("schedutil"), Some(Governor::Schedutil)); + } + + #[test] + fn parse_governor_names_case_insensitive() { + assert_eq!(Governor::from_name("PERFORMANCE"), Some(Governor::Performance)); + assert_eq!(Governor::from_name("OnDemand"), Some(Governor::Ondemand)); + assert_eq!(Governor::from_name("PowerSave"), Some(Governor::Powersave)); + } + + #[test] + fn parse_governor_names_ignores_surrounding_whitespace() { + assert_eq!( + Governor::from_name(" ondemand ".trim()), + Some(Governor::Ondemand) + ); + } + + #[test] + fn reject_invalid_governor_names() { + assert_eq!(Governor::from_name("turbo"), None); + assert_eq!(Governor::from_name(""), None); + assert_eq!(Governor::from_name("userspace"), None); + assert_eq!(Governor::from_name("performance123"), None); + } + + #[test] + fn governor_round_trip_str() { + for g in AVAILABLE_GOVERNORS { + let name = g.as_str(); + assert_eq!(Governor::from_name(name), Some(*g)); + } + } + + // --- Round-trip the governor through Arc> --- + + #[test] + fn governor_round_trip_through_arc_mutex() { + let shared = Governor::Ondemand; + let cell = Arc::new(Mutex::new(shared)); + assert_eq!(*cell.lock().unwrap(), Governor::Ondemand); + + // Simulate a scheme write. + { + let mut g = cell.lock().unwrap(); + *g = Governor::from_name("powersave").unwrap(); + } + assert_eq!(*cell.lock().unwrap(), Governor::Powersave); + + // Clone the Arc and verify the change is visible to another holder. + let other = Arc::clone(&cell); + assert_eq!(*other.lock().unwrap(), Governor::Powersave); + } + + // --- Path parsing --- + + #[test] + fn parse_cpu_index_valid() { + assert_eq!(parse_cpu_index("cpu0"), Some(0)); + assert_eq!(parse_cpu_index("cpu7"), Some(7)); + assert_eq!(parse_cpu_index("cpu127"), Some(127)); + } + + #[test] + fn parse_cpu_index_invalid() { + assert_eq!(parse_cpu_index("cpu"), None); + assert_eq!(parse_cpu_index("cpuX"), None); + assert_eq!(parse_cpu_index("cpus"), None); + assert_eq!(parse_cpu_index("0"), None); + } + + #[test] + fn resolve_root_paths() { + let known = [0u32, 1, 2]; + assert_eq!(resolve_path("", &known), Some(HandleKind::Root)); + assert_eq!(resolve_path("/", &known), Some(HandleKind::Root)); + assert_eq!(resolve_path("governor", &known), Some(HandleKind::Governor)); + assert_eq!(resolve_path("state", &known), Some(HandleKind::State)); + assert_eq!( + resolve_path("scaling_available_governors", &known), + Some(HandleKind::AvailableGovernors) + ); + assert_eq!(resolve_path("control", &known), Some(HandleKind::ControlDir)); + assert_eq!( + resolve_path("control/governor", &known), + Some(HandleKind::Governor) + ); + } + + #[test] + fn resolve_cpu_paths() { + let known = [0u32, 1]; + assert_eq!(resolve_path("cpu0", &known), Some(HandleKind::CpuDir(0))); + assert_eq!(resolve_path("cpu1", &known), Some(HandleKind::CpuDir(1))); + assert_eq!( + resolve_path("cpu0/scaling_governor", &known), + Some(HandleKind::CpuScalingGovernor(0)) + ); + assert_eq!( + resolve_path("cpu1/scaling_cur_freq", &known), + Some(HandleKind::CpuScalingCurFreq(1)) + ); + assert_eq!( + resolve_path("cpu0/cpuinfo_max_freq", &known), + Some(HandleKind::CpuCpuinfoMaxFreq(0)) + ); + assert_eq!( + resolve_path("cpu0/cpuinfo_cur_freq", &known), + Some(HandleKind::CpuCpuinfoCurFreq(0)) + ); + assert_eq!( + resolve_path("cpu0/scaling_available_governors", &known), + Some(HandleKind::CpuScalingAvailableGovernors(0)) + ); + } + + #[test] + fn resolve_rejects_unknown_cpu() { + let known = [0u32]; + assert_eq!(resolve_path("cpu9", &known), None); + assert_eq!(resolve_path("cpu9/scaling_governor", &known), None); + } + + #[test] + fn resolve_rejects_unknown_leaf() { + let known = [0u32]; + assert_eq!(resolve_path("cpu0/bogus", &known), None); + assert_eq!(resolve_path("bogus", &known), None); + } + + // --- Per-CPU override logic --- + + #[test] + fn effective_governor_falls_back_to_global() { + let global = Governor::Ondemand; + let overrides = HashMap::new(); + assert_eq!(effective_governor(global, &overrides, 0), Governor::Ondemand); + assert_eq!(effective_governor(global, &overrides, 7), Governor::Ondemand); + } + + #[test] + fn effective_governor_uses_per_cpu_override() { + let global = Governor::Ondemand; + let mut overrides = HashMap::new(); + overrides.insert(1, Governor::Performance); + assert_eq!(effective_governor(global, &overrides, 0), Governor::Ondemand); + assert_eq!(effective_governor(global, &overrides, 1), Governor::Performance); + } + + /// Integration: a global governor write clears overrides so every + /// CPU follows the new default. Mirrors the scheme's write_handle + /// contract for `/scheme/cpufreq/governor`. + #[test] + fn global_governor_write_clears_overrides() { + let governor = Arc::new(Mutex::new(Governor::Ondemand)); + let overrides = Arc::new(Mutex::new(HashMap::new())); + + // Set a per-CPU override. + overrides.lock().unwrap().insert(2, Governor::Performance); + assert_eq!( + effective_governor(*governor.lock().unwrap(), &overrides.lock().unwrap(), 2), + Governor::Performance + ); + + // Simulate a write of "powersave\n" to /scheme/cpufreq/governor: + // update the global default and clear every override. + { + let mut g = governor.lock().unwrap(); + *g = Governor::from_name("powersave").unwrap(); + overrides.lock().unwrap().clear(); + } + + // Every CPU now reports the global default. + for id in 0..8 { + assert_eq!( + effective_governor(*governor.lock().unwrap(), &overrides.lock().unwrap(), id), + Governor::Powersave + ); + } + } + + /// Full governor-switch integration test: write "ondemand\n" to the + /// shared governor, then read it back through the same Arc + /// the scheme server would use. + #[test] + fn governor_switch_integration() { + let governor = Arc::new(Mutex::new(Governor::Performance)); + let overrides = Arc::new(Mutex::new(HashMap::new())); + let cpus = Arc::new(Mutex::new(vec![mock_cpu(0), mock_cpu(1)])); + + let shared = CpufreqShared { + governor: Arc::clone(&governor), + overrides: Arc::clone(&overrides), + cpus, + }; + + // Initial governor read. + let initial = shared.governor.lock().unwrap().as_str().to_string(); + assert_eq!(initial, "performance"); + + // Write "ondemand\n" (with trailing newline, like thermald sends). + let payload = b"ondemand\n"; + let name = std::str::from_utf8(payload).unwrap().trim().to_ascii_lowercase(); + let new_governor = Governor::from_name(&name).expect("valid governor"); + *shared.governor.lock().unwrap() = new_governor; + shared.overrides.lock().unwrap().clear(); + + // Read back — must reflect the switch. + let after = shared.governor.lock().unwrap().as_str().to_string(); + assert_eq!(after, "ondemand", "governor switch must be observable on readback"); + + // Per-CPU effective governor follows the new global default. + assert_eq!( + effective_governor( + *shared.governor.lock().unwrap(), + &shared.overrides.lock().unwrap(), + 0 + ), + Governor::Ondemand + ); + } + + // --- Frequency helpers --- + + #[test] + fn freq_helpers_from_cpuinfo() { + let cpu = mock_cpu(0); + assert_eq!(cpuinfo_max_freq(&cpu), 2400); + assert_eq!(cpuinfo_min_freq(&cpu), 1200); + assert_eq!(current_freq(&cpu), 2400); // current_idx = 0 -> highest + } + + #[test] + fn current_freq_tracks_index() { + let mut cpu = mock_cpu(0); + cpu.current_idx = 3; + assert_eq!(current_freq(&cpu), 1200); + cpu.current_idx = 2; + assert_eq!(current_freq(&cpu), 1600); + } + + #[test] + fn current_freq_empty_pstates_is_zero() { + let mut cpu = mock_cpu(0); + cpu.pstates.clear(); + assert_eq!(current_freq(&cpu), 0); + } + + // --- format_scheme_state backward compatibility --- + + #[test] + fn format_state_contains_governor_line() { + let cpus = vec![mock_cpu(0)]; + let s = format_scheme_state(Governor::Ondemand, &cpus); + let gov_line = s + .lines() + .next() + .expect("state has at least one line"); + assert_eq!(gov_line, "governor=ondemand"); + } + + #[test] + fn format_state_includes_per_cpu_lines() { + let cpus = vec![mock_cpu(0), mock_cpu(1)]; + let s = format_scheme_state(Governor::Performance, &cpus); + assert!(s.contains("CPU0 [legacy]: 2400 kHz")); + assert!(s.contains("CPU1 [legacy]: 2400 kHz")); + } +}