clawft_kernel/
mesh_load.rs1use serde::{Deserialize, Serialize};
11use serde_json::Value;
12
13const MAX_LOAD: f64 = 100_000.0;
15const MAX_CORES: u64 = 65_536;
17
18#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
20pub struct LoadSample {
21 pub load1: f64,
23 pub load5: f64,
25 #[serde(default, skip_serializing_if = "Option::is_none")]
27 pub cores: Option<u32>,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub mem_avail: Option<u64>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub mem_total: Option<u64>,
34}
35
36impl LoadSample {
37 pub fn local() -> Option<Self> {
39 let (load1, load5) = load_avg()?;
40 let (mem_avail, mem_total) = memory();
41 Some(Self {
42 load1,
43 load5,
44 cores: std::thread::available_parallelism().ok().and_then(|n| u32::try_from(n.get()).ok()),
45 mem_avail,
46 mem_total,
47 })
48 }
49
50 pub fn from_json(v: &Value) -> Option<Self> {
52 let load = |k: &str| v.get(k)?.as_f64().filter(|x| x.is_finite() && (0.0..=MAX_LOAD).contains(x));
53 let s = Self {
54 load1: load("load1")?,
55 load5: load("load5")?,
56 cores: match v.get("cores") {
57 None | Some(Value::Null) => None,
58 Some(c) => Some(c.as_u64().filter(|c| (1..=MAX_CORES).contains(c))? as u32),
59 },
60 mem_avail: v.get("mem_avail").and_then(Value::as_u64),
61 mem_total: v.get("mem_total").and_then(Value::as_u64),
62 };
63 match (s.mem_avail, s.mem_total) {
65 (Some(a), Some(t)) if a > t => None,
66 _ => Some(s),
67 }
68 }
69
70 pub fn to_json(&self) -> Value {
72 serde_json::to_value(self).unwrap_or(Value::Null)
73 }
74}
75
76#[cfg(unix)]
77fn load_avg() -> Option<(f64, f64)> {
78 let mut l = [0f64; 3];
79 let n = unsafe { libc::getloadavg(l.as_mut_ptr(), 3) };
81 (n >= 2).then_some((l[0], l[1]))
82}
83#[cfg(not(unix))]
84fn load_avg() -> Option<(f64, f64)> {
85 None
86}
87
88#[cfg(target_os = "linux")]
89fn memory() -> (Option<u64>, Option<u64>) {
90 let text = std::fs::read_to_string("/proc/meminfo").unwrap_or_default();
91 let kb = |key: &str| {
92 text.lines()
93 .find_map(|l| l.strip_prefix(key)?.strip_prefix(':'))
94 .and_then(|r| r.split_whitespace().next()?.parse::<u64>().ok())
95 .map(|k| k.saturating_mul(1024))
96 };
97 (kb("MemAvailable"), kb("MemTotal"))
98}
99#[cfg(not(target_os = "linux"))]
100fn memory() -> (Option<u64>, Option<u64>) {
101 (None, None)
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use serde_json::json;
108
109 #[test]
110 fn a_sample_round_trips_through_its_json() {
111 let s = LoadSample { load1: 0.5, load5: 1.25, cores: Some(4), mem_avail: Some(1), mem_total: Some(2) };
112 assert_eq!(LoadSample::from_json(&s.to_json()), Some(s));
113 let bare = LoadSample { load1: 0.0, load5: 0.0, cores: None, mem_avail: None, mem_total: None };
114 assert_eq!(bare.to_json(), json!({ "load1": 0.0, "load5": 0.0 }));
115 assert_eq!(LoadSample::from_json(&bare.to_json()), Some(bare));
116 }
117
118 #[test]
119 fn out_of_range_values_are_refused_not_clamped() {
120 for bad in [
121 json!({ "load1": -1.0, "load5": 0.1 }),
122 json!({ "load1": 1e9, "load5": 0.1 }),
123 json!({ "load1": "1", "load5": 0.1 }),
124 json!({ "load5": 0.1 }),
125 json!({ "load1": 0.1, "load5": 0.1, "cores": 0 }),
126 json!({ "load1": 0.1, "load5": 0.1, "mem_avail": 9, "mem_total": 1 }),
127 json!(null),
128 ] {
129 assert_eq!(LoadSample::from_json(&bad), None, "{bad}");
130 }
131 }
132
133 #[cfg(unix)]
134 #[test]
135 fn this_host_reports_a_load() {
136 let s = LoadSample::local().expect("unix has getloadavg");
137 assert!(s.load1 >= 0.0 && s.cores.unwrap_or(1) >= 1);
138 }
139}