//! What a runner measures about itself (spec §11.1, docs/jetstream.md §6): //! free disk, load and the kernel each claim, cheaply; and its speed, from //! a short fixed benchmark run at start and again while idle. use crate::home::Home; use flow_core::api::Facts; use std::time::Instant; /// The benchmark's time on the reference machine (Earth), in ms: a runner /// that takes as long scores 100. const REFERENCE_MS: f64 = 225.0; /// The cheap facts, with the last speed measured. pub fn now(home: &Home, speed: Option) -> Facts { Facts { free_disk_mib: free_disk_mib(&home.root), load_x100: load_x100(), speed, speed_class: speed.map(Facts::class_of), kernel: kernel(), } } /// The kernel's release, as uname says. Not read from /proc: an Android /// app may not read it there, and is refused (and logged) every claim. #[cfg(unix)] pub fn kernel() -> Option { let mut u: libc::utsname = unsafe { std::mem::zeroed() }; // SAFETY: `u` is a writable utsname. if unsafe { libc::uname(&mut u) } != 0 { return None; } // SAFETY: uname writes NUL-terminated strings. let release = unsafe { std::ffi::CStr::from_ptr(u.release.as_ptr()) }; Some(release.to_string_lossy().into_owned()) } /// Windows' version (`Windows 10.0.26100`), from `RtlGetVersion`, which /// unlike `GetVersionEx` does not depend on the program's manifest. #[cfg(windows)] pub fn kernel() -> Option { use windows_sys::Win32::System::SystemInformation::OSVERSIONINFOW; // SAFETY: an all-zero OSVERSIONINFOW is valid; its size is set before the call. let mut v: OSVERSIONINFOW = unsafe { std::mem::zeroed() }; v.dwOSVersionInfoSize = std::mem::size_of::() as u32; // SAFETY: `v` is a writable OSVERSIONINFOW with its size set. let status = unsafe { windows_sys::Wdk::System::SystemServices::RtlGetVersion(&mut v) }; (status == 0).then(|| format!("Windows {}.{}.{}", v.dwMajorVersion, v.dwMinorVersion, v.dwBuildNumber)) } /// Free space for an unprivileged writer on the file system holding `path`, /// in MiB. #[cfg(unix)] pub fn free_disk_mib(path: &std::path::Path) -> Option { use std::os::unix::ffi::OsStrExt; let c = std::ffi::CString::new(path.as_os_str().as_bytes()).ok()?; let mut s: libc::statvfs = unsafe { std::mem::zeroed() }; // SAFETY: `c` is a valid NUL-terminated path and `s` a writable statvfs. if unsafe { libc::statvfs(c.as_ptr(), &mut s) } != 0 { return None; } Some((s.f_bavail as u64).saturating_mul(s.f_frsize as u64) >> 20) } #[cfg(windows)] pub fn free_disk_mib(path: &std::path::Path) -> Option { use std::os::windows::ffi::OsStrExt; let wide: Vec = path.as_os_str().encode_wide().chain(Some(0)).collect(); let mut free: u64 = 0; // SAFETY: `wide` is a NUL-terminated path and `free` a writable u64; // the totals it may also fill are not asked for. let ok = unsafe { windows_sys::Win32::Storage::FileSystem::GetDiskFreeSpaceExW(wide.as_ptr(), &mut free, std::ptr::null_mut(), std::ptr::null_mut()) }; (ok != 0).then_some(free >> 20) } /// The disk the files under `dir` take (their blocks, not their sizes: a /// job's disks are sparse), in MiB; links are not followed. pub fn allocated_mib(dir: &std::path::Path) -> Option { let mut total: u64 = 0; let mut stack = vec![dir.to_path_buf()]; while let Some(d) = stack.pop() { for e in std::fs::read_dir(&d).ok()?.flatten() { let m = std::fs::symlink_metadata(e.path()).ok()?; if m.is_dir() { stack.push(e.path()); } else { total = total.saturating_add(crate::portable::disk_bytes(&m)); } } } Some(total >> 20) } /// A process's peak resident memory (`VmHWM`), in MiB. pub fn peak_rss_mib(pid: u32) -> Option { let text = std::fs::read_to_string(format!("/proc/{pid}/status")).ok()?; let kib: u64 = text.lines().find_map(|l| l.strip_prefix("VmHWM:"))?.trim().trim_end_matches("kB").trim().parse().ok()?; Some(kib / 1024) } /// The 1-minute load average, times 100, from sysinfo (the kernel's own /// numbers behind /proc/loadavg, which an Android app may not read); not /// measured on Windows. #[cfg(not(any(target_os = "linux", target_os = "android")))] pub fn load_x100() -> Option { None } #[cfg(any(target_os = "linux", target_os = "android"))] pub fn load_x100() -> Option { let mut i: libc::sysinfo = unsafe { std::mem::zeroed() }; // SAFETY: `i` is a writable sysinfo. if unsafe { libc::sysinfo(&mut i) } != 0 { return None; } // Fixed point with 16 fraction bits (SI_LOAD_SHIFT). Some(((i.loads[0] as f64) / 65536.0 * 100.0).round() as u32) } /// The benchmark, one thread: BLAKE3 over 256 MiB (spec §11.1), and a sort /// of 4 Mi pseudo-random words for the integer and memory side. Answers /// the score, 100 on the reference machine, and the time it took. pub fn bench() -> (u32, u128) { let start = Instant::now(); let buf: Vec = (0..32u32 << 20).map(|i| (i.wrapping_mul(2_654_435_761) >> 13) as u8).collect(); let mut h = blake3::Hasher::new(); for _ in 0..8 { h.update(&buf); } std::hint::black_box(h.finalize()); drop(buf); let mut x: u64 = 0x9e37_79b9_7f4a_7c15; let mut words: Vec = (0..4u64 << 20) .map(|_| { x ^= x << 13; x ^= x >> 7; x ^= x << 17; x }) .collect(); words.sort_unstable(); std::hint::black_box(&words); let ms = start.elapsed().as_millis().max(1); ((REFERENCE_MS * 100.0 / ms as f64).round() as u32, ms) } #[cfg(test)] mod tests { use super::*; #[test] fn the_cheap_facts_are_read_and_classes_double() { assert!(free_disk_mib(std::path::Path::new("/")).is_some()); // The same numbers /proc gives, read without it (an Android app may not). let proc_load: f64 = std::fs::read_to_string("/proc/loadavg").unwrap().split_whitespace().next().unwrap().parse().unwrap(); let load = load_x100().unwrap() as f64 / 100.0; assert!((load - proc_load).abs() < 1.0 + proc_load / 4.0, "sysinfo {load}, /proc {proc_load}"); assert_eq!(kernel().unwrap(), std::fs::read_to_string("/proc/sys/kernel/osrelease").unwrap().trim()); assert!(peak_rss_mib(std::process::id()).is_some_and(|m| m > 0)); let dir = tempfile::tempdir().unwrap(); let f = std::fs::File::create(dir.path().join("sparse")).unwrap(); f.set_len(1 << 30).unwrap(); std::fs::write(dir.path().join("full"), vec![7u8; 3 << 20]).unwrap(); let mib = allocated_mib(dir.path()).unwrap(); assert!((3..=4).contains(&mib), "the sparse gigabyte takes nothing, the written 3 MiB do: {mib}"); assert_eq!(Facts::class_of(100), 3, "the reference machine"); assert_eq!((Facts::class_of(50), Facts::class_of(200), Facts::class_of(500), Facts::class_of(10)), (2, 4, 5, 1)); } }