draupnir 0.1.9

Draupnir — the nordisk boot/provisioning library: fire up a runtime from one BootSpec across three backends (KVM via tunnr · OCI container · Redfish bare-metal virtual-media) and drive its power lifecycle. Odin's ring that drips eight identical copies → boot a fleet of identical machines from one ISO.
Documentation
//! **LIGHT fleet-boot bench — serial vs concurrent.** Measures the wall-clock of
//! booting-and-awaiting an `n`-member fleet the serial way
//! ([`boot_fleet_and_await`]) against the concurrent way
//! ([`boot_fleet_and_await_parallel`], one scoped thread per member).
//!
//! The backend is a fake that boots instantly and reports `On` on the first poll,
//! but its `status` sleeps a small fixed **latency** — modelling the real backends'
//! readback round-trip (a QEMU handle read / a container-state query / a BMC REST
//! call). That latency is exactly what the parallel path overlaps: the serial arm
//! pays it `n` times in series (~`n × latency`), the parallel arm pays it once
//! (~`latency`). With no latency the two are near-equal (thread overhead only), so
//! the arm injects a realistic per-member latency to show the win the fleet actually
//! sees in production.
//!
//! This is the LIGHT arm; the Loki bencher runs the authoritative HEAVY pass
//! (host-stamped, plain numbers).

use std::time::Duration;

use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use draupnir::{
    boot_fleet_and_await, boot_fleet_and_await_parallel, Boot, BootSpec, Lifecycle, Machine,
    PowerState, Result, WaitOptions,
};

/// A `Sync` fake backend: instant boot, `On` on the first poll, but each `status`
/// costs `latency` — the per-member readback round-trip the parallel path overlaps.
struct LatentNode {
    latency: Duration,
}

impl Boot for LatentNode {
    fn boot(&self, spec: &BootSpec) -> Result<Machine> {
        Ok(Machine::started(format!("id-{}", spec.name), spec))
    }
}

impl Lifecycle for LatentNode {
    fn power_on(&self, _m: &Machine) -> Result<()> {
        Ok(())
    }
    fn power_off(&self, _m: &Machine) -> Result<()> {
        Ok(())
    }
    fn status(&self, _m: &Machine) -> Result<PowerState> {
        std::thread::sleep(self.latency);
        Ok(PowerState::On)
    }
}

fn bench_fleet_boot(c: &mut Criterion) {
    // A per-member readback latency; short enough for a light arm, long enough that
    // n-in-series clearly dominates thread-spawn overhead.
    let backend = LatentNode {
        latency: Duration::from_millis(2),
    };
    let spec = BootSpec::container("node", "redis:7");
    // The first poll already reads `On`, so the wait is one `status` latency per
    // member; a generous budget keeps the bench off the timeout path.
    let opts = WaitOptions::bounded(Duration::from_secs(30), Duration::from_millis(1));

    let mut group = c.benchmark_group("fleet_boot");
    for n in [4usize, 8, 16] {
        group.bench_with_input(BenchmarkId::new("serial", n), &n, |b, &n| {
            b.iter(|| boot_fleet_and_await(&spec, n, &backend, &opts));
        });
        group.bench_with_input(BenchmarkId::new("parallel", n), &n, |b, &n| {
            b.iter(|| boot_fleet_and_await_parallel(&spec, n, &backend, &opts));
        });
    }
    group.finish();
}

criterion_group!(benches, bench_fleet_boot);
criterion_main!(benches);