1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct Load {
18 pub avg1_milli: u32,
20 pub cores: u32,
22}
23
24impl Load {
25 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 pub fn avg1_text(self) -> String {
36 format!(
37 "{}.{}",
38 self.avg1_milli / 1000,
39 (self.avg1_milli % 1000) / 100
40 )
41 }
42}
43
44pub fn factor_text(milli: u32) -> String {
46 format!("×{}.{}", milli / 1000, (milli % 1000) / 100)
47}
48
49pub 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
61pub 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 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 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 #[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 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 #[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}