//! **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);