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

nordisk/facett

facett-cfd/examples/pipeflow_bench.rs

//! **What one frame of REAL LIQUID costs** — the per-frame cost of
//! [`facett_cfd::PipeFlow`] at realistic and unrealistic pipe counts, so "it holds 60
//! fps" is a measured number and not a hope.
//!
//! A frame of a facett pane has a **16.67 ms** budget for *everything*: the sim, the
//! projection, the tessellation and the submit. The system map's fluid is one small
//! tenant of that, so the number that matters is per-step ns and its share of the frame.
//!
//! The arms:
//!
//! - `serial` — [`PipeFlow::step_serial`], the 1-core reference (and the honest wasm /
//!   threadless number).
//! - `gatling` — [`PipeFlow::step_gatling`], the znippy fork-join lane (`rayon` is forbidden in
//!   facett, ROOT LAW #0). Bit-identical output; the question is only whether the
//!   fan-out earns its dispatch at each size. (`PipeFlow::step` picks between the two by
//!   size — `PARALLEL_MIN_CELLS` — which is exactly the threshold this bench sets.)
//!
//! Gravity is re-aimed every step (a tumbling camera), because that is the workload the
//! pane actually runs — a static-gravity bench would flatter the velocity integrator.
//!
//! Run: `cargo run --release -p facett-cfd --example pipeflow_bench`

use std::time::Instant;

use facett_cfd::PipeFlow;

/// Cells per pipe — the value `SystemMap` uses.
const CELLS: usize = 48;
/// Cell budget per timed arm: the step count is `BUDGET / cells`, clamped, so a huge
/// network does not turn the bench into a coffee break while a small one still gets
/// enough repeats to be stable.
const BUDGET: usize = 10_000_000;
/// One frame's budget at 60 fps, in nanoseconds.
const FRAME_NS: f64 = 16_666_667.0;

/// A ring of `n` junctions on a tilted circle, wired ring + chords, so pipes point in
/// every direction and every one of them has a real gravity component.
fn network(n: usize) -> (Vec<[f32; 3]>, Vec<(usize, usize)>) {
    let pos: Vec<[f32; 3]> = (0..n)
        .map(|i| {
            let a = i as f32 * std::f32::consts::TAU / n as f32;
            let b = i as f32 * 0.37;
            [a.cos() * (1.0 + 0.2 * b.sin()), (a * 2.0).sin() * 0.8, a.sin() * (1.0 + 0.2 * b.cos())]
        })
        .collect();
    let mut edges: Vec<(usize, usize)> = (0..n).map(|i| (i, (i + 1) % n)).collect();
    for i in (0..n).step_by(3) {
        edges.push((i, (i + 7) % n));
    }
    (pos, edges)
}

fn charged(n: usize) -> PipeFlow {
    let (pos, edges) = network(n);
    let mut f = PipeFlow::new(pos, &edges, CELLS);
    f.seed_fill(0x9E37_79B9, 0.5);
    f
}

/// Time `steps` steps, re-aiming gravity every step, returning ns/step.
fn time(f: &mut PipeFlow, gatling: bool, steps: usize) -> f64 {
    // Warm-up of the same shape (page in the buffers, spin up the gatling workers).
    for k in 0..(steps / 10).clamp(5, 200) {
        let a = k as f32 * 0.01;
        f.set_gravity([a.sin(), -a.cos(), 0.3]);
        if gatling {
            f.step_gatling(1.0 / 60.0);
        } else {
            f.step_serial(1.0 / 60.0);
        }
    }
    let t0 = Instant::now();
    for k in 0..steps {
        let a = k as f32 * 0.01;
        f.set_gravity([a.sin(), -a.cos(), 0.3]);
        if gatling {
            f.step_gatling(1.0 / 60.0);
        } else {
            f.step_serial(1.0 / 60.0);
        }
    }
    t0.elapsed().as_nanos() as f64 / steps as f64
}

fn main() {
    println!("facett-cfd::PipeFlow — per-frame cost, {CELLS} cells/pipe");
    println!("host: NORNIR_MACHINE={}", std::env::var("NORNIR_MACHINE").unwrap_or_else(|_| "(unset)".into()));
    // The load average matters for the gatling arm (thread spawns contend), so record
    // it in the output rather than leaving the reader to guess whether the box was quiet.
    println!("load: {}", std::fs::read_to_string("/proc/loadavg").unwrap_or_default().trim());
    println!();
    println!("| nodes | pipes | cells | steps | serial ns/step | gatling ns/step | serial % of 16.67 ms frame |");
    println!("|---:|---:|---:|---:|---:|---:|---:|");
    for &n in &[8usize, 32, 128, 512, 2_048, 8_192, 32_768] {
        let mut a = charged(n);
        let mut b = charged(n);
        let pipes = a.pipes();
        let cells = pipes * CELLS;
        let steps = (BUDGET / cells.max(1)).clamp(20, 20_000);
        let serial = time(&mut a, false, steps);
        let gat = time(&mut b, true, steps);
        // Sanity: the two lanes must agree (this bench is also a determinism smoke test).
        let drift = (a.total_volume() - b.total_volume()).abs();
        assert!(drift < 1e-2, "the two lanes diverged (volume drift {drift})");
        println!(
            "| {n} | {pipes} | {cells} | {steps} | {:.0} | {:.0} | {:.3}% |",
            serial,
            gat,
            serial / FRAME_NS * 100.0
        );
    }
    println!();
    println!("The System Map ships 8 pipes. Read the first row for the real answer and the");
    println!("rest as the headroom curve.");
}