use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LoadAverage {
pub one_minute: f64,
pub five_minute: f64,
pub fifteen_minute: f64,
}
impl fmt::Display for LoadAverage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{:.2} {:.2} {:.2}",
self.one_minute, self.five_minute, self.fifteen_minute
)
}
}
#[derive(Debug, thiserror::Error)]
pub enum LoadAverageError {
#[error("no load-average source on this platform ({0})")]
Unsupported(&'static str),
#[error("load-average read failed: {source} (errno {errno:?})")]
Errno {
#[source]
source: std::io::Error,
errno: Option<i32>,
},
#[error("load-average source {origin} is malformed: {detail}")]
Malformed {
origin: &'static str,
detail: String,
},
}
impl LoadAverageError {
#[must_use]
pub fn errno(&self) -> Option<i32> {
match self {
Self::Errno { errno, .. } => *errno,
_ => None,
}
}
}
pub fn read_load_average() -> Result<LoadAverage, LoadAverageError> {
#[cfg(target_os = "linux")]
{
read_proc_loadavg()
}
#[cfg(all(unix, not(target_os = "linux")))]
{
read_getloadavg()
}
#[cfg(not(unix))]
{
Err(LoadAverageError::Unsupported(std::env::consts::OS))
}
}
#[cfg(all(unix, not(target_os = "linux")))]
fn read_getloadavg() -> Result<LoadAverage, LoadAverageError> {
let mut samples = [0f64; 3];
let filled = unsafe { libc::getloadavg(samples.as_mut_ptr(), 3) };
if filled < 3 {
let source = std::io::Error::last_os_error();
let errno = source.raw_os_error();
return Err(LoadAverageError::Errno { source, errno });
}
Ok(LoadAverage {
one_minute: samples[0],
five_minute: samples[1],
fifteen_minute: samples[2],
})
}
#[cfg(target_os = "linux")]
fn read_proc_loadavg() -> Result<LoadAverage, LoadAverageError> {
let raw = std::fs::read_to_string("/proc/loadavg").map_err(|source| {
let errno = source.raw_os_error();
LoadAverageError::Errno { source, errno }
})?;
parse_proc_loadavg(&raw)
}
pub fn parse_proc_loadavg(raw: &str) -> Result<LoadAverage, LoadAverageError> {
let mut fields = raw.split_whitespace();
let mut next = |which: &str| -> Result<f64, LoadAverageError> {
let field = fields.next().ok_or_else(|| LoadAverageError::Malformed {
origin: "/proc/loadavg",
detail: format!("missing the {which} field"),
})?;
field
.parse::<f64>()
.map_err(|err| LoadAverageError::Malformed {
origin: "/proc/loadavg",
detail: format!("{which} field {field:?} does not parse: {err}"),
})
};
Ok(LoadAverage {
one_minute: next("1-minute")?,
five_minute: next("5-minute")?,
fifteen_minute: next("15-minute")?,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_reading_from_this_host_is_finite_and_non_negative() {
let avg = read_load_average().expect("a unix host exposes a load average");
for (label, value) in [
("1-minute", avg.one_minute),
("5-minute", avg.five_minute),
("15-minute", avg.fifteen_minute),
] {
assert!(
value.is_finite() && value >= 0.0,
"{label} load {value} is not a plausible run-queue length"
);
}
}
#[test]
fn a_well_formed_proc_line_parses() {
let avg = parse_proc_loadavg("0.52 1.25 2.00 3/1234 56789\n")
.expect("a canonical /proc/loadavg line parses");
assert_eq!(avg.one_minute, 0.52);
assert_eq!(avg.five_minute, 1.25);
assert_eq!(avg.fifteen_minute, 2.00);
}
#[test]
fn an_unparsable_proc_line_is_an_error() {
let err = parse_proc_loadavg("0.52 not-a-number 2.00")
.expect_err("a non-numeric field must not be read as a load");
assert!(
matches!(err, LoadAverageError::Malformed { .. }),
"expected Malformed, got {err:?}"
);
assert!(
format!("{err}").contains("5-minute"),
"the message must name the bad field: {err}"
);
}
#[test]
fn a_short_proc_line_is_an_error() {
let err = parse_proc_loadavg("0.52 1.25")
.expect_err("a truncated line must not be padded with a guess");
assert!(
format!("{err}").contains("missing the 15-minute field"),
"{err}"
);
}
#[test]
fn an_errno_is_reported_for_an_io_failure() {
let err = LoadAverageError::Errno {
source: std::io::Error::from_raw_os_error(libc::EPERM),
errno: Some(libc::EPERM),
};
assert_eq!(err.errno(), Some(libc::EPERM));
let malformed = LoadAverageError::Malformed {
origin: "/proc/loadavg",
detail: "x".to_string(),
};
assert_eq!(malformed.errno(), None);
}
#[test]
fn the_display_form_reads_like_uptime() {
let avg = LoadAverage {
one_minute: 21.4,
five_minute: 21.42,
fifteen_minute: 21.671,
};
assert_eq!(format!("{avg}"), "21.40 21.42 21.67");
}
}