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

nordisk/znippy-zoomies

tests/gatling_sort_saturation.rs

//! RED-when-broken **core-saturation** guard for [`gatling_sort`].
//!
//! Correctness (matches std, stability, edges) is unit-tested inside the gatling
//! crate. THIS test defends the *performance contract* the whole primitive
//! exists for: on a multi-core box the parallel merge sort must keep well more
//! than one core busy. It sorts a large input while measuring CPU-time/wall-time
//! (cores_busy) via `getrusage`; a regression that drops the sort back onto a
//! single core collapses cores_busy to ~1.0 and turns this test RED.
//!
//! Skipped on <4-core boxes (no saturation to assert) and off Linux (no
//! `getrusage` here), where it degrades to a plain correctness check.

use std::time::Instant;

use rand::{Rng, SeedableRng, rngs::StdRng};
use znippy_zoomies::gatling_sort::{gatling_sort_by, gatling_sort_unstable_by};

#[cfg(target_os = "linux")]
fn cpu_secs() -> f64 {
    // SAFETY: `getrusage` fills a caller-owned, zero-initialized `rusage`.
    unsafe {
        let mut ru: libc::rusage = std::mem::zeroed();
        libc::getrusage(libc::RUSAGE_SELF, &mut ru);
        let u = ru.ru_utime.tv_sec as f64 + ru.ru_utime.tv_usec as f64 * 1e-6;
        let s = ru.ru_stime.tv_sec as f64 + ru.ru_stime.tv_usec as f64 * 1e-6;
        u + s
    }
}
#[cfg(not(target_os = "linux"))]
fn cpu_secs() -> f64 {
    f64::NAN
}

fn cores() -> usize {
    std::thread::available_parallelism()
        .map(|n| n.get())
        .unwrap_or(1)
}

#[test]
fn large_unstable_sort_saturates_cores() {
    let nc = cores();
    // 4M i64 (~32 MB) — large enough to dwarf pool spin-up even in a debug test,
    // small enough to stay quick. Sorted result is verified regardless of cores.
    let n = 4_000_000usize;
    let mut rng = StdRng::seed_from_u64(0x5A17_0000);
    let base: Vec<i64> = (0..n).map(|_| rng.next_u64() as i64).collect();

    let mut want = base.clone();
    want.sort_unstable();

    let mut got = base.clone();
    let cpu0 = cpu_secs();
    let t0 = Instant::now();
    gatling_sort_unstable_by(&mut got, |a, b| a.cmp(b));
    let wall = t0.elapsed().as_secs_f64();
    let cpu = cpu_secs() - cpu0;

    assert_eq!(got, want, "parallel sort produced the wrong result");

    let cores_busy = if wall > 0.0 && cpu.is_finite() {
        cpu / wall
    } else {
        f64::NAN
    };
    eprintln!(
        "gatling_sort saturation: n={n} cores={nc} wall={wall:.3}s cpu={cpu:.3}s cores_busy={cores_busy:.2}"
    );

    if nc >= 4 && cores_busy.is_finite() {
        // Conservative floor (0.30·cores, ≥2.0): a real parallel sort clears it
        // comfortably; a serial regression (cores_busy ≈ 1.0) can never.
        let floor = (nc as f64 * 0.30).max(2.0);
        assert!(
            cores_busy >= floor,
            "cores_busy {cores_busy:.2} < floor {floor:.2}: the sort is not saturating \
             a {nc}-core box (serial regression?)"
        );
    } else {
        eprintln!("saturation floor skipped (cores={nc}, cores_busy={cores_busy:.2})");
    }
}

/// The stable variant must ALSO stay parallel (its per-run `sort_by` + merge is
/// the same fork-join), so guard its saturation too — and re-confirm it is
/// byte-identical to `slice::sort_by` on a large, tie-heavy input.
#[test]
fn large_stable_sort_saturates_and_matches_std() {
    let nc = cores();
    let n = 4_000_000usize;
    let mut rng = StdRng::seed_from_u64(0x57AB_1E00);
    // Tie-heavy keys (mod 4096) so stability is genuinely exercised at scale.
    let base: Vec<(u32, u32)> = (0..n)
        .map(|i| ((rng.next_u64() % 4096) as u32, i as u32))
        .collect();

    let mut want = base.clone();
    want.sort_by(|a, b| a.0.cmp(&b.0));

    let mut got = base.clone();
    let cpu0 = cpu_secs();
    let t0 = Instant::now();
    gatling_sort_by(&mut got, |a, b| a.0.cmp(&b.0));
    let wall = t0.elapsed().as_secs_f64();
    let cpu = cpu_secs() - cpu0;

    assert_eq!(got, want, "stable parallel sort != slice::sort_by at scale");

    let cores_busy = if wall > 0.0 && cpu.is_finite() {
        cpu / wall
    } else {
        f64::NAN
    };
    eprintln!("gatling_sort_by saturation: n={n} cores={nc} cores_busy={cores_busy:.2}");
    if nc >= 4 && cores_busy.is_finite() {
        let floor = (nc as f64 * 0.30).max(2.0);
        assert!(
            cores_busy >= floor,
            "stable sort cores_busy {cores_busy:.2} < floor {floor:.2}"
        );
    }
}