use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use crate::daemon::Source;
pub const ENV: &str = "SEMLITH_CPU_CAP";
const WINDOW: Duration = Duration::from_secs(2);
const STEP: Duration = Duration::from_millis(250);
const AIM: f64 = 0.85;
pub fn in_force() -> (u8, Source) {
if let Some(n) = std::env::var(ENV)
.ok()
.and_then(|raw| raw.trim().trim_end_matches('%').parse::<u64>().ok())
{
return (n.min(100) as u8, Source::Environment);
}
match crate::home::Settings::load().cpu_cap_percent {
Some(n) => (n.min(100), Source::Saved),
None => (100, Source::Derived),
}
}
pub fn percent() -> u8 {
static CACHED: Mutex<Option<(Instant, u8)>> = Mutex::new(None);
let mut cached = CACHED.lock().unwrap_or_else(|e| e.into_inner());
match *cached {
Some((at, cap)) if at.elapsed() < Duration::from_millis(500) => cap,
_ => {
let cap = in_force().0;
*cached = Some((Instant::now(), cap));
cap
}
}
}
#[cfg(unix)]
pub fn process_cpu_seconds() -> f64 {
let mut usage: libc::rusage = unsafe { std::mem::zeroed() };
if unsafe { libc::getrusage(libc::RUSAGE_SELF, &mut usage) } != 0 {
return 0.0;
}
let tv = |t: libc::timeval| t.tv_sec as f64 + t.tv_usec as f64 / 1e6;
tv(usage.ru_utime) + tv(usage.ru_stime)
}
#[cfg(windows)]
pub fn process_cpu_seconds() -> f64 {
use windows_sys::Win32::Foundation::FILETIME;
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetProcessTimes};
let zero = || FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let (mut created, mut exited, mut kernel, mut user) = (zero(), zero(), zero(), zero());
let ok = unsafe {
GetProcessTimes(
GetCurrentProcess(),
&mut created,
&mut exited,
&mut kernel,
&mut user,
)
};
if ok == 0 {
return 0.0;
}
let ticks = |t: FILETIME| ((t.dwHighDateTime as u64) << 32 | t.dwLowDateTime as u64) as f64;
(ticks(kernel) + ticks(user)) / 1e7
}
#[cfg(not(any(unix, windows)))]
pub fn process_cpu_seconds() -> f64 {
0.0
}
fn capped_cores() -> Option<usize> {
let cap = percent();
(cap < 100).then(|| ((cores() * f64::from(cap) / 100.0).floor() as usize).max(1))
}
fn prepare_slots() -> usize {
capped_cores().map_or(usize::MAX, |n| (n / 2).max(1))
}
pub fn thread_ceiling() -> usize {
capped_cores().map_or(usize::MAX, |n| n.saturating_sub(prepare_slots()).max(1))
}
static BUSY: Mutex<usize> = Mutex::new(0);
static FREED: std::sync::Condvar = std::sync::Condvar::new();
pub struct Slot(());
impl Drop for Slot {
fn drop(&mut self) {
let mut busy = BUSY.lock().unwrap_or_else(|e| e.into_inner());
*busy = busy.saturating_sub(1);
FREED.notify_one();
}
}
pub fn slot() -> Slot {
pace();
let mut busy = BUSY.lock().unwrap_or_else(|e| e.into_inner());
while *busy >= prepare_slots() {
busy = FREED
.wait_timeout(busy, STEP)
.unwrap_or_else(|e| e.into_inner())
.0;
}
*busy += 1;
Slot(())
}
pub fn within<R: Send>(work: impl FnOnce() -> R + Send) -> R {
static POOL: Mutex<Option<(usize, std::sync::Arc<rayon::ThreadPool>)>> = Mutex::new(None);
let Some(threads) = capped_cores() else {
return work();
};
let pool = {
let mut slot = POOL.lock().unwrap_or_else(|e| e.into_inner());
match slot.as_ref() {
Some((n, pool)) if *n == threads => Some(std::sync::Arc::clone(pool)),
_ => rayon::ThreadPoolBuilder::new()
.num_threads(threads)
.thread_name(|i| format!("semlith-capped-{i}"))
.build()
.ok()
.map(std::sync::Arc::new)
.inspect(|pool| *slot = Some((threads, std::sync::Arc::clone(pool)))),
}
};
match pool {
Some(pool) => pool.install(work),
None => work(),
}
}
fn cores() -> f64 {
std::thread::available_parallelism().map_or(1, |n| n.get()) as f64
}
static SAMPLES: Mutex<VecDeque<(Instant, f64)>> = Mutex::new(VecDeque::new());
fn share(over: Duration) -> Option<(f64, f64)> {
let now = Instant::now();
let cpu = process_cpu_seconds();
let mut samples = SAMPLES.lock().unwrap_or_else(|e| e.into_inner());
if samples
.back()
.is_none_or(|(at, _)| now.duration_since(*at) >= Duration::from_millis(20))
{
samples.push_back((now, cpu));
}
while samples.len() > 2
&& samples
.get(1)
.is_some_and(|(at, _)| now.duration_since(*at) >= WINDOW + Duration::from_secs(1))
{
samples.pop_front();
}
let &(since, from) = samples
.iter()
.rev()
.find(|(at, _)| now.duration_since(*at) >= over)
.or(samples.front())?;
let span = now.duration_since(since).as_secs_f64();
if span < 0.05 {
return None;
}
Some(((cpu - from).max(0.0) / (span * cores()) * 100.0, span))
}
pub fn measured_percent() -> f64 {
static LAST: Mutex<f64> = Mutex::new(0.0);
let mut last = LAST.lock().unwrap_or_else(|e| e.into_inner());
if let Some((percent, _)) = share(Duration::from_secs(1)) {
*last = (percent * 10.0).round() / 10.0;
}
*last
}
static PAUSED: AtomicBool = AtomicBool::new(false);
static PACING: AtomicBool = AtomicBool::new(false);
pub fn paused() -> bool {
PAUSED.load(Ordering::Relaxed)
}
pub fn paused_line() -> Option<String> {
paused().then(|| "paused by the CPU cap (0 %); raise it to resume".to_string())
}
pub fn pace() {
loop {
let cap = percent();
if cap >= 100 {
settle();
return;
}
if cap == 0 {
if !PAUSED.swap(true, Ordering::Relaxed) {
eprintln!("semlith: CPU work paused by the CPU cap (0 %)");
}
std::thread::sleep(STEP);
continue;
}
if PAUSED.swap(false, Ordering::Relaxed) {
eprintln!("semlith: CPU work resumed under a {cap} % CPU cap");
}
let Some((used, span)) = share(WINDOW) else {
return;
};
let target = f64::from(cap) * AIM;
if used <= target {
return;
}
if !PACING.swap(true, Ordering::Relaxed) {
eprintln!("semlith: pacing CPU work to the {cap} % CPU cap ({used:.0} % used)");
}
let need = span * used / target - span;
std::thread::sleep(Duration::from_secs_f64(need.max(0.01)).min(STEP));
}
}
fn settle() {
PAUSED.store(false, Ordering::Relaxed);
PACING.store(false, Ordering::Relaxed);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_environment_wins_and_is_clamped_to_a_hundred() {
crate::home::with_env_var(ENV, "250".as_ref(), || {
assert_eq!(in_force(), (100, Source::Environment));
});
crate::home::with_env_var(ENV, "35%".as_ref(), || {
assert_eq!(in_force(), (35, Source::Environment));
});
}
#[test]
fn process_cpu_time_moves_when_the_process_works() {
let before = process_cpu_seconds();
let started = Instant::now();
let mut x = 0u64;
while started.elapsed() < Duration::from_millis(200) {
x = x.wrapping_mul(6364136223846793005).wrapping_add(1);
}
std::hint::black_box(x);
assert!(process_cpu_seconds() > before);
}
}