From dde836a316f9400d9d4970ad3c29d4e9e9fc92f4 Mon Sep 17 00:00:00 2001 From: Thanos Makatos Date: Tue, 28 Jul 2026 13:35:35 +0000 Subject: [PATCH] Add experimental PSI monitoring procfs.rs gains PsiSample and read_psi_sample: parses avg10 values from /proc/pressure/{cpu,io,memory}. Engine samples once per tick when experimental_psi_monitoring is on (default off), so operators can eyeball whether the tuning correlates with kernel pressure indicators. Signed-off-by: Thanos Makatos --- README.md | 6 ++++++ io-thread-controller.json | 3 ++- src/config.rs | 4 ++++ src/controller.rs | 28 +++++++++++++++++++++++++++- src/engines/mod.rs | 13 +++++++++++++ src/engines/threshold.rs | 3 ++- 6 files changed, 54 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 68cd75e..0954d13 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,12 @@ through `virDomainQemuMonitorCommand`. Its configuration is loaded from `backends.d/qemu.json`. Named IOThreads and virtqueue mappings can be inspected or changed through the backend CLI and D-Bus operations. +## Experimental PSI monitoring + +With `experimental_psi_monitoring` enabled, each tick samples the CPU, I/O, and +memory `some.avg10` values from `/proc/pressure` and exposes them to the active +scaling engine. Sampling is disabled by default. + ## Status line Every tick the daemon emits one INFO line per tracked VM and one aggregate line diff --git a/io-thread-controller.json b/io-thread-controller.json index 4442ce3..2e4fbc2 100644 --- a/io-thread-controller.json +++ b/io-thread-controller.json @@ -11,5 +11,6 @@ "backend_config_dir": "/etc/io-thread-controller.d/backends", "vm_state_path": "/run/io-thread-controller/vm-ownership.json", "dry_run": false, - "max_instances_adjusted_per_poll": 0 + "max_instances_adjusted_per_poll": 0, + "experimental_psi_monitoring": false } diff --git a/src/config.rs b/src/config.rs index 21db2dd..567e846 100644 --- a/src/config.rs +++ b/src/config.rs @@ -79,6 +79,9 @@ pub struct Config { /// the cap. #[serde(default = "default_max_instances_adjusted_per_poll")] pub max_instances_adjusted_per_poll: u32, + /// every tick and exposing it to the active engine. + #[serde(default)] + pub experimental_psi_monitoring: bool, /// When true, log the scaling verdict but skip the actuation /// call to `set_thread_count`. #[serde(default)] @@ -140,6 +143,7 @@ impl Default for Config { enable_aggregate_status_line: true, print_status_header: false, max_instances_adjusted_per_poll: default_max_instances_adjusted_per_poll(), + experimental_psi_monitoring: false, dry_run: false, } } diff --git a/src/controller.rs b/src/controller.rs index 1381922..6ecaffa 100644 --- a/src/controller.rs +++ b/src/controller.rs @@ -21,7 +21,9 @@ use crate::{ backends::{BackendClientError, IoThreadProperties, VqMapping}, config::Config, dbus::DbusRequest, - engines::{AppliedOutcome, BlockedReason, EngineTickContext, ScaleAction, ScalingEngine}, + engines::{ + AppliedOutcome, BlockedReason, EngineTickContext, PsiSample, ScaleAction, ScalingEngine, + }, instance::{Instance, InstanceStatus}, rolling::format_1_5_15, state::{StateError, VmOwnership, VmStateStore}, @@ -199,6 +201,8 @@ pub struct Controller { last_host_cpu: Option, /// Fraction of host CPU consumed since the previous tick. host_cpu_util: f64, + /// Latest host pressure-stall snapshot. + psi: PsiSample, } impl Controller { @@ -215,6 +219,7 @@ impl Controller { tick_index: 0, last_host_cpu: None, host_cpu_util: 0.0, + psi: PsiSample::default(), }) } @@ -475,11 +480,15 @@ impl Controller { } self.last_host_cpu = Some(current); } + if self.cfg.experimental_psi_monitoring { + self.psi = read_psi_sample(); + } let context = EngineTickContext { now: Instant::now(), min_thread_count: self.cfg.min_thread_count, max_thread_count: self.cfg.max_thread_count, host_cpu_util: self.host_cpu_util, + psi: self.psi, tick_index: self.tick_index, }; let fleet: Vec<_> = self.instances.values().cloned().collect(); @@ -822,6 +831,23 @@ impl Controller { } } +/// Read CPU, I/O, and memory pressure; unavailable values become zero. +fn read_psi_sample() -> PsiSample { + use procfs::{CpuPressure, Current, IoPressure, MemoryPressure}; + + PsiSample { + cpu_some_avg10: CpuPressure::current() + .map(|pressure| pressure.some.avg10 as f64) + .unwrap_or(0.0), + io_some_avg10: IoPressure::current() + .map(|pressure| pressure.some.avg10 as f64) + .unwrap_or(0.0), + memory_some_avg10: MemoryPressure::current() + .map(|pressure| pressure.some.avg10 as f64) + .unwrap_or(0.0), + } +} + /// Read aggregate host CPU counters. fn read_host_cpu_sample() -> Result { let total = procfs::KernelStats::current()?.total; diff --git a/src/engines/mod.rs b/src/engines/mod.rs index e96fac2..08f8976 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -38,6 +38,17 @@ pub enum EngineError { NoSuchEngine(String), } +/// Host pressure-stall information sampled from `/proc/pressure`. +#[derive(Debug, Default, Clone, Copy)] +pub struct PsiSample { + /// CPU `some.avg10` percentage. + pub cpu_some_avg10: f64, + /// I/O `some.avg10` percentage. + pub io_some_avg10: f64, + /// Memory `some.avg10` percentage. + pub memory_some_avg10: f64, +} + /// Scaling operation selected by an engine. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] pub enum ScaleAction { @@ -88,6 +99,8 @@ pub struct EngineTickContext { /// Host-wide CPU utilisation (0.0-1.0) from successive `/proc/stat` /// samples; zero until two samples exist. pub host_cpu_util: f64, + /// Latest host pressure-stall snapshot. + pub psi: PsiSample, /// Monotonically increasing tick sequence number. pub tick_index: u64, } diff --git a/src/engines/threshold.rs b/src/engines/threshold.rs index 24dbcd5..8941360 100644 --- a/src/engines/threshold.rs +++ b/src/engines/threshold.rs @@ -498,7 +498,7 @@ mod tests { use super::{ThresholdConfig, ThresholdEngine, log_performance_revert}; use crate::{ backends::BackendClientError, - engines::{AppliedOutcome, EngineTickContext, ScaleAction, ScalingEngine}, + engines::{AppliedOutcome, EngineTickContext, PsiSample, ScaleAction, ScalingEngine}, instance::{ Instance, InstanceClient, InstancePerfSample, InstanceStatus, ThreadPoolSnapshot, }, @@ -561,6 +561,7 @@ mod tests { min_thread_count: 2, max_thread_count: 8, host_cpu_util: 0.0, + psi: PsiSample::default(), tick_index: 0, } }