mirror of
https://github.com/bdbrown4/peltier.io.git
synced 2026-11-07 19:08:27 +00:00
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>
265 lines
8.7 KiB
Rust
265 lines
8.7 KiB
Rust
//! 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())
|
||
}
|