OXYGENHUB

Coverage / crates/flow/src/facts.rs 59.1%

55 of 93 lines run, 10 of 22 functions · run #228 on main · run missed

1
//! What a runner measures about itself (spec §11.1, docs/jetstream.md §6):
2
//! free disk, load and the kernel each claim, cheaply; and its speed, from
3
//! a short fixed benchmark run at start and again while idle.
4
5
use crate::home::Home;
6
use flow_core::api::Facts;
7
use std::time::Instant;
8
9
/// The benchmark's time on the reference machine (Earth), in ms: a runner
10
/// that takes as long scores 100.
11
const REFERENCE_MS: f64 = 225.0;
12
13
/// The cheap facts, with the last speed measured.
140×
pub fn now(home: &Home, speed: Option<u32>) -> Facts {
150×
    Facts {
160×
        free_disk_mib: free_disk_mib(&home.root),
170×
        load_x100: load_x100(),
180×
        speed,
190×
        speed_class: speed.map(Facts::class_of),
200×
        kernel: kernel(),
210×
    }
220×
}
23
24
/// The kernel's release, as uname says. Not read from /proc: an Android
25
/// app may not read it there, and is refused (and logged) every claim.
26
#[cfg(unix)]
271×
pub fn kernel() -> Option<String> {
281×
    let mut u: libc::utsname = unsafe { std::mem::zeroed() };
29
    // SAFETY: `u` is a writable utsname.
301×
    if unsafe { libc::uname(&mut u) } != 0 {
310×
        return None;
321×
    }
33
    // SAFETY: uname writes NUL-terminated strings.
341×
    let release = unsafe { std::ffi::CStr::from_ptr(u.release.as_ptr()) };
351×
    Some(release.to_string_lossy().into_owned())
361×
}
37
38
/// Windows' version (`Windows 10.0.26100`), from `RtlGetVersion`, which
39
/// unlike `GetVersionEx` does not depend on the program's manifest.
40
#[cfg(windows)]
41
pub fn kernel() -> Option<String> {
42
    use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW;
43
    // SAFETY: an all-zero OSVERSIONINFOW is valid; its size is set before the call.
44
    let mut v: OSVERSIONINFOW = unsafe { std::mem::zeroed() };
45
    v.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
46
    // SAFETY: `v` is a writable OSVERSIONINFOW with its size set.
47
    let status = unsafe { windows_sys::Wdk::System::SystemServices::RtlGetVersion(&mut v) };
48
    (status == 0).then(|| format!("Windows {}.{}.{}", v.dwMajorVersion, v.dwMinorVersion, v.dwBuildNumber))
49
}
50
51
/// Free space for an unprivileged writer on the file system holding `path`,
52
/// in MiB.
53
#[cfg(unix)]
541×
pub fn free_disk_mib(path: &std::path::Path) -> Option<u64> {
55
    use std::os::unix::ffi::OsStrExt;
561×
    let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?;
571×
    let mut s: libc::statvfs = unsafe { std::mem::zeroed() };
58
    // SAFETY: `c` is a valid NUL-terminated path and `s` a writable statvfs.
591×
    if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 {
600×
        return None;
611×
    }
621×
    Some((s.f_bavail as u64).saturating_mul(s.f_frsize as u64) >> 20)
631×
}
64
65
#[cfg(windows)]
66
pub fn free_disk_mib(path: &std::path::Path) -> Option<u64> {
67
    use std::os::windows::ffi::OsStrExt;
68
    let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
69
    let mut free: u64 = 0;
70
    // SAFETY: `wide` is a NUL-terminated path and `free` a writable u64;
71
    // the totals it may also fill are not asked for.
72
    let ok = unsafe {
73
        windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(wide.as_ptr(), &mut free, std::ptr::null_mut(), std::ptr::null_mut())
74
    };
75
    (ok != 0).then_some(free >> 20)
76
}
77
78
/// The disk the files under `dir` take (their blocks, not their sizes: a
79
/// job's disks are sparse), in MiB; links are not followed.
801×
pub fn allocated_mib(dir: &std::path::Path) -> Option<u64> {
811×
    let mut total: u64 = 0;
821×
    let mut stack = vec![dir.to_path_buf()];
832×
    while let Some(d) = stack.pop() {
842×
        for e in std::fs::read_dir(&d).ok()?.flatten() {
852×
            let m = std::fs::symlink_metadata(e.path()).ok()?;
862×
            if m.is_dir() {
870×
                stack.push(e.path());
882×
            } else {
892×
                total = total.saturating_add(crate::portable::disk_bytes(&m));
902×
            }
91
        }
92
    }
931×
    Some(total >> 20)
941×
}
95
96
/// A process's peak resident memory (`VmHWM`), in MiB.
9716×
pub fn peak_rss_mib(pid: u32) -> Option<u64> {
9816×
    let text = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?;
99336×
    let kib: u64 = text.lines().find_map(|l| l.strip_prefix("VmHWM:"))?.trim().trim_end_matches("kB").trim().parse().ok()?;
10016×
    Some(kib / 1024)
10116×
}
102
103
/// The 1-minute load average, times 100, from sysinfo (the kernel's own
104
/// numbers behind /proc/loadavg, which an Android app may not read); not
105
/// measured on Windows.
106
#[cfg(not(any(target_os = "linux", target_os = "android")))]
107
pub fn load_x100() -> Option<u32> {
108
    None
109
}
110
111
#[cfg(any(target_os = "linux", target_os = "android"))]
1121×
pub fn load_x100() -> Option<u32> {
1131×
    let mut i: libc::sysinfo = unsafe { std::mem::zeroed() };
114
    // SAFETY: `i` is a writable sysinfo.
1151×
    if unsafe { libc::sysinfo(&mut i) } != 0 {
1160×
        return None;
1171×
    }
118
    // Fixed point with 16 fraction bits (SI_LOAD_SHIFT).
1191×
    Some(((i.loads[0] as f64) / 65536.0 * 100.0).round() as u32)
1201×
}
121
122
/// The benchmark, one thread: BLAKE3 over 256 MiB (spec §11.1), and a sort
123
/// of 4 Mi pseudo-random words for the integer and memory side. Answers
124
/// the score, 100 on the reference machine, and the time it took.
1250×
pub fn bench() -> (u32, u128) {
1260×
    let start = Instant::now();
1270×
    let buf: Vec<u8> = (0..32u32 << 20).map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8).collect();
1280×
    let mut h = blake3::Hasher::new();
1290×
    for _ in 0..8 {
1300×
        h.update(&buf);
1310×
    }
1320×
    std::hint::black_box(h.finalize());
1330×
    drop(buf);
1340×
    let mut x: u64 = 0x9e37_79b9_7f4a_7c15;
1350×
    let mut words: Vec<u64> = (0..4u64 << 20)
1360×
        .map(|_| {
1370×
            x ^= x << 13;
1380×
            x ^= x >> 7;
1390×
            x ^= x << 17;
1400×
            x
1410×
        })
1420×
        .collect();
1430×
    words.sort_unstable();
1440×
    std::hint::black_box(&words);
1450×
    let ms = start.elapsed().as_millis().max(1);
1460×
    ((REFERENCE_MS * 100.0 / ms as f64).round() as u32, ms)
1470×
}
148
149
#[cfg(test)]
150
mod tests {
151
    use super::*;
152
153
    #[test]
1541×
    fn the_cheap_facts_are_read_and_classes_double() {
1551×
        assert!(free_disk_mib(std::path::Path::new("/")).is_some());
156
        // The same numbers /proc gives, read without it (an Android app may not).
1571×
        let proc_load: f64 = std::fs::read_to_string("/proc/loadavg").unwrap().split_whitespace().next().unwrap().parse().unwrap();
1581×
        let load = load_x100().unwrap() as f64 / 100.0;
1591×
        assert!((load - proc_load).abs() < 1.0 + proc_load / 4.0, "sysinfo {load}, /proc {proc_load}");
1601×
        assert_eq!(kernel().unwrap(), std::fs::read_to_string("/proc/sys/kernel/osrelease").unwrap().trim());
1611×
        assert!(peak_rss_mib(std::process::id()).is_some_and(|m| m > 0));
1621×
        let dir = tempfile::tempdir().unwrap();
1631×
        let f = std::fs::File::create(dir.path().join("sparse")).unwrap();
1641×
        f.set_len(1 << 30).unwrap();
1651×
        std::fs::write(dir.path().join("full"), vec![7u8; 3 << 20]).unwrap();
1661×
        let mib = allocated_mib(dir.path()).unwrap();
1671×
        assert!((3..=4).contains(&mib), "the sparse gigabyte takes nothing, the written 3 MiB do: {mib}");
1681×
        assert_eq!(Facts::class_of(100), 3, "the reference machine");
1691×
        assert_eq!((Facts::class_of(50), Facts::class_of(200), Facts::class_of(500), Facts::class_of(10)), (2, 4, 5, 1));
1701×
    }
171
}