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>
71 lines
3.3 KiB
Rust
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};
|