Files
rutster/crates/rutster-sim/src/lib.rs
Aaron D. Lee da63d58f61 feat(sim): TickLagGauge -- per-tick wall-clock measurement (slice-4½ S6)
Spec section 3.6 says the gauge polls MediaCmd::Stats from slice-5/seams MediaThread.
S4 standalone-path conclusion (per kickoff hard rule) means the SimCall wires itself
in tokio WITHOUT registering with the binary MediaThread -- no MediaCmd::Stats channel
to poll. This S6 implementation adapts: the gauge is wired INTO the SimCalls tick loop
directly via a shared Arc<TickLagStats> handle. SimCall::run_with_gauge records per-tick
wall-clock duration via Instant::now() measurement around the tick work (not including
the 20ms sleep -- matches MediaStats.last_tick_micros semantics).

Same conceptual metric (max tick lag + overrun count + pct), different source (in-process
tokio SimCall tick loop rather than binary MediaThread poll loop). Future slice (post-spearhead
refinement, paired with network-realism mode) wires the gauge against the binary MediaThread
per spec section 3.6 -- requires either MediaThread registration (the RegisterSim variant
forbidden this slice) OR a client-server sim mode (deferred per spec 8.6).

TickLagStats uses atomics (AtomicU64) -- 3 atomic ops per tick (CAS-max, conditional
fetch_add on overruns, unconditional fetch_add on total) -- rather than Mutex<Vec<Duration>>
which would lock the vector per tick + add ordering overhead significant to the per-tick
microseconds budget.

ConcurrencyRunner creates one shared Arc<TickLagStats> per concurrency level + passes
clones to each of the N SimCalls via run_with_gauge. After the sweep, reads the gauges
fields to populate PerConcurrencyReports tick-lag fields (max_tick_lag_micros, tick_overruns,
total_ticks, tick_overrun_pct) -- the ADR-0010 doctrine-drift detector.

SimCall::run(self) -> LatencyProbe is preserved as a convenience default that calls
run_with_gauge(TickLagStats::new()) (gauge data discarded); callers needing tick-lag
data (ConcurrencyRunner) use run_with_gauge directly.

Signed-off-by: Aaron D. Lee <himself@adlee.work>
2026-07-05 12:19:12 -04:00

71 lines
3.3 KiB
Rust

//! # rutster-sim — the self-hostable benchmark + simulation harness
//!
//! **Status:** spearhead step 4½ (ADR-0010). The wedge's measurement surface.
//!
//! This crate drives synthetic callers through the SAME media-leg path real
//! callers use, measures p50/p99 mouth-to-ear latency + barge-in kill-time
//! against slice-4's ≤60 ms kill budget, and runs the same measurements at
//! 1 / 10 / 50 concurrent calls. A separate CI job
//! (`cargo test --all --features=sim-bench`) asserts thresholds per commit;
//! a latency regression fails the build (ADR-0010).
//!
//! # Why this crate exists (the FOB differentiator)
//!
//! Slice-4 ships a reflex loop + a synthetic e2e test. SIM-BENCH is the
//! artifact that turns arithmetic latency claims into CI-regressed
//! measurement. See
//! [`docs/superpowers/specs/2026-07-05-slice-4-half-benchmark-sim-design.md`]
//! for the design.
//!
//! # Why a separate crate (not in-tree tests)
//!
//! The harness is hot-path-adjacent + differentiating (ADR-0008 FOB) — it
//! earns cratehood the same way `rutster-tap` did. The dep direction is
//! clean: `rutster-sim` → `rutster-media` + `rutster`. The harness
//! consumes types; it doesn't ride on the binary's internal plumbing.
//!
//! # Module map (lands across S1-S7)
//!
//! - [`scenario`] (S1) — `Scenario` + `ScenarioStep` TOML-deserializable
//! scripted-caller data types. Determinism is the point.
//! - [`sim_audio_pipe`] (S2) — `SimAudioPipe: AudioPipe` test-double that
//! IS the caller; captures both clocks (`Instant::now()` at onset +
//! receipt). The measurement boundary (spec §2.2).
//! - [`latency`] (S3) — `LatencyProbe`: post-hoc p50/p99 kill + mouth-to-ear
//! computation from the `Capture` event stream.
//! - [`runner`] (S4) — `SimCall` + `ScenarioRunner`: drives one synthetic
//! caller end-to-end against the FOB reflex loop standalone in tokio
//! (no `MediaThread` extension per S4 standalone-path conclusion).
//! - [`concurrency`] (S5) — `ConcurrencyRunner`: N concurrent `SimCall`s
//! against the same MockRealtimeBrain; aggregates per-call latencies
//! into a `SweepReport`.
//! - [`tick_lag`] (S6) — `TickLagGauge`: the ADR-0010 doctrine-drift
//! detector. Surfaces `tick_overruns` / `last_tick_micros` in the
//! `SweepReport`.
//! - [`thresholds`] (S7) — Threshold consts + the `#[cfg(feature =
//! "sim-bench")] #[tokio::test]` assertion tests. A latency regression
//! fails the build.
// All modules declared upfront so the lib.rs is stable across task
// commits; each module file grows from a `//! stub` header to its full
// impl as its task lands. Only the `pub use` re-exports for landed
// modules are present — they grow as each task's symbols become available.
pub mod concurrency;
pub mod latency;
pub mod runner;
pub mod scenario;
pub mod sim_audio_pipe;
pub mod thresholds;
pub mod tick_lag;
pub use concurrency::{ConcurrencyRunner, PerConcurrencyReport, SweepReport};
pub use latency::LatencyProbe;
pub use runner::{ScenarioRunner, SimCall};
pub use scenario::{Scenario, ScenarioError, ScenarioStep};
pub use sim_audio_pipe::{Capture, SimAudioPipe};
pub use thresholds::{
BARGE_IN_KILL_TIME_P99_MS, MOUTH_TO_EAR_P99_MS, SWEEP_CONCURRENCIES, TICK_LAG_MAX_MS,
TICK_OVERRUN_PCT_MAX,
};
pub use tick_lag::{TickLagGauge, TickLagStats};