use crate::platform::{SystemMonitor, SystemState};
use std::time::{Duration, Instant};
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct GateConfig {
pub max_cpu_load: f64,
pub min_available_ram_bytes: u64,
pub max_cpu_temp_c: Option<f64>,
pub max_heavy_processes: usize,
pub max_wait: Duration,
pub poll_interval: Duration,
pub strict: bool,
pub max_wait_count: usize,
pub enabled: bool,
}
impl Default for GateConfig {
fn default() -> Self {
Self {
max_cpu_load: 0.20,
min_available_ram_bytes: 512 * 1024 * 1024,
max_cpu_temp_c: Some(85.0),
max_heavy_processes: 1, max_wait: Duration::from_secs(30),
poll_interval: Duration::from_millis(500),
strict: false,
max_wait_count: 10,
enabled: true,
}
}
}
impl GateConfig {
pub fn ci() -> Self {
Self {
max_cpu_load: 0.50,
min_available_ram_bytes: 256 * 1024 * 1024,
max_cpu_temp_c: None, max_heavy_processes: 5,
max_wait: Duration::from_secs(30),
strict: false,
max_wait_count: 20,
..Default::default()
}
}
pub fn strict() -> Self {
Self {
max_cpu_load: 0.05,
max_heavy_processes: 0,
strict: true,
max_wait_count: 5,
..Default::default()
}
}
pub fn disabled() -> Self {
Self {
enabled: false,
..Default::default()
}
}
pub fn max_cpu_load(mut self, load: f64) -> Self {
self.max_cpu_load = load;
self
}
pub fn min_available_ram_mb(mut self, mb: u64) -> Self {
self.min_available_ram_bytes = mb * 1024 * 1024;
self
}
pub fn max_cpu_temp_c(mut self, temp: Option<f64>) -> Self {
self.max_cpu_temp_c = temp;
self
}
pub fn max_heavy_processes(mut self, count: usize) -> Self {
self.max_heavy_processes = count;
self
}
pub fn max_wait(mut self, dur: Duration) -> Self {
self.max_wait = dur;
self
}
}
#[derive(Debug, Clone)]
pub enum GateReason {
CpuLoad(f64),
LowRam(u64),
CpuTemp(f64),
HeavyProcesses(usize),
}
impl std::fmt::Display for GateReason {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GateReason::CpuLoad(load) => write!(f, "CPU load {:.0}%", load * 100.0),
GateReason::LowRam(bytes) => write!(f, "available RAM {}MB", bytes / 1024 / 1024),
GateReason::CpuTemp(temp) => write!(f, "CPU temp {:.0}°C", temp),
GateReason::HeavyProcesses(n) => write!(f, "{} heavy process(es)", n),
}
}
}
pub struct ResourceGate {
config: GateConfig,
monitor: SystemMonitor,
total_waits: usize,
total_wait_time: Duration,
}
impl ResourceGate {
pub fn new(config: GateConfig) -> Self {
Self {
monitor: SystemMonitor::new(),
config,
total_waits: 0,
total_wait_time: Duration::ZERO,
}
}
#[allow(dead_code)] pub fn check(&self) -> Option<GateReason> {
if !self.config.enabled {
return None;
}
let state = self.monitor.snapshot();
self.check_state(&state)
}
fn check_state(&self, state: &SystemState) -> Option<GateReason> {
if state.cpu_load > self.config.max_cpu_load {
return Some(GateReason::CpuLoad(state.cpu_load));
}
if state.available_ram_bytes < self.config.min_available_ram_bytes {
return Some(GateReason::LowRam(state.available_ram_bytes));
}
if let (Some(max_temp), Some(current_temp)) = (self.config.max_cpu_temp_c, state.cpu_temp_c)
{
if current_temp > max_temp {
return Some(GateReason::CpuTemp(current_temp));
}
}
let effective_max_heavy = self.config.max_heavy_processes;
if state.heavy_process_count > effective_max_heavy {
return Some(GateReason::HeavyProcesses(state.heavy_process_count));
}
None
}
#[allow(dead_code)] pub fn wait_for_clear(&mut self) -> bool {
self.wait_for_clear_with_deadline(None)
}
#[allow(dead_code)] pub fn wait_for_clear_with_deadline(&mut self, deadline: Option<Duration>) -> bool {
if !self.config.enabled {
return true;
}
let effective_max = match deadline {
Some(dl) => self.config.max_wait.min(dl),
None => self.config.max_wait,
};
let start = Instant::now();
let mut last_status = Instant::now() - Duration::from_secs(10); loop {
let state = self.monitor.snapshot();
match self.check_state(&state) {
None => {
crate::report::clear_status();
return true;
}
Some(reason) => {
if start.elapsed() >= effective_max {
crate::report::clear_status();
return false;
}
if last_status.elapsed() >= Duration::from_secs(5) {
let elapsed = start.elapsed().as_secs_f64();
let max = effective_max.as_secs_f64();
crate::report::status(&format!(
"[zenbench] waiting ({elapsed:.0}s/{max:.0}s): {reason}"
));
last_status = Instant::now();
}
self.total_waits += 1;
std::thread::sleep(self.config.poll_interval);
self.total_wait_time += self.config.poll_interval;
}
}
}
}
pub fn wait_for_no_benchmarks(&mut self) {
if !self.config.enabled {
return;
}
let our_pid = sysinfo::get_current_pid().ok();
let mut excluded_pids: Vec<sysinfo::Pid> = Vec::new();
if let Some(our) = our_pid {
excluded_pids.push(our);
}
if let Ok(pids_str) = std::env::var("ZENBENCH_LAUNCHER_PIDS") {
for s in pids_str.split(',') {
if let Ok(pid) = s.trim().parse::<usize>() {
excluded_pids.push(sysinfo::Pid::from(pid));
}
}
}
let start = Instant::now();
let max_wait = Duration::from_secs(30);
let mut warned = false;
loop {
let mut sys = sysinfo::System::new();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let mut scan_excluded = excluded_pids.clone();
collect_ancestors(&sys, our_pid, &mut scan_excluded);
let bench_count = sys
.processes()
.values()
.filter(|p| {
if scan_excluded.contains(&p.pid()) {
return false;
}
process_is_benchmark(p)
})
.count();
if bench_count == 0 {
if warned {
crate::report::clear_status();
}
return;
}
if start.elapsed() >= max_wait {
crate::report::clear_status();
return; }
if !warned {
crate::report::status(&format!(
"[zenbench] waiting for {bench_count} other benchmark process(es) to finish..."
));
warned = true;
}
std::thread::sleep(Duration::from_secs(1));
self.total_waits += 1;
self.total_wait_time += Duration::from_secs(1);
}
}
pub fn check_and_record(&mut self) {
if !self.config.enabled {
return;
}
let state = self.monitor.snapshot();
if self.check_state(&state).is_some() {
self.total_waits += 1;
}
}
#[allow(dead_code)]
pub fn brief_wait(&mut self, max_wait: Duration) {
if !self.config.enabled {
return;
}
let start = Instant::now();
loop {
let state = self.monitor.snapshot();
if self.check_state(&state).is_none() {
crate::report::clear_status();
return; }
if start.elapsed() >= max_wait {
crate::report::clear_status();
self.total_waits += 1;
self.total_wait_time += start.elapsed();
return; }
if self.total_waits == 0 {
if let Some(reason) = self.check_state(&state) {
crate::report::status(&format!(
"[zenbench] system busy ({reason}), waiting up to {:.0}s...",
max_wait.as_secs_f64(),
));
}
}
std::thread::sleep(self.config.poll_interval);
}
}
#[allow(dead_code)] pub fn is_unreliable(&self) -> bool {
self.config.strict && self.total_waits > self.config.max_wait_count
}
pub fn total_waits(&self) -> usize {
self.total_waits
}
pub fn total_wait_time(&self) -> Duration {
self.total_wait_time
}
}
#[cfg(test)]
fn parse_launcher_pids(val: &str) -> Vec<sysinfo::Pid> {
val.split(',')
.filter_map(|s| s.trim().parse::<usize>().ok().map(sysinfo::Pid::from))
.collect()
}
const BENCH_NAME_PATTERNS: &[&str] = &["criterion", "divan", "zenbench", "cargo-bench", "bench-"];
fn basename_lower(s: &str) -> String {
s.rsplit(['/', '\\']).next().unwrap_or(s).to_lowercase()
}
fn process_is_benchmark(p: &sysinfo::Process) -> bool {
let name = p.name().to_string_lossy().to_lowercase();
if BENCH_NAME_PATTERNS.iter().any(|&pat| name.contains(pat)) {
return true;
}
if let Some(argv0) = p.cmd().first() {
let base = basename_lower(&argv0.to_string_lossy());
if BENCH_NAME_PATTERNS.iter().any(|&pat| base.contains(pat)) {
return true;
}
}
false
}
fn collect_ancestors(
sys: &sysinfo::System,
start: Option<sysinfo::Pid>,
out: &mut Vec<sysinfo::Pid>,
) {
let mut cur = start;
for _ in 0..1024 {
let Some(pid) = cur else { return };
let Some(proc_) = sys.process(pid) else {
return;
};
let Some(parent) = proc_.parent() else {
return;
};
if out.contains(&parent) {
return;
}
out.push(parent);
cur = Some(parent);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_launcher_pids_single() {
let pids = parse_launcher_pids("12345");
assert_eq!(pids.len(), 1);
assert_eq!(pids[0], sysinfo::Pid::from(12345));
}
#[test]
fn parse_launcher_pids_multiple() {
let pids = parse_launcher_pids("100,200,300");
assert_eq!(pids.len(), 3);
assert_eq!(pids[0], sysinfo::Pid::from(100));
assert_eq!(pids[1], sysinfo::Pid::from(200));
assert_eq!(pids[2], sysinfo::Pid::from(300));
}
#[test]
fn parse_launcher_pids_with_whitespace() {
let pids = parse_launcher_pids(" 100 , 200 , 300 ");
assert_eq!(pids.len(), 3);
}
#[test]
fn parse_launcher_pids_empty() {
let pids = parse_launcher_pids("");
assert!(pids.is_empty());
}
#[test]
fn parse_launcher_pids_ignores_invalid() {
let pids = parse_launcher_pids("123,not_a_pid,456");
assert_eq!(pids.len(), 2);
assert_eq!(pids[0], sysinfo::Pid::from(123));
assert_eq!(pids[1], sysinfo::Pid::from(456));
}
#[test]
fn parse_launcher_pids_chained() {
let pids = parse_launcher_pids("1000,2000");
assert_eq!(pids.len(), 2);
}
#[test]
fn gate_disabled_skips_benchmark_check() {
let mut gate = ResourceGate::new(GateConfig::disabled());
gate.wait_for_no_benchmarks();
assert_eq!(gate.total_waits(), 0);
}
#[test]
fn gate_config_defaults_are_sane() {
let config = GateConfig::default();
assert!(config.enabled);
assert!(config.max_cpu_load > 0.0 && config.max_cpu_load < 1.0);
assert!(config.min_available_ram_bytes > 0);
assert!(config.max_wait > Duration::ZERO);
assert!(config.poll_interval > Duration::ZERO);
}
#[test]
fn gate_config_ci_is_more_permissive() {
let default = GateConfig::default();
let ci = GateConfig::ci();
assert!(ci.max_cpu_load >= default.max_cpu_load);
assert!(ci.max_heavy_processes >= default.max_heavy_processes);
}
#[test]
fn collect_ancestors_includes_our_parent() {
let mut sys = sysinfo::System::new();
sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
let our_pid = sysinfo::get_current_pid().ok();
let mut ancestors = Vec::new();
collect_ancestors(&sys, our_pid, &mut ancestors);
if let Some(pid) = our_pid {
if let Some(parent) = sys.process(pid).and_then(|p| p.parent()) {
assert!(
ancestors.contains(&parent),
"ancestor walk should include our immediate parent {parent:?}"
);
}
}
assert!(
our_pid.is_none_or(|pid| !ancestors.contains(&pid)),
"ancestor walk must not include our own PID"
);
}
#[test]
fn collect_ancestors_none_start_is_empty() {
let sys = sysinfo::System::new();
let mut ancestors = Vec::new();
collect_ancestors(&sys, None, &mut ancestors);
assert!(ancestors.is_empty());
}
#[test]
fn basename_lower_strips_path_and_lowercases() {
assert_eq!(
basename_lower("/home/x/target/release/deps/Decode_Zenbench-abc"),
"decode_zenbench-abc"
);
assert_eq!(basename_lower("cargo-bench"), "cargo-bench");
assert_eq!(basename_lower(r"C:\bin\Foo.exe"), "foo.exe");
assert_eq!(basename_lower(""), "");
}
#[test]
fn bench_name_patterns_match_harness_argv0_not_backup_args() {
let argv0 = basename_lower("/w/target/release/deps/decode_zenbench-9f3");
assert!(BENCH_NAME_PATTERNS.iter().any(|&p| argv0.contains(p)));
let rsync_argv0 = basename_lower("/usr/bin/rsync");
assert!(!BENCH_NAME_PATTERNS.iter().any(|&p| rsync_argv0.contains(p)));
}
}