Skip to main content

amont_runtime/
load.rs

1//! The host's load, and how far it stretches a silence budget (ADR-0009).
2//!
3//! A machine running several worktrees and agents at once makes every tool
4//! slower, and a tool that is slow because it is waiting for a core is not
5//! stuck. The silence budget is therefore multiplied by how oversubscribed
6//! the host is — the one-minute load average over the core count — capped
7//! by `amont.idleLoadScale`. A false kill costs a parked commit; a slower
8//! verdict on a genuine hang costs minutes.
9//!
10//! The arithmetic is a calculation with no clock or syscall in it; the
11//! reading is one `getloadavg` on Linux and macOS, and a factor of one
12//! everywhere else, which the messages say rather than claim a load they
13//! never measured.
14
15/// What was read from the host, in thousandths so it travels as atomics.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Load {
18    /// The one-minute load average × 1000.
19    pub avg1_milli: u32,
20    /// Logical cores the process may run on.
21    pub cores: u32,
22}
23
24impl Load {
25    /// Oversubscription × 1000: `avg1 / cores`, floored at 1.0 and capped at
26    /// `cap`. A host with headroom stretches nothing.
27    pub fn factor_milli(self, cap: u64) -> u32 {
28        let cores = u64::from(self.cores.max(1));
29        let raw = u64::from(self.avg1_milli) / cores;
30        let capped = raw.clamp(1000, cap.max(1).saturating_mul(1000));
31        u32::try_from(capped).unwrap_or(u32::MAX)
32    }
33
34    /// `31.2`, for a message.
35    pub fn avg1_text(self) -> String {
36        format!(
37            "{}.{}",
38            self.avg1_milli / 1000,
39            (self.avg1_milli % 1000) / 100
40        )
41    }
42}
43
44/// `×3.9`, from a factor in thousandths.
45pub fn factor_text(milli: u32) -> String {
46    format!("×{}.{}", milli / 1000, (milli % 1000) / 100)
47}
48
49/// The silence budget under load: `idle × clamp(load1 / cores, 1, cap)`,
50/// rounded to whole seconds, then no more than the ceiling when there is
51/// one. Pure, so it is tested to the second.
52pub fn scaled_budget(idle: u64, load: Load, cap: u64, ceiling: Option<u64>) -> u64 {
53    let factor = u64::from(load.factor_milli(cap));
54    let scaled = (idle.saturating_mul(factor) + 500) / 1000;
55    match ceiling {
56        Some(c) if c > 0 => scaled.min(c),
57        _ => scaled,
58    }
59}
60
61/// The host right now, where it can be read.
62pub fn read() -> Option<Load> {
63    let avg1 = platform::avg1()?;
64    let cores = std::thread::available_parallelism()
65        .map(|n| u32::try_from(n.get()).unwrap_or(u32::MAX))
66        .unwrap_or(1);
67    Some(Load {
68        avg1_milli: u32::try_from((avg1 * 1000.0).round() as i64).unwrap_or(u32::MAX),
69        cores,
70    })
71}
72
73#[cfg(any(target_os = "linux", target_os = "macos"))]
74mod platform {
75    // libc, which std already links.
76    extern "C" {
77        #[link_name = "getloadavg"]
78        fn getloadavg_raw(loadavg: *mut f64, nelem: i32) -> i32;
79    }
80
81    pub fn avg1() -> Option<f64> {
82        let mut avg = [0f64; 3];
83        // SAFETY: a three-element buffer we own, and we ask for three.
84        let n = unsafe { getloadavg_raw(avg.as_mut_ptr(), 3) };
85        (n >= 1 && avg[0].is_finite() && avg[0] >= 0.0).then_some(avg[0])
86    }
87}
88
89#[cfg(not(any(target_os = "linux", target_os = "macos")))]
90mod platform {
91    pub fn avg1() -> Option<f64> {
92        None
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    fn load(avg1: f64, cores: u32) -> Load {
101        Load {
102            avg1_milli: (avg1 * 1000.0) as u32,
103            cores,
104        }
105    }
106
107    /// Headroom stretches nothing; 3.9 over 8 cores is under one; 31.2 over
108    /// 8 is ×3.9; 40 over 8 is capped at ×4; a cap of 1 disables the
109    /// stretch; the ceiling clamps the result.
110    #[test]
111    fn the_budget_scales_with_oversubscription_up_to_the_cap() {
112        assert_eq!(scaled_budget(120, load(1.0, 8), 4, Some(3600)), 120);
113        assert_eq!(scaled_budget(120, load(3.9, 8), 4, Some(3600)), 120);
114        assert_eq!(scaled_budget(120, load(16.0, 8), 4, Some(3600)), 240);
115        assert_eq!(scaled_budget(120, load(31.2, 8), 4, Some(3600)), 468);
116        assert_eq!(scaled_budget(120, load(40.0, 8), 4, Some(3600)), 480);
117        assert_eq!(scaled_budget(120, load(31.2, 8), 1, Some(3600)), 120);
118        assert_eq!(scaled_budget(120, load(80.0, 8), 16, Some(3600)), 1200);
119        assert_eq!(scaled_budget(120, load(31.2, 8), 4, Some(300)), 300);
120        assert_eq!(scaled_budget(120, load(40.0, 8), 4, None), 480);
121        // Zero cores cannot divide by zero.
122        assert_eq!(scaled_budget(120, load(2.0, 0), 4, None), 240);
123    }
124
125    #[test]
126    fn factors_and_averages_read_as_one_decimal() {
127        assert_eq!(load(31.2, 8).factor_milli(4), 3900);
128        assert_eq!(factor_text(3900), "×3.9");
129        assert_eq!(factor_text(1000), "×1.0");
130        assert_eq!(load(31.25, 8).avg1_text(), "31.2");
131    }
132
133    /// Where the platform reads a load at all, it is a finite non-negative
134    /// number with a core count behind it.
135    #[cfg(any(target_os = "linux", target_os = "macos"))]
136    #[test]
137    fn the_platform_reads_a_sane_load() {
138        let l = read().expect("linux and macos read the load");
139        assert!(l.cores >= 1);
140        assert!(l.avg1_milli < 100_000_000);
141    }
142}