4ded365124
In read-only mode (detected VM/QEMU/KVM) apply_pstate is a no-op so c.current_idx never advances. The previous log line was emitted whenever the *requested* target (n) differed from current_idx, regardless of whether the write actually fired — on QEMU that produced thousands of P0->P1 lines per boot even though no transition ever took place. Gate the info!() on whether current_idx actually changed. Also skip the dwell accumulation entirely on read-only hosts: writes cannot take effect, so the hysteresis counter is meaningless. The governor still tracks the target so the load value reflects real demand, but no work fires per poll. Closes Phase H of the LG Gram 16 (2025) S/P-state work.
509 lines
22 KiB
Rust
509 lines
22 KiB
Rust
use std::env;
|
|
use std::fs;
|
|
use std::io::{Read, Write};
|
|
use std::time::{Duration, Instant};
|
|
use log::{info, warn, LevelFilter};
|
|
|
|
// MSR addresses — see Intel SDM Vol 3B §14
|
|
const IA32_PERF_CTL: u32 = 0x199; // legacy P-state
|
|
const IA32_PERF_STATUS: u32 = 0x198; // current P-state (read-only)
|
|
const IA32_HWP_REQUEST: u32 = 0x774; // HWP control
|
|
const IA32_HWP_CAPABILITIES: u32 = 0x771; // HWP range
|
|
const IA32_PM_ENABLE: u32 = 0x770; // HWP enable bit
|
|
|
|
// EPP values for IA32_HWP_REQUEST[31:24]
|
|
const EPP_PERFORMANCE: u64 = 0x00;
|
|
const EPP_BALANCE_PERFORMANCE: u64 = 0x80;
|
|
const EPP_BALANCE_POWER: u64 = 0xC0;
|
|
const EPP_POWERSAVE: u64 = 0xFF;
|
|
|
|
// 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
|
|
// 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;
|
|
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 }
|
|
|
|
struct StderrLogger;
|
|
impl log::Log for StderrLogger {
|
|
fn enabled(&self, m: &log::Metadata) -> bool { m.level() <= LevelFilter::Info }
|
|
fn log(&self, r: &log::Record) { eprintln!("[{}] cpufreqd: {}", r.level(), r.args()); }
|
|
fn flush(&self) {}
|
|
}
|
|
|
|
/// HWP = Hardware P-states (Intel Speed Shift).
|
|
/// Arrow Lake-H always has HWP enabled by BIOS. Legacy IA32_PERF_CTL
|
|
/// 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 }
|
|
|
|
#[derive(Clone)]
|
|
struct PState {
|
|
freq_khz: u32,
|
|
power_mw: u32,
|
|
latency_us: u32,
|
|
ctl: u64,
|
|
}
|
|
|
|
/// Minimum dwell time (in polls) at the current P-state before we
|
|
/// allow a transition to a different one. Prevents the
|
|
/// P0->P1->P0 oscillation seen in the Ondemand governor when
|
|
/// the load sits at exactly the threshold (load=0% on idle systems).
|
|
/// With POLL_MS=100ms and DWELL_POLLS=3, the minimum dwell is
|
|
/// 300ms — fast enough for real workloads but slow enough to
|
|
/// stop the threshold-flapping noise.
|
|
const DWELL_POLLS: u32 = 3;
|
|
|
|
#[derive(Clone)]
|
|
struct CpuInfo {
|
|
id: u32,
|
|
pstates: Vec<PState>,
|
|
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]
|
|
hwp_guaranteed: u8, // from MSR 0x771[23:16]
|
|
hwp_efficient: u8, // from MSR 0x771[31:24]
|
|
/// 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,
|
|
/// 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,
|
|
/// 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,
|
|
}
|
|
|
|
fn detect_cpus() -> Vec<u32> {
|
|
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::<u32>() {
|
|
if 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 v.is_empty() { v.push(0); }
|
|
v
|
|
}
|
|
|
|
fn read_msr_u32(cpu: u32, msr: u32) -> Option<u32> {
|
|
let path = format!("/scheme/sys/msr/{}/0x{:x}", cpu, msr);
|
|
let mut f = fs::File::open(&path).ok()?;
|
|
let mut buf = [0u8; 8];
|
|
f.read_exact(&mut buf).ok()?;
|
|
Some(u32::from_le_bytes(buf[..4].try_into().ok()?))
|
|
}
|
|
|
|
fn detect_pstate_mode(cpu: u32) -> PstateMode {
|
|
// IA32_PM_ENABLE bit 0 == HWP_ENABLE
|
|
match read_msr_u32(cpu, IA32_PM_ENABLE) {
|
|
Some(pm) if (pm & 1) != 0 => PstateMode::Hwp,
|
|
_ => PstateMode::LegacyPerfCtl,
|
|
}
|
|
}
|
|
|
|
fn read_hwp_capabilities(cpu: u32) -> (u8, u8, u8, u8) {
|
|
// IA32_HWP_CAPABILITIES layout (Vol 3B §14.4.3):
|
|
// [7:0] Highest Performance
|
|
// [15:8] Guaranteed Performance
|
|
// [23:16] Most Efficient Performance
|
|
// [31:24] Lowest Performance
|
|
let cap = read_msr_u32(cpu, IA32_HWP_CAPABILITIES).unwrap_or(0);
|
|
let max = (cap & 0xFF) as u8;
|
|
let guaranteed = ((cap >> 8) & 0xFF) as u8;
|
|
let efficient = ((cap >> 16) & 0xFF) as u8;
|
|
let min = ((cap >> 24) & 0xFF) as u8;
|
|
(min, max, guaranteed, efficient)
|
|
}
|
|
|
|
fn read_acpi_pss(cpu: u32) -> Vec<PState> {
|
|
let path = format!("/scheme/acpi/processor/CPU{}/pss", cpu);
|
|
if let Ok(d) = fs::read_to_string(&path) {
|
|
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(la), Ok(ct)) =
|
|
(w[0].parse(), w[2].parse(), w[4].parse(), u64::from_str_radix(w[5], 16))
|
|
{ s.push(PState { freq_khz: f, power_mw: pw, latency_us: la, ctl: ct }); }
|
|
}
|
|
}
|
|
if !s.is_empty() { return s; }
|
|
}
|
|
vec![
|
|
PState { freq_khz: 2400, power_mw: 35000, latency_us: 10, ctl: 0x1a00 },
|
|
PState { freq_khz: 2000, power_mw: 25000, latency_us: 10, ctl: 0x1600 },
|
|
PState { freq_khz: 1600, power_mw: 18000, latency_us: 10, ctl: 0x1200 },
|
|
PState { freq_khz: 1200, power_mw: 12000, latency_us: 10, ctl: 0x0e00 },
|
|
]
|
|
}
|
|
|
|
fn write_msr(cpu: u32, msr: u32, val: u64) -> bool {
|
|
let path = format!("/scheme/sys/msr/{}/0x{:x}", cpu, msr);
|
|
fs::OpenOptions::new().write(true).open(&path).ok()
|
|
.map(|mut f| f.write_all(&val.to_ne_bytes()).is_ok()).unwrap_or(false)
|
|
}
|
|
|
|
/// Read the current operating P-state index from IA32_PERF_STATUS
|
|
/// (MSR 0x198). The state occupies bits [3:0] of the 64-bit read.
|
|
/// Returns None if the read fails or the value is reserved (>15).
|
|
///
|
|
/// This is the "readback" that prevents the P0->P1->P0 oscillation
|
|
/// seen on QEMU: the MSR write to IA32_PERF_CTL silently succeeds
|
|
/// (PIIX4 emulation in QEMU) but the CPU never actually changes
|
|
/// state, so reading IA32_PERF_STATUS back returns the unchanged
|
|
/// P0. We use that to detect the no-op and short-circuit the
|
|
/// governor's next transition until something actually changes.
|
|
fn read_current_pstate(cpu: u32) -> Option<u8> {
|
|
let path = format!("/scheme/sys/msr/{}/0x{:x}", cpu, IA32_PERF_STATUS);
|
|
let mut f = fs::File::open(&path).ok()?;
|
|
let mut buf = [0u8; 8];
|
|
f.read_exact(&mut buf).ok()?;
|
|
let val = u64::from_le_bytes(buf);
|
|
let pstate = (val & 0xF) as u8;
|
|
if pstate > 15 { None } else { Some(pstate) }
|
|
}
|
|
|
|
/// Map a P-state index to IA32_HWP_REQUEST value.
|
|
/// IA32_HWP_REQUEST layout (Vol 3B §14.4.4):
|
|
/// [7:0] Minimum Performance
|
|
/// [15:8] Maximum Performance
|
|
/// [23:16] Desired Performance
|
|
/// [31:24] Energy-Performance Preference
|
|
/// [42:32] Activity Window (set to 0 = auto)
|
|
/// [42] Package Control
|
|
fn hwp_request_for(idx: usize, ci: &CpuInfo) -> u64 {
|
|
let m = ci.pstates.len().saturating_sub(1).max(1);
|
|
// Map index 0 (lowest perf, "powersave") to lowest HWP performance
|
|
// and index m (highest perf, "performance") to highest HWP performance.
|
|
let frac = idx as f64 / m as f64;
|
|
let range = ci.hwp_max.saturating_sub(ci.hwp_min) as f64;
|
|
let desired = ci.hwp_min as f64 + range * frac;
|
|
// EPP follows the same map: performance=0, powersave=255
|
|
let epp = ((1.0 - frac) * 255.0) as u64;
|
|
(ci.hwp_min as u64)
|
|
| ((desired as u64) << 8)
|
|
| ((desired as u64) << 16)
|
|
| (epp << 24)
|
|
}
|
|
|
|
fn measure_load(cpu: u32, prev: &mut (u64, u64)) -> f64 {
|
|
if let Ok(d) = fs::read_to_string(format!("/scheme/sys/cpu/{}/stat", cpu)) {
|
|
let p: Vec<u64> = d.split_whitespace().filter_map(|s| s.parse().ok()).collect();
|
|
if p.len() >= 4 {
|
|
let t: u64 = p.iter().sum(); let i = p.get(3).copied().unwrap_or(0);
|
|
let (pt, pi) = *prev; *prev = (t, i);
|
|
if t > pt { let td = t - pt; let id = i.saturating_sub(pi); return 1.0 - (id as f64 / td as f64); }
|
|
}
|
|
}
|
|
0.0
|
|
}
|
|
|
|
fn avg_load(ci: &CpuInfo) -> f64 { ci.load_history.iter().sum::<f64>() / SAMPLE_WINDOW as f64 }
|
|
|
|
fn choose_pstate(g: Governor, ci: &CpuInfo) -> usize {
|
|
if ci.throttle { return (ci.pstates.len().saturating_sub(1)).min(ci.pstates.len()); }
|
|
let l = avg_load(ci); let m = ci.pstates.len().saturating_sub(1); let c = ci.current_idx.min(m);
|
|
match g {
|
|
Governor::Performance => 0,
|
|
Governor::Powersave => m,
|
|
Governor::Ondemand => { if l > 0.8 && c > 0 { c - 1 } else if l < 0.3 && c < m { c + 1 } else { c } }
|
|
Governor::Conservative => { if l > 0.9 && c > 0 { c - 1 } else if l < 0.2 && c < m { c + 1 } else { c } }
|
|
Governor::Schedutil => { let t = ((l * (m + 1) as f64) as usize).min(m); if t < c && c < m { c + 1 } else if t > c && c > 0 { c - 1 } else { c } }
|
|
}
|
|
}
|
|
|
|
fn apply_pstate(ci: &mut CpuInfo, idx: usize) {
|
|
// On a VM host, MSR writes are no-ops on the underlying hardware
|
|
// emulation; we don't bother trying. The governor still tracks
|
|
// the dwell counter, but the target state doesn't actually
|
|
// change. This is what stops the P0->P1->P0 oscillation on QEMU
|
|
// where the dwell counter on bare metal would have the
|
|
// transition actually fire after 3 consecutive polls but on
|
|
// QEMU it would just keep writing silently.
|
|
if ci.read_only {
|
|
return;
|
|
}
|
|
// On real hardware, trust the write: QEMU's PIIX4 emulation
|
|
// does not model IA32_PERF_STATUS (it always returns 0), so a
|
|
// readback is not reliable for state confirmation. The dwell
|
|
// counter in the main loop (DWELL_POLLS consecutive polls at
|
|
// the same target) is the actual hysteresis that prevents
|
|
// oscillation under real load.
|
|
let handle = |ci: &mut CpuInfo, msr: u32, val: u64| -> bool {
|
|
let path = format!("/scheme/sys/msr/{}/0x{:x}", ci.id, msr);
|
|
fs::OpenOptions::new().write(true).open(&path).ok()
|
|
.map(|mut f| f.write_all(&val.to_ne_bytes()).is_ok()).unwrap_or(false)
|
|
};
|
|
match ci.mode {
|
|
PstateMode::Hwp => {
|
|
let val = hwp_request_for(idx, ci);
|
|
if handle(ci, IA32_HWP_REQUEST, val) {
|
|
ci.msr_errors = 0;
|
|
ci.msr_suppressed = false;
|
|
ci.current_idx = idx;
|
|
ci.dwell = 0;
|
|
} else {
|
|
ci.msr_errors += 1;
|
|
if !ci.msr_suppressed {
|
|
warn!("CPU{}: HWP write failed ({}/{})", ci.id, ci.msr_errors, MSR_ERROR_SUPPRESS_COUNT);
|
|
if ci.msr_errors >= MSR_ERROR_SUPPRESS_COUNT { ci.msr_suppressed = true; }
|
|
}
|
|
}
|
|
}
|
|
PstateMode::LegacyPerfCtl => {
|
|
let ct = ci.pstates[idx].ctl;
|
|
if handle(ci, IA32_PERF_CTL, ct) {
|
|
ci.msr_errors = 0;
|
|
ci.msr_suppressed = false;
|
|
ci.current_idx = idx;
|
|
ci.dwell = 0;
|
|
} else {
|
|
ci.msr_errors += 1;
|
|
if !ci.msr_suppressed {
|
|
warn!("CPU{}: MSR write failed ({}/{})", ci.id, ci.msr_errors, MSR_ERROR_SUPPRESS_COUNT);
|
|
if ci.msr_errors >= MSR_ERROR_SUPPRESS_COUNT { ci.msr_suppressed = true; }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct ThermalCache { data: bool, last_check: Instant }
|
|
impl ThermalCache {
|
|
fn new() -> Self { Self { data: false, last_check: Instant::now() - Duration::from_secs(10) } }
|
|
fn get(&mut self) -> bool {
|
|
if self.last_check.elapsed() < Duration::from_millis(THERMAL_CACHE_MS) { return self.data; }
|
|
self.data = check_thermal_raw(); self.last_check = Instant::now(); self.data
|
|
}
|
|
}
|
|
|
|
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.
|
|
//
|
|
// Two signals are checked:
|
|
//
|
|
// 1. DMI strings at the Redox-correct path /scheme/acpi/dmi/<field>
|
|
// (NOT /sys/class/dmi/id/<field> — that is the Linux path).
|
|
// The Redox acpid exposes SMBIOS fields there. On QEMU with
|
|
// OVMF the strings are "QEMU", "KVM", or similar; on real
|
|
// hardware (e.g. LG Gram 2025) they are the OEM strings
|
|
// ("LG Electronics", "16Z90TR"). When SMBIOS is absent (the
|
|
// Redox acpid log line "SMBIOS: no entry point found" can
|
|
// happen on some QEMU configurations), the read returns Err
|
|
// and we fall through to the CPUID check.
|
|
//
|
|
// 2. The CPUID hypervisor-present bit (leaf 1, ECX bit 31) read
|
|
// via inline assembly. This is the canonical x86 VM-detection
|
|
// signal — it works regardless of whether SMBIOS is exposed.
|
|
// On QEMU/KVM/VMware/VirtualBox/Hyper-V/Xen the bit is set;
|
|
// on bare metal it is clear. The pattern mirrors
|
|
// redbear-power/src/cpuid.rs:168 which already uses this bit.
|
|
//
|
|
// When either signal says "virtualized", cpufreqd is constructed
|
|
// with read_only = true and apply_pstate short-circuits at the
|
|
// top: load is still tracked, governor choice is still logged,
|
|
// but no MSR writes fire. On real hardware the writes go to the
|
|
// kernel scheme (/scheme/sys/msr/{cpu}/0x{msr_hex}) and the
|
|
// governor progresses through P-states normally.
|
|
if let Ok(s) = fs::read_to_string("/scheme/acpi/dmi/sys_vendor") {
|
|
let s = s.to_ascii_lowercase();
|
|
if s.contains("qemu") || s.contains("kvm") || s.contains("vmware")
|
|
|| s.contains("virtualbox") || s.contains("hyper-v") || s.contains("xen")
|
|
|| s.contains("microsoft corporation") || s.contains("amazon")
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
if let Ok(s) = fs::read_to_string("/scheme/acpi/dmi/product_name") {
|
|
let s = s.to_ascii_lowercase();
|
|
if s.contains("virtual") || s.contains("kvm") || s.contains("qemu") {
|
|
return true;
|
|
}
|
|
}
|
|
// SMBIOS absent or uninformative: fall back to the CPUID
|
|
// hypervisor-present bit. Inline assembly because the Redox
|
|
// kernel does not expose raw CPUID leaves via a scheme.
|
|
if cpuid_hypervisor_bit() {
|
|
return true;
|
|
}
|
|
// No SMBIOS, no CPUID bit: assume real hardware. cpufreqd's
|
|
// P-state writes through /scheme/sys/msr/{cpu}/0x{msr_hex}
|
|
// are meaningful.
|
|
false
|
|
}
|
|
|
|
#[cfg(target_arch = "x86_64")]
|
|
fn cpuid_hypervisor_bit() -> bool {
|
|
// CPUID leaf 1, subleaf 0, ECX bit 31 — the standard x86
|
|
// hypervisor-present bit. Returns true when running under
|
|
// any hypervisor that advertises itself (QEMU/KVM, VMware,
|
|
// VirtualBox, Hyper-V, Xen, bhyve, etc.).
|
|
let cpuid = unsafe { core::arch::x86_64::__cpuid_count(1, 0) };
|
|
(cpuid.ecx & (1 << 31)) != 0
|
|
}
|
|
|
|
#[cfg(not(target_arch = "x86_64"))]
|
|
fn cpuid_hypervisor_bit() -> bool {
|
|
false
|
|
}
|
|
|
|
fn main() {
|
|
log::set_logger(&StderrLogger).ok();
|
|
log::set_max_level(LevelFilter::Info);
|
|
|
|
let virtualized = detect_virtualization();
|
|
if virtualized {
|
|
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() {
|
|
"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<CpuInfo> = cpus.iter().map(|&id| {
|
|
let mode = detect_pstate_mode(id);
|
|
let (hwp_min, hwp_max, hwp_guaranteed, hwp_efficient) = if mode == PstateMode::Hwp {
|
|
read_hwp_capabilities(id)
|
|
} else {
|
|
(0, 0, 0, 0)
|
|
};
|
|
if mode == PstateMode::Hwp {
|
|
info!("CPU{}: HWP active (range {}-{}, EPP cap {}-{})", id, hwp_min, hwp_max, hwp_efficient, hwp_guaranteed);
|
|
} else {
|
|
info!("CPU{}: legacy P-states (HWP not enabled)", id);
|
|
}
|
|
let ps = read_acpi_pss(id);
|
|
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, hwp_guaranteed, hwp_efficient, dwell: 0, dwell_target: 0, read_only: virtualized }
|
|
}).collect();
|
|
let mut prev: Vec<(u64, u64)> = vec![(0, 0); 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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
loop {
|
|
std::thread::sleep(Duration::from_millis(POLL_MS));
|
|
let tt = thermal.get();
|
|
for (i, c) in ci.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);
|
|
// 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
|
|
// real hardware with stable 0% load) where the governor
|
|
// would otherwise toggle the state every poll cycle.
|
|
// On read-only hosts (detected VM/QEMU) writes cannot
|
|
// take effect, so there is no point accumulating dwell
|
|
// — skip the check entirely.
|
|
if !c.read_only && n != c.current_idx {
|
|
if n == c.dwell_target {
|
|
c.dwell = c.dwell.saturating_add(1);
|
|
} else {
|
|
c.dwell_target = n;
|
|
c.dwell = 1;
|
|
}
|
|
if c.dwell < DWELL_POLLS {
|
|
continue; // not enough polls at this target yet
|
|
}
|
|
} else {
|
|
// Same state as last poll (or read-only host): reset
|
|
// dwell counter (no transition was requested so dwell
|
|
// stays at 0).
|
|
c.dwell = 0;
|
|
}
|
|
if n != c.current_idx && n < c.pstates.len() {
|
|
let prev_idx = c.current_idx;
|
|
let prev_freq = c.pstates[c.current_idx].freq_khz;
|
|
let next_freq = c.pstates[n].freq_khz;
|
|
let l_pct = l * 100.0;
|
|
match c.mode {
|
|
PstateMode::Hwp => {
|
|
apply_pstate(c, n);
|
|
if c.current_idx != prev_idx {
|
|
info!("CPU{} HWP→{}% ({}→{} kHz, load={:.0}%)", c.id, c.hwp_max.saturating_sub(n as u8 * (c.hwp_max - c.hwp_min) / c.pstates.len().saturating_sub(1).max(1) as u8), prev_freq, next_freq, l_pct);
|
|
}
|
|
}
|
|
PstateMode::LegacyPerfCtl => {
|
|
apply_pstate(c, n);
|
|
if c.current_idx != prev_idx {
|
|
info!("CPU{}: P{}→P{} ({}→{} kHz, load={:.0}%)", c.id, prev_idx, n, prev_freq, next_freq, l_pct);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if last_state_write.elapsed() >= Duration::from_secs(STATE_WRITE_INTERVAL_S) { write_scheme_state(governor, &ci); last_state_write = Instant::now(); }
|
|
}
|
|
}
|