Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/znippy

znippy-common/tests/hostinfo_vs_sysinfo.rs

//! `hostinfo` vs `sysinfo`: the same five numbers, read two ways.
//!
//! `znippy-common` stopped linking `sysinfo` at runtime (see that crate's entry
//! in `Cargo.toml` — its `multithread` feature put `rayon` into every consumer's
//! graph, breaking ROOT LAW #0 through a third party). The numbers still have to
//! be right, so `sysinfo` stays on as a **dev-dependency oracle**: an independent
//! implementation, written by someone else, that `hostinfo` is asserted against.
//!
//! That is worth more here than it ever was as a runtime dep. A hand-rolled
//! `/proc` parser is exactly the kind of code that silently returns a plausible
//! wrong number, and a plausible wrong number is what sizes the compress slot
//! pool.
//!
//! These tests are Linux-only: `hostinfo` answers `None` elsewhere by
//! construction, and there would be nothing to compare.

#![cfg(target_os = "linux")]

use znippy_common::hostinfo;

/// Guard against the identity-value trap: a comparison of two zeros passes
/// whatever the parsers do. Every value asserted below must first be real.
fn assert_plausible(label: &str, v: u64) {
    assert!(
        v > 0,
        "{label} read as 0 — a zero here would make every equality below vacuous"
    );
}

#[test]
fn mem_total_matches_sysinfo_exactly() {
    let sys = sysinfo::System::new_with_specifics(
        sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
    );
    let theirs = sys.total_memory();
    let ours = hostinfo::mem_total_bytes().expect("hostinfo could not read MemTotal on Linux");

    assert_plausible("MemTotal", theirs);
    assert_plausible("MemTotal", ours);
    // MemTotal does not move while the machine is up, so this is exact — no
    // tolerance to hide a units bug (the KiB→bytes ×1024 is the easy one to get
    // wrong, and a tolerance would swallow it).
    assert_eq!(
        ours, theirs,
        "hostinfo MemTotal {ours} != sysinfo total_memory {theirs}"
    );
}

#[test]
fn mem_available_tracks_sysinfo() {
    let mut sys = sysinfo::System::new_with_specifics(
        sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
    );
    sys.refresh_memory();
    let theirs = sys.available_memory();
    let ours =
        hostinfo::mem_available_bytes().expect("hostinfo could not read MemAvailable on Linux");

    assert_plausible("MemAvailable", theirs);
    assert_plausible("MemAvailable", ours);

    // The two reads are microseconds apart but not simultaneous, and this box
    // runs a build fleet, so MemAvailable genuinely moves between them. 5% is
    // wide enough not to flake and far too tight to survive a units error
    // (×1024) or a wrong key (MemFree is ~10% of MemAvailable on oden).
    let (hi, lo) = (ours.max(theirs), ours.min(theirs));
    let drift = (hi - lo) as f64 / hi as f64;
    assert!(
        drift < 0.05,
        "hostinfo MemAvailable {ours} vs sysinfo {theirs}: {:.1}% apart, \
         which is more than live drift can explain",
        drift * 100.0
    );
}

#[test]
fn core_counts_match_sysinfo_exactly() {
    let mut sys = sysinfo::System::new_with_specifics(
        sysinfo::RefreshKind::nothing().with_cpu(sysinfo::CpuRefreshKind::nothing()),
    );
    sys.refresh_cpu_list(sysinfo::CpuRefreshKind::nothing());

    let their_logical = sys.cpus().len();
    let our_logical = hostinfo::logical_cpu_count();
    assert!(their_logical > 0, "sysinfo reported 0 logical CPUs");
    assert_eq!(
        our_logical, their_logical,
        "hostinfo logical_cpu_count {our_logical} != sysinfo cpus().len() {their_logical}"
    );

    // `physical_core_count` is a port of sysinfo's own algorithm, so it is exact
    // on purpose — including the fallback for hosts that print no topology.
    let theirs = sysinfo::System::physical_core_count();
    let ours = hostinfo::physical_core_count();
    assert_eq!(
        ours, theirs,
        "hostinfo physical_core_count {ours:?} != sysinfo {theirs:?}"
    );

    // This is the number that becomes `max_core_allowed`. It must never be 0 and
    // never exceed the logical width.
    let effective = ours.unwrap_or(our_logical);
    assert!(
        effective > 0 && effective <= our_logical,
        "effective core width {effective} is not in 1..={our_logical}"
    );
}

/// The regression guard for a tier that was dead.
///
/// `sysinfo::System::cgroup_limits()` is `limits_for_system()`: it reads
/// `/sys/fs/cgroup/memory.max` and `memory.current` — the **root** cgroup, which
/// on cgroup v2 has no memory-controller files at all. It therefore answers
/// `None` on a normal host no matter what limit this process is actually under,
/// and `common_config`'s documented "use the cgroup ceiling, not host free RAM"
/// tier could never fire.
///
/// `hostinfo::cgroup_memory` follows `/proc/self/cgroup`. This test rebuilds the
/// expected answer from the sysfs files independently — a second reader, in the
/// test — and asserts `hostinfo` agrees. It always asserts something: when the
/// process is under no ceiling at all, the expectation is "no ceiling" and that
/// is checked too.
#[test]
fn cgroup_reader_follows_the_process_cgroup_not_the_root() {
    use std::path::Path;

    let mem_total = hostinfo::mem_total_bytes().expect("MemTotal");
    let self_cgroup = std::fs::read_to_string("/proc/self/cgroup").expect("/proc/self/cgroup");

    // v2 line: `0::/system.slice/whatever.service`
    let rel = self_cgroup
        .lines()
        .find_map(|l| {
            let mut p = l.splitn(3, ':');
            let _id = p.next()?;
            if !p.next()?.is_empty() {
                return None;
            }
            Some(p.next()?.trim_start_matches('/').to_string())
        })
        .expect("no cgroup-v2 line in /proc/self/cgroup — this host is v1-only, \
                 which this test does not cover");

    let base = Path::new("/sys/fs/cgroup").join(&rel);
    if !base.join("memory.current").exists() {
        // No memory controller on this process's cgroup: hostinfo must say so
        // rather than invent a ceiling. Still a real assertion.
        assert!(
            hostinfo::cgroup_memory().is_none(),
            "no memory.current at {base:?}, yet cgroup_memory() returned Some"
        );
        return;
    }

    // Independent walk: tightest memory.max from this cgroup up to the root.
    let mut expected = mem_total;
    for anc in base.ancestors() {
        let raw = std::fs::read_to_string(anc.join("memory.max")).unwrap_or_default();
        let max = match raw.trim() {
            "" | "max" => u64::MAX,
            s => s.parse::<u64>().unwrap_or(u64::MAX),
        };
        if max <= mem_total {
            expected = expected.min(max);
        }
        if anc == Path::new("/sys/fs/cgroup") {
            break;
        }
    }
    let expected_rss: u64 = std::fs::read_to_string(base.join("memory.stat"))
        .expect("memory.stat")
        .lines()
        .find_map(|l| {
            let mut it = l.split_whitespace();
            (it.next()? == "anon").then(|| it.next()?.parse().ok())?
        })
        .expect("no `anon` key in memory.stat");

    let got = hostinfo::cgroup_memory().expect(
        "memory.current exists for this process's cgroup, so cgroup_memory() must \
         return Some — returning None here is exactly the sysinfo bug being fixed",
    );

    assert_eq!(
        got.total_memory, expected,
        "cgroup ceiling: hostinfo {} != independent walk {expected}",
        got.total_memory
    );
    assert_plausible("cgroup total_memory", got.total_memory);
    assert!(
        got.total_memory <= mem_total,
        "cgroup ceiling {} exceeds MemTotal {mem_total}",
        got.total_memory
    );
    // rss moves under load, so this is a tolerance and not an equality — but a
    // tight one. Reading the wrong key from memory.stat is the failure mode it
    // exists to catch, and the neighbouring keys are orders of magnitude away
    // (`file` and `slab` on oden), so 50% cannot let one through.
    assert_plausible("cgroup rss", got.rss);
    assert_plausible("cgroup rss (independent read)", expected_rss);
    let (hi, lo) = (got.rss.max(expected_rss), got.rss.min(expected_rss));
    let drift = (hi - lo) as f64 / hi as f64;
    assert!(
        drift < 0.5,
        "cgroup rss: hostinfo {} vs independent read {expected_rss} — {:.0}% apart, \
         which means a different memory.stat key, not live drift",
        got.rss,
        drift * 100.0,
    );

    // For the record, and so a future sysinfo change is visible in the log
    // rather than silently reinstating the old behaviour.
    let mut sys = sysinfo::System::new_with_specifics(
        sysinfo::RefreshKind::nothing().with_memory(sysinfo::MemoryRefreshKind::everything()),
    );
    sys.refresh_memory();
    eprintln!(
        "cgroup ceiling: hostinfo = Some({}), sysinfo System::cgroup_limits() = {:?}",
        got.total_memory,
        sys.cgroup_limits().map(|c| c.total_memory),
    );
}