Files
peltier.io/crates/bench-runner/src/service.rs
T
benbrownandClaude Opus 4.8 899d55f4b6 gates: close the audit findings — enforce the gates the docs claimed
An adversarial audit found the enforcement story ahead of the code: gates
that were documented as hard gates were advisory, manual, or absent, and
several human-facing claims outran the machine record. This makes the
claims true.

High findings

- Differential fuzz was documentation, not enforcement: verdict hardcoded
  fuzz_iters=0 and diff-test hardcoded the gate to Skipped, while the case
  studies claimed 10k-iteration fuzz on the same wins. Fuzz is now a real
  pipeline gate (strict FUZZ-RESULT contract, executed count recorded), and
  it is inherently baseline-vs-candidate, so it runs on the accept path and
  reports Skipped in the standalone `just gates` flow. New hard rule: a
  machine `accepted` verdict is impossible unless the fuzz gate actually
  passed — otherwise it caps at needs-human-review.
- The agent's system prompt claimed FP/concurrency/unsafe patches were
  "routed to human review automatically". No such mechanism existed. Added
  a conservative lexical risk classifier (crates/verdict/src/risk.rs) that
  forces needs-human-review on any would-be accept carrying those signals;
  fp-tolerance equivalence always signals.
- The local verdict path ran agent-influenced code unsandboxed. It now runs
  under scripts/no-net.sh (network namespace) and fails closed. Upstream
  test suites are hash-verified against corpora/<t>/TESTSUITE.sha256 when
  present (scripts/pin-testsuite.sh generates them).

Also fixed, found while fixing the above

- The pipeline script's `set -e` was inert (brace group as the left operand
  of `&&`), so a failing candidate build did not abort the run and verdict
  could bench a stale binary. Now a scoped subshell whose status is branched
  on at top level.
- A wrapper that exits before exec'ing (no-net.sh refusing, bad wrapper path)
  stranded RUNNING.lock and left read_verdict polling forever. Failure
  accounting now lives outside the wrapper.
- harnessd's patch allowlist ignored git rename/copy headers and --no-prefix
  diffs; the rename-escape case is now a committed regression test.
- The workspace did not compile on Windows (ungated Unix APIs). cfg-gated;
  71 portable tests now run on both platforms.
- pin-check regenerated the corpus manifest instead of verifying it; it is
  now verify-only, with a deliberate `just pin-corpus` to re-pin.
- Max RSS is captured per run on Unix; PMU counters are opt-in diagnostics
  (compare --perf-stat), never part of the accept decision.

Docs now match the code, including what is NOT yet enforced: no target ships
a TESTSUITE.sha256 pin, no target declares [build].tsan, and matmul is not
wired to the fp-tolerance gate — each is stated as mechanized-but-inert
rather than done. Corrected the stale "33 ledger rows" (34), the playbook's
class-3 cJSON win (rejected), adversarial-review.md's sanitizer-separation
claim (five accepted rows carry sanitizers_clean=false), the unsupported
"40,000 requests" figure, and the blanket "historical rows record
fuzz_iters=0" (8 of 10; phase0-tokei-002/-003 record 4,332).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 13:10:14 -04:00

265 lines
8.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Service-mode latency bench (SPEC §3.1 mode (c)). A coordinated-
//! omission-CORRECT, open-loop, fixed-rate load generator: requests are
//! scheduled at `start + i/rate` and each latency is measured from its
//! *intended* send time, not its actual send time. So when the server
//! falls behind, the requests that pile up behind it each carry the full
//! queueing delay a real client would see — the exact error (coordinated
//! omission) that closed-loop tools like `ab` hide. We store every
//! latency and take exact percentiles (no HdrHistogram bucketing error;
//! the sample is bounded).
//!
//! Interleaved A/B, like the wall-time bench: each session yields one
//! percentile (p50/p99) per side; the existing bootstrap ratio-CI machine
//! then works unchanged, treating "p99 latency" the way it treats "wall
//! time" — lower is better, speedup = baseline / candidate.
use anyhow::{anyhow, ensure, Result};
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{Ipv4Addr, TcpStream};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant};
use crate::stats::{self, RatioCi};
pub struct ServiceCfg {
pub server_bin: String,
pub doc: String,
pub iters: u64,
pub rate: f64,
pub count: usize,
pub warmup: usize,
pub workers: usize,
/// Prefix that pins the SERVER to a core (e.g. `taskset -c 2`); the
/// load generator runs on the other cores.
pub pin_prefix: String,
/// Calibration only: inject this many µs of busy-wait per request
/// (via HOTPATH_INJECT_US) to validate the harness detects a known
/// latency regression. None in every real run.
pub inject_us: Option<u64>,
}
/// Result of one interleaved A/B service compare.
pub struct CompareResult {
pub ci50: RatioCi,
pub ci99: RatioCi,
pub base_p50: Vec<f64>,
pub cand_p50: Vec<f64>,
pub base_p99: Vec<f64>,
pub cand_p99: Vec<f64>,
pub drop_rate: f64,
}
/// Interleaved A/B: `rounds` alternating baseline/candidate sessions,
/// each yielding one p50 and one p99; bootstrap ratio-CI over the rounds.
#[allow(clippy::too_many_arguments)]
pub fn run_compare(
base: &ServiceCfg,
cand: &ServiceCfg,
rounds: usize,
bootstrap_iters: usize,
confidence: f64,
seed: u64,
mut on_round: impl FnMut(usize, f64, f64, f64, f64),
) -> Result<CompareResult> {
let (mut b50, mut b99, mut c50, mut c99) = (Vec::new(), Vec::new(), Vec::new(), Vec::new());
let mut dropped = 0usize;
for k in 0..rounds {
let bs = run_session(base, free_port()?)?;
let cs = run_session(cand, free_port()?)?;
dropped += bs.dropped + cs.dropped;
let (bp50, bp99) = (
percentile(&bs.latencies, 0.50),
percentile(&bs.latencies, 0.99),
);
let (cp50, cp99) = (
percentile(&cs.latencies, 0.50),
percentile(&cs.latencies, 0.99),
);
b50.push(bp50);
b99.push(bp99);
c50.push(cp50);
c99.push(cp99);
on_round(k, bp50, bp99, cp50, cp99);
}
let total = rounds * base.count * 2;
let drop_rate = dropped as f64 / total as f64;
let ci50 = stats::bootstrap_ratio_ci(&b50, &c50, bootstrap_iters, confidence, seed);
let ci99 = stats::bootstrap_ratio_ci(&b99, &c99, bootstrap_iters, confidence, seed);
Ok(CompareResult {
ci50,
ci99,
base_p50: b50,
cand_p50: c50,
base_p99: b99,
cand_p99: c99,
drop_rate,
})
}
pub struct SessionResult {
pub latencies: Vec<f64>,
pub dropped: usize,
}
/// Exact percentile (0.0–1.0) of a sorted-in-place copy.
pub fn percentile(latencies: &[f64], q: f64) -> f64 {
if latencies.is_empty() {
return f64::NAN;
}
let mut xs = latencies.to_vec();
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
let idx = ((xs.len() as f64 - 1.0) * q).round() as usize;
xs[idx.min(xs.len() - 1)]
}
fn spawn_server(cfg: &ServiceCfg, port: u16) -> Result<Child> {
let inject = match cfg.inject_us {
Some(us) => format!("HOTPATH_INJECT_US={us} "),
None => String::new(),
};
let cmd = format!(
"{inject}{pin} {bin} {port} {doc} {iters}",
pin = cfg.pin_prefix,
bin = cfg.server_bin,
doc = cfg.doc,
iters = cfg.iters,
);
let mut child = Command::new("sh")
.arg("-c")
.arg(cmd.trim())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()?;
// Block until the server prints "READY <port>".
let stdout = child
.stdout
.take()
.ok_or_else(|| anyhow!("no server stdout"))?;
let mut reader = BufReader::new(stdout);
let mut line = String::new();
reader.read_line(&mut line)?;
ensure!(
line.starts_with("READY"),
"server did not signal READY (got {line:?})"
);
Ok(child)
}
#[cfg(unix)]
fn set_linger_zero(s: &TcpStream) {
// SO_LINGER with a 0 timeout → RST on close instead of FIN, so the
// tens of thousands of short client connections a load session opens
// don't pile up in TIME_WAIT and exhaust the ephemeral port range.
// std::net::TcpStream::set_linger is still unstable, so go through
// setsockopt directly. The response is fully read before close, so the
// RST is harmless.
use std::os::unix::io::AsRawFd;
let l = libc::linger {
l_onoff: 1,
l_linger: 0,
};
unsafe {
libc::setsockopt(
s.as_raw_fd(),
libc::SOL_SOCKET,
libc::SO_LINGER,
&l as *const libc::linger as *const libc::c_void,
std::mem::size_of::<libc::linger>() as libc::socklen_t,
);
}
}
#[cfg(not(unix))]
fn set_linger_zero(_s: &TcpStream) {
// Service mode is POSIX-only at runtime (`sh`, taskset); this stub
// only keeps the crate compiling on non-Unix hosts.
}
fn one_request(port: u16) -> std::io::Result<()> {
let mut s = TcpStream::connect((Ipv4Addr::LOCALHOST, port))?;
s.set_nodelay(true)?;
set_linger_zero(&s);
s.write_all(b"GET / HTTP/1.1\r\nHost: x\r\n\r\n")?;
let mut buf = [0u8; 512];
// Read the full response (server sends Content-Length + Connection: close).
loop {
match s.read(&mut buf) {
Ok(0) => break,
Ok(_) => {}
Err(e) => return Err(e),
}
}
Ok(())
}
/// One open-loop load session against a freshly spawned server.
pub fn run_session(cfg: &ServiceCfg, port: u16) -> Result<SessionResult> {
ensure!(
cfg.rate.is_finite() && cfg.rate > 0.0,
"arrival rate must be finite and positive (got {})",
cfg.rate
);
ensure!(
cfg.count > 0 && cfg.workers > 0,
"count and workers must be > 0"
);
let mut child = spawn_server(cfg, port)?;
// Warm up closed-loop (untimed) so the first timed request doesn't eat
// page faults / cold caches.
for _ in 0..cfg.warmup {
let _ = one_request(port);
}
let period = Duration::from_secs_f64(1.0 / cfg.rate);
let start = Instant::now();
let idx = Arc::new(AtomicUsize::new(0));
let out = Arc::new(Mutex::new((Vec::<f64>::with_capacity(cfg.count), 0usize)));
let count = cfg.count;
let mut handles = Vec::new();
for _ in 0..cfg.workers {
let idx = idx.clone();
let out = out.clone();
handles.push(std::thread::spawn(move || {
let mut local = Vec::new();
let mut dropped = 0usize;
loop {
let i = idx.fetch_add(1, Ordering::Relaxed);
if i >= count {
break;
}
let target = start + period * (i as u32);
let now = Instant::now();
if now < target {
std::thread::sleep(target - now);
}
// Latency from the INTENDED send time — the CO correction.
match one_request(port) {
Ok(()) => local.push(Instant::now().duration_since(target).as_secs_f64()),
Err(_) => dropped += 1,
}
}
let mut g = out.lock().unwrap();
g.0.extend(local);
g.1 += dropped;
}));
}
for h in handles {
let _ = h.join();
}
let _ = child.kill();
let _ = child.wait();
let (latencies, dropped) = Arc::try_unwrap(out).unwrap().into_inner().unwrap();
Ok(SessionResult { latencies, dropped })
}
/// Pick a free loopback TCP port (small race between probe-close and the
/// server bind; harmless on loopback with SO_REUSEADDR).
pub fn free_port() -> Result<u16> {
let l = std::net::TcpListener::bind((Ipv4Addr::LOCALHOST, 0))?;
Ok(l.local_addr()?.port())
}