mirror of
https://github.com/bdbrown4/peltier.io.git
synced 2026-11-07 10:58:28 +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>
219 lines
7.5 KiB
Rust
219 lines
7.5 KiB
Rust
//! Opt-in `perf stat` PMU counter capture. Diagnostics only — counters
|
|
//! never feed the accept decision (wall time does, SPEC §3.1). Each
|
|
//! timed run is wrapped `perf stat -x, -e <events> -o <tmpfile> -- sh -c
|
|
//! <cmd>`; the CSV file is parsed after every run and per-side medians
|
|
//! are printed with the standard caveats. Unix-only at runtime; the
|
|
//! parser is portable and unit-tested.
|
|
|
|
use crate::exec::MeasuredSamples;
|
|
use crate::schedule::{interleaved, Side};
|
|
use crate::stats;
|
|
use anyhow::Context;
|
|
use std::path::Path;
|
|
use std::process::{Command, Stdio};
|
|
use std::time::Instant;
|
|
|
|
pub const PERF_EVENTS: [&str; 4] = ["cycles", "instructions", "cache-misses", "branch-misses"];
|
|
|
|
/// One side's counter samples: `samples[i]` holds one value per measured
|
|
/// run for `PERF_EVENTS[i]`; runs where perf reported `<not counted>` or
|
|
/// `<not supported>` contribute nothing.
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct PerfSide {
|
|
pub samples: [Vec<u64>; 4],
|
|
}
|
|
|
|
#[derive(Debug, Clone, Default)]
|
|
pub struct PerfReport {
|
|
pub baseline: PerfSide,
|
|
pub candidate: PerfSide,
|
|
}
|
|
|
|
/// Parse `perf stat -x,` CSV: field 0 is the counter value, field 2 the
|
|
/// event name (perf may append a modifier, e.g. `cycles:u`). Comment
|
|
/// lines, blank lines, and non-numeric values (`<not counted>`,
|
|
/// `<not supported>`) are skipped. Returns (event, count) in file order.
|
|
pub fn parse_perf_csv(text: &str) -> Vec<(String, u64)> {
|
|
let mut out = Vec::new();
|
|
for line in text.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') {
|
|
continue;
|
|
}
|
|
let mut fields = line.split(',');
|
|
let value = fields.next().unwrap_or("");
|
|
let _unit = fields.next();
|
|
let Some(event) = fields.next() else {
|
|
continue;
|
|
};
|
|
let Ok(v) = value.trim().parse::<u64>() else {
|
|
continue;
|
|
};
|
|
out.push((event.trim().to_string(), v));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// Index into PERF_EVENTS, tolerating a perf modifier suffix
|
|
/// (`cycles:u` when perf_event_paranoid restricts kernel counting).
|
|
fn event_index(event: &str) -> Option<usize> {
|
|
PERF_EVENTS.iter().position(|name| {
|
|
event == *name || (event.starts_with(name) && event[name.len()..].starts_with(':'))
|
|
})
|
|
}
|
|
|
|
impl PerfSide {
|
|
fn absorb(&mut self, parsed: &[(String, u64)]) {
|
|
for (event, value) in parsed {
|
|
if let Some(i) = event_index(event) {
|
|
self.samples[i].push(*value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PerfReport {
|
|
/// Per-side medians, each with its command (workload) and run count.
|
|
pub fn print(&self, baseline_cmd: &str, candidate_cmd: &str) {
|
|
println!("perf-stat per-side medians:");
|
|
for (label, cmd, side) in [
|
|
("baseline ", baseline_cmd, &self.baseline),
|
|
("candidate", candidate_cmd, &self.candidate),
|
|
] {
|
|
let cols: Vec<String> = PERF_EVENTS
|
|
.iter()
|
|
.zip(&side.samples)
|
|
.map(|(name, xs)| {
|
|
if xs.is_empty() {
|
|
format!("{name}=n/a")
|
|
} else {
|
|
let v: Vec<f64> = xs.iter().map(|&x| x as f64).collect();
|
|
format!(
|
|
"{name}={:.0} (median of {} runs)",
|
|
stats::median(&v),
|
|
xs.len()
|
|
)
|
|
}
|
|
})
|
|
.collect();
|
|
println!(" {label} `{cmd}`: {}", cols.join(", "));
|
|
}
|
|
println!(
|
|
" note: PMU counters unavailable in most VMs — flag is opt-in diagnostics, \
|
|
not part of the accept decision"
|
|
);
|
|
}
|
|
}
|
|
|
|
fn time_once_perf(shell_cmd: &str, out_file: &Path) -> anyhow::Result<(f64, Vec<(String, u64)>)> {
|
|
let start = Instant::now();
|
|
let status = Command::new("perf")
|
|
.args(["stat", "-x,", "-e", &PERF_EVENTS.join(",")])
|
|
.arg("-o")
|
|
.arg(out_file)
|
|
.args(["--", "sh", "-c", shell_cmd])
|
|
.stdout(Stdio::null())
|
|
.stderr(Stdio::null())
|
|
.status()
|
|
.with_context(|| format!("failed to spawn perf stat for: {shell_cmd}"))?;
|
|
let elapsed = start.elapsed().as_secs_f64();
|
|
anyhow::ensure!(
|
|
status.success(),
|
|
"perf stat run exited nonzero: {shell_cmd}"
|
|
);
|
|
let text = std::fs::read_to_string(out_file)
|
|
.with_context(|| format!("perf stat output missing: {}", out_file.display()))?;
|
|
Ok((elapsed, parse_perf_csv(&text)))
|
|
}
|
|
|
|
/// Interleaved A/B with each timed run wrapped in `perf stat`. The perf
|
|
/// wrapper overhead lands on both sides identically, but this path is
|
|
/// diagnostics only — verdicts always measure through the unwrapped
|
|
/// `exec::run_interleaved`.
|
|
pub fn run_interleaved_perf(
|
|
baseline_cmd: &str,
|
|
candidate_cmd: &str,
|
|
runs_per_side: usize,
|
|
warmup: usize,
|
|
) -> anyhow::Result<(MeasuredSamples, PerfReport)> {
|
|
anyhow::ensure!(cfg!(unix), "--perf-stat requires a Unix host with perf(1)");
|
|
let tmp = std::env::temp_dir().join(format!("bench-runner-perf-{}.csv", std::process::id()));
|
|
let mut samples = MeasuredSamples {
|
|
baseline_s: Vec::with_capacity(runs_per_side),
|
|
candidate_s: Vec::with_capacity(runs_per_side),
|
|
baseline_max_rss_kib: Vec::new(),
|
|
candidate_max_rss_kib: Vec::new(),
|
|
};
|
|
let mut report = PerfReport::default();
|
|
for (side, measured) in interleaved(runs_per_side, warmup) {
|
|
let cmd = match side {
|
|
Side::Baseline => baseline_cmd,
|
|
Side::Candidate => candidate_cmd,
|
|
};
|
|
let (elapsed, parsed) = time_once_perf(cmd, &tmp)?;
|
|
if measured {
|
|
match side {
|
|
Side::Baseline => {
|
|
samples.baseline_s.push(elapsed);
|
|
report.baseline.absorb(&parsed);
|
|
}
|
|
Side::Candidate => {
|
|
samples.candidate_s.push(elapsed);
|
|
report.candidate.absorb(&parsed);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let _ = std::fs::remove_file(&tmp);
|
|
Ok((samples, report))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const CANNED: &str = "\
|
|
# started on Mon Jul 13 12:00:00 2026
|
|
|
|
123456789,,cycles,401000000,100.00,,
|
|
234567890,,instructions:u,401000000,100.00,1.90,insn per cycle
|
|
<not supported>,,cache-misses,0,100.00,,
|
|
<not counted>,,branch-misses,0,0.00,,
|
|
garbage line without commas
|
|
999,,unrelated-event,1,100.00,,
|
|
";
|
|
|
|
#[test]
|
|
fn parses_values_and_skips_junk() {
|
|
let parsed = parse_perf_csv(CANNED);
|
|
assert_eq!(
|
|
parsed,
|
|
vec![
|
|
("cycles".to_string(), 123_456_789),
|
|
("instructions:u".to_string(), 234_567_890),
|
|
("unrelated-event".to_string(), 999),
|
|
]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn event_index_tolerates_modifier_suffix_only() {
|
|
assert_eq!(event_index("cycles"), Some(0));
|
|
assert_eq!(event_index("instructions:u"), Some(1));
|
|
assert_eq!(event_index("cache-misses"), Some(2));
|
|
assert_eq!(event_index("cycle"), None);
|
|
assert_eq!(event_index("cycles_total"), None);
|
|
assert_eq!(event_index("unrelated-event"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn absorb_buckets_by_event() {
|
|
let mut side = PerfSide::default();
|
|
side.absorb(&parse_perf_csv(CANNED));
|
|
assert_eq!(side.samples[0], vec![123_456_789]);
|
|
assert_eq!(side.samples[1], vec![234_567_890]);
|
|
assert!(side.samples[2].is_empty(), "<not supported> is skipped");
|
|
assert!(side.samples[3].is_empty(), "<not counted> is skipped");
|
|
}
|
|
}
|