josie / alder-tools

chore: move LLM memory files to .LLM_Memory/ (ignored), commit current work

00a524b0474554cdcbb346dfbe4aeedd2f93f315
josie <josie@example.com> · 2026-10-05T21:04 · browse files at this commit

parents: af309d6

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..588abe1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+# LLM project memory — local only, never committed
+.LLM_Memory/
+
+# Rust build artifacts
+target/
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..14b9774
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,7 @@
+This is Slop License
+
+I made this with LLMs. As such, I claim no ownership and provide no warranty. 
+
+Do whatever the fuck you want with this repo. 
+
+
diff --git a/alderbench/Cargo.lock b/alderbench/Cargo.lock
index a2af345..1d1bf0e 100644
--- a/alderbench/Cargo.lock
+++ b/alderbench/Cargo.lock
@@ -2,14 +2,6 @@
 # It is not intended for manual editing.
 version = 4
 
-[[package]]
-name = "alderbench"
-version = "0.1.0"
-dependencies = [
- "crossterm",
- "ratatui",
-]
-
 [[package]]
 name = "aho-corasick"
 version = "1.1.5"
@@ -19,6 +11,14 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "alderbench"
+version = "1.0.0"
+dependencies = [
+ "crossterm",
+ "ratatui",
+]
+
 [[package]]
 name = "allocator-api2"
 version = "0.2.21"
diff --git a/alderbench/Cargo.toml b/alderbench/Cargo.toml
index 16e8b50..255c8c4 100644
--- a/alderbench/Cargo.toml
+++ b/alderbench/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "alderbench"
-version = "0.1.0"
+version = "1.0.0"
 edition = "2021"
 
 [dependencies]
diff --git a/alderbench/Makefile b/alderbench/Makefile
new file mode 100644
index 0000000..b0de8cf
--- /dev/null
+++ b/alderbench/Makefile
@@ -0,0 +1,30 @@
+# alderbench — ISA-cycling benchmark + stress sweep. Gentoo-friendly:
+# respects PREFIX, DESTDIR. `make check` runs fmt+clippy+test.
+PREFIX  ?= /usr/local
+SYSCONF ?= /etc
+
+BIN  := $(DESTDIR)$(PREFIX)/bin/alderbench
+CONF := $(DESTDIR)$(SYSCONF)/alderbench/alderbench.conf
+
+.PHONY: all build check install uninstall clean
+
+all: build
+
+build:
+	cargo build --release
+
+check:
+	cargo fmt --check
+	cargo clippy --all-targets -- -D warnings
+	cargo test
+
+install: build
+	install -Dm755 target/release/alderbench $(BIN)
+	install -Dm644 alderbench.conf $(CONF)
+
+uninstall:
+	rm -f $(BIN) $(CONF)
+	rmdir -p --ignore-fail-on-non-empty $(CONF) 2>/dev/null || true
+
+clean:
+	cargo clean
diff --git a/alderbench/README.md b/alderbench/README.md
index 7b5e2c8..7c817d2 100644
--- a/alderbench/README.md
+++ b/alderbench/README.md
@@ -2,70 +2,111 @@
 
 Sister app to [aldermon](../aldermon) for the same Alder Lake box
 (i5-12600KF, ASRock Z690M ITX/ax). Runs **comparative benchmarks and CPU
-stress tests** across stock vs OC/UV configurations, with a live TUI and
-a JSON report file on exit.
+stress tests** across stock vs OC/UV configurations, cycling instruction
+sets so each clock rail (integer / SSE / AVX2) is tested separately —
+"OCCT but it cycles ISAs", with per-ISA scores written to JSON reports.
 
-Standalone — no shared code with aldermon. Sensor conventions and the
-config parser shape are mirrored, not linked.
+Standalone — no shared code with aldermon. aldermon owns the live
+sensors (clocks/temps/power); alderbench owns the workloads + scores.
+Run them side by side during a sweep to watch what the load is doing.
 
 ## Usage
 
 ```sh
-cargo run --release                # run configured workloads, live TUI (default)
-cargo run -- --report runs/foo.json   # print a past run's human summary
-cargo run -- --list                # enumerate available workloads
+alderbench                     # TUI picker: Stability | Bench | Core-cycle | Custom
+alderbench --tui               # TUI sweep straight from the config (no picker)
+alderbench --preset stability  # 1-hour ISA-cycling stability sweep (TUI)
+alderbench --preset bench      # comparative scores sweep (TUI)
+alderbench --preset core-cycle # per-core single-core stability cycle (TUI)
+alderbench --headless          # scripted sweep, no TUI (CI / scripts)
+alderbench --smoke-workload 7zip-bench   # run one workload 3s, print verdict + score
+alderbench --report runs/1756-c2c-latency.json  # print a past run's summary
+alderbench --list              # enumerate workload names
+alderbench --help              # usage
 ```
 
-The TUI needs a real terminal (pty); it will not run on redirected output.
+The TUI needs a real terminal; `--headless` covers scripts.
 
 ## Workloads
 
-v1 drives **stress-ng** (`/usr/bin/stress-ng`, unprivileged for CPU
-classes) for stability testing — CPU stressors at a configurable core
-count and duration, with per-tick sensor traces (vCore, package power,
-package temp, peak clock) captured throughout. Stability verdict per run:
-clean / WHEA seen / throttle seen / crash.
-
-Additional workloads (mprime, y-cruncher, phoronix-test-suite) are
-pluggable via the `Workload` trait but not yet implemented — see
-`to-do.md`.
+| name             | what it does                                        | score            |
+|------------------|-----------------------------------------------------|------------------|
+| `stress-ng-cpu`  | integer stressor, all `--cpu-method`s (stability)   | verdict only     |
+| `y-cruncher-sse` | SSE4.2 FFT stress via the SNB variant binary        | bits/sec per algo|
+| `y-cruncher-avx2`| AVX2 FFT stress via the HSW variant binary          | bits/sec per algo|
+| `7zip-bench`     | `7z b` compression benchmark                        | MIPS rating      |
+| `c2c-latency`    | cache-line ping-pong across every pinned cpu pair   | ns/hop per pair  |
+| `dram-latency`   | pointer-chase at L1/L2/L3/DRAM-sized working sets   | ns/access per size |
+
+Verdicts come from each workload's own self-check (y-cruncher "Passed",
+7z exit code, ping-pong sequence integrity, chase cycle integrity) —
+that's the stability signal per ISA rail. `c2c`/`dram` are in-process
+(zero-dep: raw `sched_setaffinity` + `Instant`), and double as
+coherency/memory stability checks.
+
+Requirements: `stress-ng` + `7z` on PATH. y-cruncher: download the
+static build and unpack it under `./bin/` (gitignored) — alderbench
+invokes the per-microarchitecture variant binaries (`Binaries/11-SNB ~
+Hina` for SSE, `13-HSW ~ Airi` for AVX2) directly, never the wrapper.
+`minecraft-server` is planned; the run loop skips it with a warning.
+
+## TUI keys
+
+Picker: `j/k`/arrows navigate, `Enter` starts, `q` quits. Editor: `j/k`
+rows, `←/→` fields, `Enter` on Name cycles workloads, on Cores cycles
+`0-15`/`0-11`/`12-15`, on Duration/Cooldown/Loops opens numeric input;
+`a`/`d` add/delete rows, `</>` reorder. During a sweep `q` cancels —
+while a workload runs the panel heartbeat-refreshes every few seconds
+(never at render-loop frequency, so it won't steal CPU from single-core
+runs); workloads that outrun their estimate (7z's fixed passes) say so
+explicitly. Watch aldermon for live load.
 
 ## Config
 
-`alderbench.conf` is read from the working directory, then
-`~/.config/alderbench/alderbench.conf`. Simple `KEY = VALUE` lines; `#`
-comments; unknown keys ignored. All values optional — defaults target the
-i5-12600KF / Z690M ITX box.
+`alderbench.conf` is searched at `./` → `~/.config/alderbench/` →
+`/etc/alderbench/` (first match wins). Simple `KEY = VALUE` lines; `#`
+comments; unknown keys ignored; out-of-range numerics rejected.
 
 ```ini
-workloads     = stress-ng-cpu     # comma list (v1: only stress-ng-cpu)
-duration_secs = 60                # per-workload run length
-poll_ms       = 250               # sensor sample interval
-cool_secs     = 15                # cooldown between workloads
-vcore_limit   = 1.403             # shown as marker on the vCore hero
-temp_crit     = 95.0              # throttle/crit flag threshold
-report_dir    = ./runs            # where JSON reports are written
+poll_ms      = 250          # tick interval (50..60000)
+cool_secs    = 15           # cooldown between workloads (0..3600)
+loop_count   = 1            # sweep repeats; 0 = infinite
+report_dir   = runs         # where JSON reports go
+bin_dir      = bin          # where the y-cruncher bundle lives
+
+# Either a comma list (names inherit per-workload defaults)…
+workloads = stress-ng-cpu, y-cruncher-sse, 7zip-bench
+
+# …or per-entry blocks:
+workload.1.name           = y-cruncher-avx2
+workload.1.duration_secs  = 300        # estimate, not a hard stop
+workload.1.cores          = 0-11       # taskset-style range: 0-11 P-only, 12-15 E-only
 ```
 
+`duration_secs` is a display estimate: workloads run until they finish
+themselves (self-terminating stressors) or you quit; `7z b`'s fixed
+passes may overrun it.
+
 ## Reports
 
-Each run writes `./runs/<timestamp>-<workload>.json` (`schema_version: 1`):
-run metadata, per-tick sensor samples, workload exit status, and a
-stability verdict. A human summary is also printed to stdout after the
-TUI exits. Incremental flush — a crash mid-run still leaves a partial
-report.
+One JSON file per finished workload:
+`runs/<unix-seconds>-<workload>.json`, schema version 2:
 
-## Package power (RAPL)
+```json
+{"schema_version":2,"workload":"c2c-latency","params":"30s cores=0-15",
+ "started":1758366463,"isa_class":"other","verdict":{"kind":"clean"},
+ "score":{"kind":"c2c","pairs":[{"a":0,"b":1,"ns":28.0}, ...]}}
+```
 
-`energy_uj` is root-only on this kernel. To read watts as a normal user,
-install aldermon's udev rule once (it covers both apps — same box, same
-device):
+`alderbench --report <path>` prints a one-line human summary of any of
+these. Diff report dirs across OC configs to compare scores.
+
+## Install
 
 ```sh
-doas ../aldermon/powercap-pl-rules.sh
-doas usermod -aG adm $USER   # then re-login
+make check     # fmt + clippy + tests
+make install   # DESTDIR/PREFIX aware; bin → $(PREFIX)/bin,
+               # conf → /etc/alderbench/alderbench.conf
 ```
 
-## Status
-
-Scaffold. See `to-do.md` for the build order.
\ No newline at end of file
+Gentoo-friendly: `make install DESTDIR="$D" PREFIX=/usr` in an ebuild.
diff --git a/alderbench/src/config.rs b/alderbench/src/config.rs
index 19cb0a7..78c2f74 100644
--- a/alderbench/src/config.rs
+++ b/alderbench/src/config.rs
@@ -83,6 +83,16 @@ fn spec(name: &str, duration_secs: u64, cores: &str) -> WorkloadSpec {
     }
 }
 
+/// Parse an unsigned numeric value; out-of-range or junk keeps the
+/// current value. Mirrors aldermon's parse_in so a hand-edited conf
+/// can't produce degenerate values (zero-length polls, absurd durations).
+fn parse_u64_in(val: &str, cur: u64, range: std::ops::RangeInclusive<u64>) -> u64 {
+    match val.parse::<u64>() {
+        Ok(v) if range.contains(&v) => v,
+        _ => cur,
+    }
+}
+
 /// Parse one config line over a Config (mutates in place). Shape matches
 /// aldermon's apply_line: strip `#` comments, trim, split on `=`, match
 /// the key. Unknown keys silently ignored.
@@ -97,8 +107,8 @@ fn apply_line(cfg: &mut Config, line: &str) {
     let key = key.trim();
     let val = val.trim();
     match key {
-        "poll_ms" => cfg.poll_ms = val.parse().unwrap_or(cfg.poll_ms).max(50),
-        "cool_secs" => cfg.cool_secs = val.parse().unwrap_or(cfg.cool_secs),
+        "poll_ms" => cfg.poll_ms = parse_u64_in(val, cfg.poll_ms, 50..=60_000),
+        "cool_secs" => cfg.cool_secs = parse_u64_in(val, cfg.cool_secs, 0..=3600),
         "loop_count" => cfg.loop_count = val.parse().unwrap_or(cfg.loop_count),
         "report_dir" => cfg.report_dir = PathBuf::from(val),
         "bin_dir" => cfg.bin_dir = PathBuf::from(val),
@@ -137,18 +147,28 @@ fn parse_workload_list(val: &str) -> Vec<WorkloadSpec> {
         .collect()
 }
 
+/// Search order: ./alderbench.conf (dev tree) → ~/.config/alderbench/
+/// (user override) → /etc/alderbench/ (system default, installed).
+/// First readable file wins; no merging.
 pub fn load() -> Config {
     let mut cfg = Config::default();
     let candidates = [
         Some(PathBuf::from("alderbench.conf")),
         dirs_home().map(|h| h.join(".config/alderbench/alderbench.conf")),
+        Some(PathBuf::from("/etc/alderbench/alderbench.conf")),
     ];
     let mut indexed: Vec<WorkloadSpec> = Vec::new();
     let mut saw_indexed = false;
     for path in candidates.into_iter().flatten() {
         if let Ok(text) = std::fs::read_to_string(&path) {
             for line in text.lines() {
-                if let Some(rest) = line.split('#').next().unwrap_or("").trim().strip_prefix("workload.") {
+                if let Some(rest) = line
+                    .split('#')
+                    .next()
+                    .unwrap_or("")
+                    .trim()
+                    .strip_prefix("workload.")
+                {
                     if apply_indexed(&mut indexed, rest) {
                         saw_indexed = true;
                     }
@@ -194,7 +214,7 @@ fn apply_indexed(list: &mut Vec<WorkloadSpec>, rest: &str) -> bool {
     let w = &mut list[idx - 1];
     match field {
         "name" => w.name = val.to_string(),
-        "duration_secs" => w.duration_secs = val.parse().unwrap_or(w.duration_secs),
+        "duration_secs" => w.duration_secs = parse_u64_in(val, w.duration_secs, 1..=86_400),
         "cores" => w.cores = val.to_string(),
         _ => return false,
     }
@@ -229,10 +249,29 @@ mod tests {
     }
 
     #[test]
-    fn poll_ms_floors_at_50() {
+    fn poll_ms_rejects_out_of_range() {
         let mut cfg = Config::default();
         apply_line(&mut cfg, "poll_ms = 1");
-        assert_eq!(cfg.poll_ms, 50);
+        assert_eq!(cfg.poll_ms, 250); // below floor → keep current
+        apply_line(&mut cfg, "poll_ms = 99999999");
+        assert_eq!(cfg.poll_ms, 250); // above ceiling → keep current
+        apply_line(&mut cfg, "poll_ms = 100");
+        assert_eq!(cfg.poll_ms, 100);
+    }
+
+    #[test]
+    fn rejects_out_of_range() {
+        let mut cfg = Config::default();
+        apply_line(&mut cfg, "cool_secs = 999999");
+        assert_eq!(cfg.cool_secs, 15);
+        let mut indexed: Vec<WorkloadSpec> = Vec::new();
+        assert!(apply_indexed(&mut indexed, "1.name = stress-ng-cpu"));
+        assert!(apply_indexed(&mut indexed, "1.duration_secs = 0"));
+        assert_eq!(indexed[0].duration_secs, 60); // below floor → keep
+        assert!(apply_indexed(&mut indexed, "1.duration_secs = 99999999"));
+        assert_eq!(indexed[0].duration_secs, 60); // above ceiling → keep
+        assert!(apply_indexed(&mut indexed, "1.duration_secs = 90"));
+        assert_eq!(indexed[0].duration_secs, 90);
     }
 
     #[test]
@@ -298,4 +337,4 @@ mod tests {
         assert!(!apply_indexed(&mut indexed, "0.name = bad"));
         assert!(indexed.is_empty());
     }
-}
\ No newline at end of file
+}
diff --git a/alderbench/src/inproc.rs b/alderbench/src/inproc.rs
new file mode 100644
index 0000000..dc9ab89
--- /dev/null
+++ b/alderbench/src/inproc.rs
@@ -0,0 +1,584 @@
+//! In-process workloads: c2c-latency (cache-line ping-pong between core
+//! pairs) and dram-latency (pointer-chase through the memory hierarchy).
+//! Zero-dep: CPU pinning goes through a raw `sched_setaffinity` syscall
+//! (x86_64 nr 203); timing uses `std::time::Instant` (clock_gettime via
+//! the vDSO — ns resolution, no TSC calibration needed).
+//!
+//! Both workloads measure while pinning threads, so they double as
+//! coherency/memory stability checks: the ping-pong verifies every
+//! cache-line handoff carries the exact expected value (a coherence or
+//! clock failure shows up as a skipped/duplicated sequence number =
+//! Verdict::Failed), and the pointer-chase verifies the chase walks one
+//! full cycle of the randomized chain (a flipped address bit breaks the
+//! cycle). Verdict is the stability signal; the latencies are the score.
+
+use std::io;
+use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use std::sync::{Arc, Mutex};
+use std::thread;
+use std::time::{Duration, Instant};
+
+use crate::workload::{C2cPair, DramLevel, Score, Verdict, Workload};
+
+/// Parse a stress-ng `--taskset`-style cpu list ("0-11", "12-15",
+/// "0-15", "3,7", ""/all = every logical cpu). Out-of-range entries are
+/// clamped to the CPU count; invalid tokens are skipped. Sorted + deduped.
+pub fn parse_cpu_list(cores: &str) -> Vec<usize> {
+    let nproc = thread::available_parallelism()
+        .map(|n| n.get())
+        .unwrap_or(1);
+    let trimmed = cores.trim();
+    if trimmed.is_empty() || trimmed.eq_ignore_ascii_case("all") {
+        return (0..nproc).collect();
+    }
+    let mut cpus = Vec::new();
+    for token in trimmed.split(',') {
+        let token = token.trim();
+        if let Some((a, b)) = token.split_once('-') {
+            if let (Ok(a), Ok(b)) = (a.trim().parse::<usize>(), b.trim().parse::<usize>()) {
+                for c in a..=b {
+                    if c < nproc {
+                        cpus.push(c);
+                    }
+                }
+            }
+        } else if let Ok(c) = token.parse::<usize>() {
+            if c < nproc {
+                cpus.push(c);
+            }
+        }
+    }
+    cpus.sort_unstable();
+    cpus.dedup();
+    cpus
+}
+
+/// Pin the CALLING thread to one logical cpu. Raw syscall — std doesn't
+/// expose sched_setaffinity and we don't want a libc dep. Returns false
+/// if pinning isn't available (non-x86_64) or the kernel rejected it.
+fn pin_self(cpu: usize) -> bool {
+    let words = cpu / 64 + 1;
+    let mut mask = vec![0u64; words];
+    mask[cpu / 64] |= 1u64 << (cpu % 64);
+    let ret =
+        unsafe { sched_setaffinity_syscall(0, words * std::mem::size_of::<u64>(), mask.as_ptr()) };
+    ret == 0
+}
+
+#[cfg(target_arch = "x86_64")]
+unsafe fn sched_setaffinity_syscall(pid: i32, len: usize, mask: *const u64) -> isize {
+    const SYS_SCHED_SETAFFINITY: isize = 203;
+    let ret: isize;
+    core::arch::asm!(
+        "syscall",
+        inlateout("rax") SYS_SCHED_SETAFFINITY => ret,
+        in("rdi") pid,
+        in("rsi") len,
+        in("rdx") mask,
+        lateout("rcx") _,
+        lateout("r11") _,
+        options(nostack)
+    );
+    ret
+}
+
+#[cfg(not(target_arch = "x86_64"))]
+unsafe fn sched_setaffinity_syscall(_pid: i32, _len: usize, _mask: *const u64) -> isize {
+    -1
+}
+
+// ---------------------------------------------------------------------------
+// Shared in-process runner skeleton
+// ---------------------------------------------------------------------------
+
+/// Measurement body: runs to completion, honors `cancel` between units of
+/// work, returns (verdict, score). `Stops early → (Stopped, None)`.
+type Worker = fn(duration_secs: u64, cpus: &[usize], cancel: &AtomicBool) -> (Verdict, Score);
+
+/// One struct for both in-process workloads: the trait plumbing (start /
+/// is_running / stop / wait / score) is identical; only the worker fn
+/// pointer + name differ.
+struct InProc {
+    name: &'static str,
+    duration_secs: u64,
+    cores: String,
+    worker: Worker,
+    handle: Option<thread::JoinHandle<()>>,
+    cancel: Arc<AtomicBool>,
+    done: Arc<AtomicBool>,
+    result: Arc<Mutex<Option<(Verdict, Score)>>>,
+}
+
+impl InProc {
+    fn new(name: &'static str, duration_secs: u64, cores: String, worker: Worker) -> Self {
+        InProc {
+            name,
+            duration_secs,
+            cores,
+            worker,
+            handle: None,
+            cancel: Arc::new(AtomicBool::new(false)),
+            done: Arc::new(AtomicBool::new(false)),
+            result: Arc::new(Mutex::new(None)),
+        }
+    }
+}
+
+impl Workload for InProc {
+    fn name(&self) -> &str {
+        self.name
+    }
+
+    fn params(&self) -> String {
+        format!("~{}s cores={}", self.duration_secs, self.cores)
+    }
+
+    fn start(&mut self) -> io::Result<()> {
+        self.cancel.store(false, Ordering::Relaxed);
+        self.done.store(false, Ordering::Relaxed);
+        *self.result.lock().unwrap() = None;
+        let duration = self.duration_secs;
+        let cores = self.cores.clone();
+        let cancel = Arc::clone(&self.cancel);
+        let done = Arc::clone(&self.done);
+        let result = Arc::clone(&self.result);
+        let worker = self.worker;
+        self.handle = Some(thread::spawn(move || {
+            let cpus = parse_cpu_list(&cores);
+            let out = worker(duration, &cpus, &cancel);
+            *result.lock().unwrap() = Some(out);
+            done.store(true, Ordering::Release);
+        }));
+        Ok(())
+    }
+
+    fn is_running(&mut self) -> bool {
+        !self.done.load(Ordering::Acquire)
+    }
+
+    fn stop(&mut self) -> io::Result<()> {
+        self.cancel.store(true, Ordering::Relaxed);
+        Ok(())
+    }
+
+    fn wait(&mut self) -> Verdict {
+        match self.handle.take() {
+            Some(handle) => {
+                if handle.join().is_err() {
+                    return Verdict::Error("measurement thread panicked".to_string());
+                }
+                let guard = self.result.lock().unwrap();
+                match &*guard {
+                    Some((v, _)) => v.clone(),
+                    None => Verdict::Stopped,
+                }
+            }
+            None => Verdict::Stopped,
+        }
+    }
+
+    fn score(&mut self) -> Score {
+        let guard = self.result.lock().unwrap();
+        match &*guard {
+            Some((_, s)) => s.clone(),
+            None => Score::None,
+        }
+    }
+}
+
+/// Public constructors used by workload::from_spec.
+pub fn c2c_latency(duration_secs: u64, cores: String) -> Box<dyn Workload> {
+    Box::new(InProc::new(
+        "c2c-latency",
+        duration_secs,
+        cores,
+        measure_c2c,
+    ))
+}
+
+pub fn dram_latency(duration_secs: u64, cores: String) -> Box<dyn Workload> {
+    Box::new(InProc::new(
+        "dram-latency",
+        duration_secs,
+        cores,
+        measure_dram,
+    ))
+}
+
+// ---------------------------------------------------------------------------
+// c2c-latency: cache-line ping-pong between pinned core pairs
+// ---------------------------------------------------------------------------
+
+/// Round trips per pair. ~2 ms of transfers per pair on this chip; the
+/// full upper triangle of 16 logical cpus (120 pairs) lands well under
+/// the 30 s default duration.
+const C2C_ROUND_TRIPS: u64 = 20_000;
+
+fn measure_c2c(_duration: u64, cpus: &[usize], cancel: &AtomicBool) -> (Verdict, Score) {
+    if cpus.len() < 2 {
+        return (
+            Verdict::Error("c2c-latency needs at least 2 logical cpus".to_string()),
+            Score::None,
+        );
+    }
+    let mut pairs = Vec::new();
+    for (ia, &a) in cpus.iter().enumerate() {
+        for &b in &cpus[ia + 1..] {
+            if cancel.load(Ordering::Relaxed) {
+                return (Verdict::Stopped, Score::None);
+            }
+            match ping_pong(a, b) {
+                Ok(ns_per_hop) => pairs.push(C2cPair {
+                    cpu_a: a,
+                    cpu_b: b,
+                    ns_per_hop,
+                }),
+                Err(v) => return (v, Score::None),
+            }
+        }
+    }
+    (Verdict::Clean, Score::C2c { pairs })
+}
+
+/// Ping-pong a cache line (one AtomicU64) between cpu `a` and cpu `b`
+/// for C2C_ROUND_TRIPS round trips. Each sequence value has exactly one
+/// writer, so a plain spin-load that sees a value past its expectation
+/// means the line was corrupted or the other side double-wrote — a
+/// coherency/clock failure, reported as Verdict::Failed.
+fn ping_pong(a: usize, b: usize) -> Result<f64, Verdict> {
+    let ball = Arc::new(AtomicU64::new(0));
+    let corrupt = Arc::new(AtomicBool::new(false));
+    let pin_fail = Arc::new(AtomicBool::new(false));
+
+    let ball_a = Arc::clone(&ball);
+    let ball_b = Arc::clone(&ball);
+    let corrupt_a = Arc::clone(&corrupt);
+    let corrupt_b = Arc::clone(&corrupt);
+    let pf_a = Arc::clone(&pin_fail);
+    let pf_b = Arc::clone(&pin_fail);
+
+    // Initiator: A. Stores 1, waits for 2, stores 3, … timing brackets
+    // the full 0 → 2N transfer sequence.
+    let tx = thread::spawn(move || {
+        if !pin_self(a) {
+            pf_a.store(true, Ordering::Relaxed);
+        }
+        let t0 = Instant::now();
+        for i in 0..C2C_ROUND_TRIPS {
+            let expect = 2 * i;
+            loop {
+                let v = ball_a.load(Ordering::Relaxed);
+                if v == expect {
+                    break;
+                }
+                if v > expect {
+                    corrupt_a.store(true, Ordering::Relaxed);
+                    return Duration::ZERO;
+                }
+                std::hint::spin_loop();
+            }
+            ball_a.store(expect + 1, Ordering::Relaxed);
+        }
+        // Wait for B's final store so the timing covers 2N transfers.
+        let final_expect = 2 * C2C_ROUND_TRIPS;
+        loop {
+            let v = ball_a.load(Ordering::Relaxed);
+            if v == final_expect {
+                break;
+            }
+            if v > final_expect {
+                corrupt_a.store(true, Ordering::Relaxed);
+                return Duration::ZERO;
+            }
+            std::hint::spin_loop();
+        }
+        t0.elapsed()
+    });
+    // Responder: B. Waits for 1, stores 2, waits for 3, …
+    let rx = thread::spawn(move || {
+        if !pin_self(b) {
+            pf_b.store(true, Ordering::Relaxed);
+        }
+        for i in 0..C2C_ROUND_TRIPS {
+            let expect = 2 * i + 1;
+            loop {
+                let v = ball_b.load(Ordering::Relaxed);
+                if v == expect {
+                    break;
+                }
+                if v > expect {
+                    corrupt_b.store(true, Ordering::Relaxed);
+                    return;
+                }
+                std::hint::spin_loop();
+            }
+            ball_b.store(expect + 1, Ordering::Relaxed);
+        }
+    });
+
+    let elapsed = match (tx.join(), rx.join()) {
+        (Ok(e), Ok(())) => e,
+        _ => return Err(Verdict::Error("ping-pong thread panicked".to_string())),
+    };
+    if pin_fail.load(Ordering::Relaxed) {
+        return Err(Verdict::Error(format!(
+            "can't pin cpus {a}/{b} (sched_setaffinity failed)"
+        )));
+    }
+    if corrupt.load(Ordering::Relaxed) {
+        return Err(Verdict::Failed(format!(
+            "cache-line handoff {a}<->{b} corrupted"
+        )));
+    }
+    let transfers = 2.0 * C2C_ROUND_TRIPS as f64;
+    Ok(elapsed.as_nanos() as f64 / transfers)
+}
+
+// ---------------------------------------------------------------------------
+// dram-latency: pointer-chase through the cache hierarchy
+// ---------------------------------------------------------------------------
+
+/// Chase buffers, bytes. Sizes straddle the 12600KF hierarchy: L1D 48K
+/// per P-core, L2 2MB per P-core, L3 20MB shared. 32K fits L1, 512K fits
+/// L2, 8M fits L3, 64M falls through to DRAM.
+const DRAM_SIZES: [u64; 4] = [32 * 1024, 512 * 1024, 8 * 1024 * 1024, 64 * 1024 * 1024];
+
+/// Minimum time per size so the slow levels (DRAM) get enough samples.
+const DRAM_MIN_PER_SIZE: Duration = Duration::from_millis(200);
+
+fn measure_dram(_duration: u64, cpus: &[usize], cancel: &AtomicBool) -> (Verdict, Score) {
+    let Some(&cpu) = cpus.first() else {
+        return (
+            Verdict::Error("dram-latency needs at least 1 cpu".to_string()),
+            Score::None,
+        );
+    };
+    // Runs on the InProc worker thread itself — pin it (the thread exits
+    // with the measurement, so no affinity restore needed).
+    if !pin_self(cpu) {
+        return (
+            Verdict::Error(format!("can't pin cpu {cpu} (sched_setaffinity failed)")),
+            Score::None,
+        );
+    }
+    let mut levels = Vec::new();
+    for &size in &DRAM_SIZES {
+        if cancel.load(Ordering::Relaxed) {
+            return (Verdict::Stopped, Score::None);
+        }
+        let lines = (size / 64) as usize;
+        let table = build_chase_table(lines);
+        match chase(&table, lines, cancel) {
+            Ok(ns) if ns.is_nan() => return (Verdict::Stopped, Score::None), // cancelled mid-size
+            Ok(ns) => levels.push(DramLevel {
+                size_kb: size / 1024,
+                ns_per_access: ns,
+            }),
+            Err(v) => return (v, Score::None),
+        }
+    }
+    (Verdict::Clean, Score::Dram { levels })
+}
+
+/// Random single cycle over `lines` cache lines, laid out in a u64 table
+/// with one u64 payload per 64-byte line. `table[line*8]` = next line.
+/// A cycle of length exactly `lines` starting at line 0.
+fn build_chase_table(lines: usize) -> Vec<u64> {
+    let mut order: Vec<u32> = (0..lines as u32).collect();
+    // Fisher-Yates with a xorshift RNG (deterministic per run — fine for
+    // a latency measurement; the chase only needs to be irregular).
+    let mut state: u64 = 0x9E3779B97F4A7C15 ^ lines as u64;
+    for i in (1..lines).rev() {
+        state ^= state << 13;
+        state ^= state >> 7;
+        state ^= state << 17;
+        let j = (state % (i as u64 + 1)) as usize;
+        order.swap(i, j);
+    }
+    // order[0] is the head (chase start). link[order[i]] = order[i+1].
+    let mut table = vec![0u64; lines * 8];
+    for i in 0..lines {
+        let line = order[i] as usize;
+        table[line * 8] = order[(i + 1) % lines] as u64;
+    }
+    table
+}
+
+/// Chase full laps of the cycle until DRAM_MIN_PER_SIZE elapsed (or
+/// cancel). Returns ns/access. The table is ONE cycle covering all
+/// `lines` nodes, so starting anywhere, exactly `lines` dependent loads
+/// must land back on the start — if a stored address is ever wrong
+/// (flipped bit), the lap check fails: that's the memory-stability
+/// self-check.
+fn chase(table: &[u64], lines: usize, cancel: &AtomicBool) -> Result<f64, Verdict> {
+    let mut idx = 0u64;
+    let mut laps = 0u64;
+    let t0 = Instant::now();
+    loop {
+        for _ in 0..lines {
+            idx = std::hint::black_box(table[idx as usize * 8]);
+        }
+        laps += 1;
+        if idx != 0 {
+            return Err(Verdict::Failed(
+                "pointer-chase left the cycle (memory fault?)".to_string(),
+            ));
+        }
+        if t0.elapsed() >= DRAM_MIN_PER_SIZE || laps > 100_000_000 {
+            break;
+        }
+        if cancel.load(Ordering::Relaxed) {
+            return Ok(f64::NAN); // ignored — caller checks cancel separately
+        }
+    }
+    let accesses = laps as f64 * lines as f64;
+    Ok(t0.elapsed().as_nanos() as f64 / accesses)
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn parse_cpu_list_forms() {
+        let n = thread::available_parallelism()
+            .map(|n| n.get())
+            .unwrap_or(1);
+        assert_eq!(parse_cpu_list("").len(), n);
+        assert_eq!(parse_cpu_list("all").len(), n);
+        assert_eq!(parse_cpu_list("2-4"), vec![2, 3, 4]);
+        assert_eq!(parse_cpu_list("3,1,2-2"), vec![1, 2, 3]); // sort + dedup
+        assert_eq!(parse_cpu_list("junk"), Vec::<usize>::new());
+        // Out-of-range clamped away (cpu id >= nproc dropped).
+        assert_eq!(parse_cpu_list("0-999").len(), n);
+    }
+
+    #[test]
+    fn chase_table_is_one_cycle() {
+        let lines = 4096;
+        let table = build_chase_table(lines);
+        let mut idx = 0u64;
+        for _ in 0..lines {
+            idx = table[idx as usize * 8];
+            assert!(idx < lines as u64);
+        }
+        assert_eq!(
+            idx, 0,
+            "single cycle must return to the start after `lines` hops"
+        );
+    }
+
+    #[test]
+    fn chase_small_table_measures_ns() {
+        let lines = 1024;
+        let table = build_chase_table(lines);
+        let cancel = AtomicBool::new(false);
+        let ns = chase(&table, lines, &cancel).unwrap();
+        // Only a floor + deadlock cap: absolute ns bounds are meaningless
+        // when the suite runs on a box under load (the dev machine's
+        // benchmarks), and the hierarchy assertions live in dram_e2e.
+        assert!(ns > 0.0 && ns < 1e6, "sanity range, got {ns}");
+    }
+
+    #[test]
+    fn chase_fails_on_desynced_lap() {
+        let lines = 1024;
+        let mut table = build_chase_table(lines);
+        // Rewire one edge to skip a node: the cycle splits, so a
+        // `lines`-hop lap from 0 no longer lands back on 0.
+        let a = table[0];
+        let b = table[a as usize * 8];
+        table[a as usize * 8] = table[b as usize * 8];
+        let cancel = AtomicBool::new(false);
+        let r = chase(&table, lines, &cancel);
+        assert!(matches!(r, Err(Verdict::Failed(_))), "got {r:?}");
+    }
+
+    #[test]
+    #[cfg_attr(not(target_arch = "x86_64"), ignore)] // pinning is x86_64-only here
+    fn ping_pong_between_two_cpus() {
+        let cpus = parse_cpu_list("0-15");
+        if cpus.len() < 2 {
+            return; // single-cpu box (container?) — nothing to test
+        }
+        let ns = ping_pong(cpus[0], cpus[1]).expect("clean handoff");
+        // Floor + deadlock cap only — a preempted responder (this test
+        // runs on a box that may be mid-benchmark) legitimately pushes
+        // handoffs way past the tens-of-ns idle range.
+        assert!(ns > 0.0 && ns < 1e7, "sanity range, got {ns}");
+    }
+
+    #[test]
+    #[cfg_attr(not(target_arch = "x86_64"), ignore)]
+    fn c2c_e2e_via_trait() {
+        use crate::config::WorkloadSpec;
+        let cpus = parse_cpu_list("0-15");
+        if cpus.len() < 2 {
+            return;
+        }
+        let spec = WorkloadSpec {
+            name: "c2c-latency".to_string(),
+            duration_secs: 1,
+            cores: "0-3".to_string(),
+        };
+        let mut w = crate::workload::from_spec(&spec, std::path::Path::new("bin")).unwrap();
+        assert_eq!(w.name(), "c2c-latency");
+        w.start().unwrap();
+        while w.is_running() {
+            thread::sleep(Duration::from_millis(25));
+        }
+        assert_eq!(w.wait(), Verdict::Clean);
+        match w.score() {
+            Score::C2c { pairs } => {
+                // 4 cpus → 6 upper-triangle pairs.
+                assert_eq!(pairs.len(), 6);
+                assert!(pairs.iter().all(|p| p.ns_per_hop > 0.0));
+            }
+            other => panic!("expected C2c score, got {other:?}"),
+        }
+    }
+
+    #[test]
+    #[cfg_attr(not(target_arch = "x86_64"), ignore)]
+    fn dram_e2e_via_trait() {
+        use crate::config::WorkloadSpec;
+        let spec = WorkloadSpec {
+            name: "dram-latency".to_string(),
+            duration_secs: 1,
+            cores: "0".to_string(),
+        };
+        let mut w = crate::workload::from_spec(&spec, std::path::Path::new("bin")).unwrap();
+        w.start().unwrap();
+        while w.is_running() {
+            thread::sleep(Duration::from_millis(50));
+        }
+        assert_eq!(w.wait(), Verdict::Clean);
+        match w.score() {
+            Score::Dram { levels } => {
+                assert_eq!(levels.len(), 4);
+                // DRAM (64 MiB) must be far slower than the L1-sized set.
+                assert!(levels[0].ns_per_access < levels[3].ns_per_access);
+                assert!(
+                    levels[3].ns_per_access > 30.0,
+                    "DRAM tier too fast: {levels:?}"
+                );
+            }
+            other => panic!("expected Dram score, got {other:?}"),
+        }
+    }
+
+    #[test]
+    fn stop_mid_run_yields_stopped() {
+        let cpus = parse_cpu_list("0-15");
+        if cpus.len() < 2 {
+            return;
+        }
+        let mut w = c2c_latency(3600, "0-15".to_string());
+        w.start().unwrap();
+        w.stop().unwrap();
+        while w.is_running() {
+            thread::sleep(Duration::from_millis(25));
+        }
+        assert_eq!(w.wait(), Verdict::Stopped);
+    }
+}
diff --git a/alderbench/src/main.rs b/alderbench/src/main.rs
index d75fc23..52a971e 100644
--- a/alderbench/src/main.rs
+++ b/alderbench/src/main.rs
@@ -1,51 +1,148 @@
+//! alderbench — CLI entry point + arg dispatch.
+//!
+//! Modes: no args = TUI picker (default); `--tui` = TUI with the loaded
+//! config; `--preset NAME` = TUI straight into a preset sweep;
+//! `--headless` = scripted sweep, stdout only; `--smoke-workload NAME` =
+//! run one workload 3s + print the verdict; `--report PATH` = print a
+//! past run's summary; `--list` = enumerate workloads.
+
 mod config;
+mod inproc;
 mod presets;
 mod report;
 mod run;
 mod ui;
 mod workload;
 
+use std::io::IsTerminal;
+
+const USAGE: &str = "\
+usage: alderbench [OPTION]...
+
+  (no option)              pick a preset or edit a custom sweep (TUI)
+  --tui                    TUI with the config as loaded (no picker)
+  --preset NAME            TUI straight into a preset: stability | bench |
+                           core-cycle
+  --headless               run the configured sweep without a TUI
+  --smoke-workload NAME    run one workload for 3s + print verdict/score
+  --report PATH            print the summary of a past run's JSON report
+  --list                   enumerate available workload names
+  --version                print version and exit
+  --help                   show this help";
+
+const OPTION_FLAGS: &[&str] = &[
+    "--tui",
+    "--preset",
+    "--headless",
+    "--smoke-workload",
+    "--report",
+    "--list",
+    "--version",
+    "--help",
+];
+
 fn main() {
-    let cfg = config::load();
     let args: Vec<String> = std::env::args().collect();
+    for a in &args[1..] {
+        if a.starts_with('-') && !OPTION_FLAGS.contains(&a.as_str()) {
+            // A leading-dash token that is a *value* (e.g. `--preset -x`)
+            // is caught here too — acceptable: our values never start
+            // with '-'.
+            eprintln!("alderbench: unknown option '{a}'\n{USAGE}");
+            std::process::exit(2);
+        }
+    }
+    if args.iter().any(|a| a == "--help") {
+        println!("{USAGE}");
+        return;
+    }
+    if args.iter().any(|a| a == "--version") {
+        println!("alderbench {}", env!("CARGO_PKG_VERSION"));
+        return;
+    }
+    if args.iter().any(|a| a == "--list") {
+        for name in presets::AVAILABLE_WORKLOADS {
+            println!("{name}");
+        }
+        return;
+    }
+    if let Some(idx) = args.iter().position(|a| a == "--report") {
+        match args.get(idx + 1) {
+            Some(path) => report_print(path),
+            None => {
+                eprintln!("alderbench: --report needs a path\n{USAGE}");
+                std::process::exit(2);
+            }
+        }
+        return;
+    }
     if let Some(idx) = args.iter().position(|a| a == "--smoke-workload") {
-        if let Some(name) = args.get(idx + 1) {
-            smoke_workload(name, &cfg);
-            return;
+        match args.get(idx + 1) {
+            Some(name) => smoke_workload(name, &config::load()),
+            None => {
+                eprintln!("alderbench: --smoke-workload needs a name\n{USAGE}");
+                std::process::exit(2);
+            }
         }
+        return;
     }
     if args.iter().any(|a| a == "--headless") {
-        headless_run(cfg);
+        headless_run(config::load());
         return;
     }
     if let Some(idx) = args.iter().position(|a| a == "--preset") {
-        if let Some(name) = args.get(idx + 1) {
-            match name.as_str() {
-                "stability" => {
-                    ui::run_tui(presets::stability());
-                    return;
-                }
-                "bench" => {
-                    ui::run_tui(presets::bench());
-                    return;
-                }
-                "core-cycle" => {
-                    ui::run_tui(presets::stability_core_cycle(60));
-                    return;
-                }
-                _ => {
-                    eprintln!("unknown preset: {name} (available: stability, bench, core-cycle)");
-                    std::process::exit(1);
-                }
+        match args.get(idx + 1).map(|s| s.as_str()) {
+            Some("stability") => require_tty_then(|| ui::run_tui(presets::stability())),
+            Some("bench") => require_tty_then(|| ui::run_tui(presets::bench())),
+            Some("core-cycle") => {
+                require_tty_then(|| ui::run_tui(presets::stability_core_cycle(60)));
+            }
+            Some(other) => {
+                eprintln!("unknown preset: {other} (available: stability, bench, core-cycle)");
+                std::process::exit(1);
+            }
+            None => {
+                eprintln!("alderbench: --preset needs a name\n{USAGE}");
+                std::process::exit(2);
             }
         }
+        return;
     }
     if args.iter().any(|a| a == "--tui") {
-        ui::run_tui(cfg);
+        require_tty_then(|| ui::run_tui(config::load()));
         return;
     }
     // Default mode = TUI picker (lets the user choose a preset or custom).
-    ui::run_tui(cfg);
+    require_tty_then(|| ui::run_tui(config::load()));
+}
+
+/// ratatui panics without a real pty — fail with a clear message instead.
+fn require_tty_then(run: impl FnOnce()) {
+    if !std::io::stdout().is_terminal() {
+        eprintln!(
+            "alderbench: the TUI needs a real terminal (stdout is not a tty) — try --headless"
+        );
+        std::process::exit(1);
+    }
+    run();
+}
+
+/// `--report PATH`: print the human summary of a past run's JSON report.
+fn report_print(path: &str) {
+    let text = match std::fs::read_to_string(path) {
+        Ok(t) => t,
+        Err(e) => {
+            eprintln!("alderbench: can't read {path}: {e}");
+            std::process::exit(1);
+        }
+    };
+    match report::summary_from_json(&text) {
+        Some(s) => println!("{s}"),
+        None => {
+            eprintln!("alderbench: {path} is not an alderbench report (schema v2)");
+            std::process::exit(1);
+        }
+    }
 }
 
 /// `--headless` mode: drive the run loop without a TUI. Prints one line
@@ -66,10 +163,20 @@ fn headless_run(cfg: config::Config) {
     let mut count = 0usize;
     loop {
         match run.tick() {
-            Ok(run::TickOutcome::WorkloadStarted { idx, name, duration_secs }) => {
+            Ok(run::TickOutcome::WorkloadStarted {
+                idx,
+                name,
+                duration_secs,
+            }) => {
                 println!("[{idx}] starting {name} ({duration_secs}s)");
             }
-            Ok(run::TickOutcome::WorkloadFinished { idx, name, verdict, score, report_path }) => {
+            Ok(run::TickOutcome::WorkloadFinished {
+                idx,
+                name,
+                verdict,
+                score,
+                report_path,
+            }) => {
                 println!("[{idx}] finished {name}: {:?}", verdict);
                 println!("  score:  {}", format_score(&score));
                 println!("  report: {}", report_path.display());
@@ -96,7 +203,13 @@ fn format_score(s: &workload::Score) -> String {
     use workload::Score;
     match s {
         Score::None => "(no score)".to_string(),
-        Score::SevenZip { rating_mips, r_u_mips, usage_pct, compress_mips, decompress_mips } => {
+        Score::SevenZip {
+            rating_mips,
+            r_u_mips,
+            usage_pct,
+            compress_mips,
+            decompress_mips,
+        } => {
             format!(
                 "rating={rating_mips} MIPS  r/u={r_u_mips}  usage={usage_pct}%  \
                  compress={compress_mips} MIPS  decompress={decompress_mips} MIPS"
@@ -110,11 +223,44 @@ fn format_score(s: &workload::Score) -> String {
                 .iter()
                 .map(|t| {
                     let gbps = t.bits_per_sec / 1e9;
-                    format!("{}={} {:.2} Gb/s", t.tag, if t.passed { "pass" } else { "FAIL" }, gbps)
+                    format!(
+                        "{}={} {:.2} Gb/s",
+                        t.tag,
+                        if t.passed { "pass" } else { "FAIL" },
+                        gbps
+                    )
                 })
                 .collect::<Vec<_>>()
                 .join("  ")
         }
+        Score::C2c { pairs } => {
+            if pairs.is_empty() {
+                return "(no pairs measured)".to_string();
+            }
+            let min = pairs
+                .iter()
+                .min_by(|x, y| x.ns_per_hop.total_cmp(&y.ns_per_hop))
+                .unwrap();
+            let max = pairs
+                .iter()
+                .max_by(|x, y| x.ns_per_hop.total_cmp(&y.ns_per_hop))
+                .unwrap();
+            format!(
+                "{} pairs  min {:.1} ns (cpu {}<->{}), max {:.1} ns (cpu {}<->{})",
+                pairs.len(),
+                min.ns_per_hop,
+                min.cpu_a,
+                min.cpu_b,
+                max.ns_per_hop,
+                max.cpu_a,
+                max.cpu_b
+            )
+        }
+        Score::Dram { levels } => levels
+            .iter()
+            .map(|l| format!("{} KiB={:.1} ns", l.size_kb, l.ns_per_access))
+            .collect::<Vec<_>>()
+            .join("  "),
     }
 }
 
@@ -161,4 +307,3 @@ fn smoke_workload(name: &str, cfg: &config::Config) {
         Err(e) => println!("smoke: start failed: {e}"),
     }
 }
-
diff --git a/alderbench/src/presets.rs b/alderbench/src/presets.rs
index 1c5d8ef..3e8f424 100644
--- a/alderbench/src/presets.rs
+++ b/alderbench/src/presets.rs
@@ -16,21 +16,20 @@
 
 use crate::config::{Config, WorkloadSpec};
 
-/// Workload names available in v1 (the from_spec match arms in workload.rs).
+/// Workload names available (the from_spec match arms in workload.rs).
 /// The custom editor iterates this list for its toggle rows.
 pub const AVAILABLE_WORKLOADS: &[&str] = &[
     "stress-ng-cpu",
     "y-cruncher-sse",
     "y-cruncher-avx2",
     "7zip-bench",
+    "c2c-latency",
+    "dram-latency",
 ];
 
 /// Cores range options for the custom editor's per-workload cores field.
 /// P = performance cores only, E = efficiency cores only, All = both.
-/// (Reserved for the editor's cores-field cycling; not yet wired into
-/// the TUI's cores edit — the editor currently accepts free-text cores
-/// input via the numeric/string edit buffer.)
-#[allow(dead_code)]
+/// Enter on the Cores field cycles through this list (wired in ui.rs).
 pub const CORES_OPTIONS: &[&str] = &["0-15", "0-11", "12-15"];
 
 fn spec(name: &str, duration_secs: u64, cores: &str) -> WorkloadSpec {
@@ -82,7 +81,7 @@ pub fn stability_core_cycle(per_core_secs: u64) -> Config {
     }
     Config {
         workloads,
-        cool_secs: 5, // shorter cooldown — single-core load doesn't heat the package
+        cool_secs: 5,  // shorter cooldown — single-core load doesn't heat the package
         loop_count: 1, // the core-cycle IS the loop; don't loop the whole thing
         ..Config::default()
     }
@@ -165,7 +164,9 @@ mod tests {
         assert!(AVAILABLE_WORKLOADS.contains(&"y-cruncher-sse"));
         assert!(AVAILABLE_WORKLOADS.contains(&"y-cruncher-avx2"));
         assert!(AVAILABLE_WORKLOADS.contains(&"7zip-bench"));
-        // Deferred impls NOT in the list (c2c/dram/minecraft).
-        assert!(!AVAILABLE_WORKLOADS.contains(&"c2c-latency"));
+        assert!(AVAILABLE_WORKLOADS.contains(&"c2c-latency"));
+        assert!(AVAILABLE_WORKLOADS.contains(&"dram-latency"));
+        // Deferred impl NOT in the list (minecraft).
+        assert!(!AVAILABLE_WORKLOADS.contains(&"minecraft-server"));
     }
-}
\ No newline at end of file
+}
diff --git a/alderbench/src/report.rs b/alderbench/src/report.rs
index 7926296..5e67230 100644
--- a/alderbench/src/report.rs
+++ b/alderbench/src/report.rs
@@ -133,7 +133,9 @@ fn json_verdict(v: &Option<Verdict>) -> String {
     match v {
         None => "null".to_string(),
         Some(Verdict::Clean) => "{\"kind\":\"clean\"}".to_string(),
-        Some(Verdict::Failed(msg)) => format!("{{\"kind\":\"failed\",\"detail\":{}}}", json_str(msg)),
+        Some(Verdict::Failed(msg)) => {
+            format!("{{\"kind\":\"failed\",\"detail\":{}}}", json_str(msg))
+        }
         Some(Verdict::Stopped) => "{\"kind\":\"stopped\"}".to_string(),
         Some(Verdict::Error(msg)) => format!("{{\"kind\":\"error\",\"detail\":{}}}", json_str(msg)),
     }
@@ -172,6 +174,34 @@ fn json_score(s: &Option<Score>) -> String {
             out.push_str("]}");
             out
         }
+        Some(Score::C2c { pairs }) => {
+            let mut out = String::from("{\"kind\":\"c2c\",\"pairs\":[");
+            for (i, p) in pairs.iter().enumerate() {
+                if i > 0 {
+                    out.push(',');
+                }
+                out.push_str(&format!(
+                    "{{\"a\":{},\"b\":{},\"ns\":{}}}",
+                    p.cpu_a, p.cpu_b, p.ns_per_hop
+                ));
+            }
+            out.push_str("]}");
+            out
+        }
+        Some(Score::Dram { levels }) => {
+            let mut out = String::from("{\"kind\":\"dram\",\"levels\":[");
+            for (i, l) in levels.iter().enumerate() {
+                if i > 0 {
+                    out.push(',');
+                }
+                out.push_str(&format!(
+                    "{{\"kb\":{},\"ns\":{}}}",
+                    l.size_kb, l.ns_per_access
+                ));
+            }
+            out.push_str("]}");
+            out
+        }
     }
 }
 
@@ -211,6 +241,12 @@ pub fn human_summary(r: &Report) -> String {
                 format!("{} tests", tests.len())
             }
         }
+        Some(Score::C2c { pairs }) => format!("{} pairs", pairs.len()),
+        Some(Score::Dram { levels }) => levels
+            .iter()
+            .map(|l| format!("{}K {:.0}ns", l.size_kb, l.ns_per_access))
+            .collect::<Vec<_>>()
+            .join(" "),
     };
     format!(
         "{:<18} {:<8} {:<24} verdict={}",
@@ -218,6 +254,231 @@ pub fn human_summary(r: &Report) -> String {
     )
 }
 
+// ---------------------------------------------------------------------------
+// Report reader (`--report <path>`): parse back a JSON file written above
+// and print a human summary. Zero-dep: we only ever read our own output,
+// so a minimal depth-tracking object scanner replaces a JSON crate.
+// ---------------------------------------------------------------------------
+
+/// Split a JSON object body (`{...}`) into (key, raw value slice) pairs at
+/// depth 0. Handles nested objects/arrays and string escapes. Returns None
+/// if `obj` isn't a well-formed-enough `{...}` object.
+fn object_entries(obj: &str) -> Option<Vec<(&str, &str)>> {
+    let bytes = obj.as_bytes();
+    if bytes.len() < 2 || bytes[0] != b'{' || *bytes.last()? != b'}' {
+        return None;
+    }
+    let mut entries = Vec::new();
+    let mut depth = 0i32;
+    let mut in_str = false;
+    let mut esc = false;
+    let mut entry_start = 1usize;
+    for i in 1..bytes.len() - 1 {
+        let c = bytes[i];
+        if in_str {
+            if esc {
+                esc = false;
+            } else if c == b'\\' {
+                esc = true;
+            } else if c == b'"' {
+                in_str = false;
+            }
+            continue;
+        }
+        match c {
+            b'"' => in_str = true,
+            b'{' | b'[' => depth += 1,
+            b'}' | b']' => depth -= 1,
+            b',' if depth == 0 => {
+                entries.push(entry_pair(&obj[entry_start..i])?);
+                entry_start = i + 1;
+            }
+            _ => {}
+        }
+    }
+    let last = &obj[entry_start..obj.len() - 1];
+    if !last.trim().is_empty() {
+        entries.push(entry_pair(last)?);
+    }
+    Some(entries)
+}
+
+/// Turn one `"key": value` entry slice into a (key, value) pair. Our
+/// writer's keys never contain a colon, so the first `:` is the split.
+fn entry_pair(raw: &str) -> Option<(&str, &str)> {
+    let (key, val) = raw.split_once(':')?;
+    let key = key.trim();
+    let key = key.strip_prefix('"')?.strip_suffix('"')?;
+    Some((key, val.trim()))
+}
+
+/// Split a JSON array (`[...]`) into its element slices at depth 0.
+fn array_items(arr: &str) -> Option<Vec<&str>> {
+    let bytes = arr.as_bytes();
+    if bytes.len() < 2 || bytes[0] != b'[' || *bytes.last()? != b']' {
+        return None;
+    }
+    let mut items = Vec::new();
+    let mut depth = 0i32;
+    let mut in_str = false;
+    let mut esc = false;
+    let mut start = 1usize;
+    for i in 1..bytes.len() - 1 {
+        let c = bytes[i];
+        if in_str {
+            if esc {
+                esc = false;
+            } else if c == b'\\' {
+                esc = true;
+            } else if c == b'"' {
+                in_str = false;
+            }
+            continue;
+        }
+        match c {
+            b'"' => in_str = true,
+            b'{' | b'[' => depth += 1,
+            b'}' | b']' => depth -= 1,
+            b',' if depth == 0 => {
+                items.push(arr[start..i].trim());
+                start = i + 1;
+            }
+            _ => {}
+        }
+    }
+    let last = arr[start..arr.len() - 1].trim();
+    if !last.is_empty() {
+        items.push(last);
+    }
+    Some(items)
+}
+
+/// Unquote a JSON string value (our writer only emits the basic escapes).
+fn unquote(s: &str) -> Option<String> {
+    let inner = s.strip_prefix('"')?.strip_suffix('"')?;
+    let mut out = String::with_capacity(inner.len());
+    let mut chars = inner.chars();
+    while let Some(c) = chars.next() {
+        if c != '\\' {
+            out.push(c);
+            continue;
+        }
+        match chars.next()? {
+            '"' => out.push('"'),
+            '\\' => out.push('\\'),
+            'n' => out.push('\n'),
+            'r' => out.push('\r'),
+            't' => out.push('\t'),
+            other => out.push(other),
+        }
+    }
+    Some(out)
+}
+
+fn get_str<'a>(entries: &[(&'a str, &'a str)], key: &str) -> Option<String> {
+    entries
+        .iter()
+        .find(|(k, _)| *k == key)
+        .and_then(|(_, v)| unquote(v))
+}
+
+fn get_raw<'a>(entries: &[(&'a str, &'a str)], key: &str) -> Option<&'a str> {
+    entries.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
+}
+
+/// Human-readable one-line summary from raw report JSON (the `--report`
+/// mode). Mirrors `human_summary`'s layout, reconstructed from the file.
+pub fn summary_from_json(json: &str) -> Option<String> {
+    let top = object_entries(json)?;
+    let workload = get_str(&top, "workload")?;
+    let isa = get_str(&top, "isa_class").unwrap_or_else(|| "other".to_string());
+    let verdict = match get_raw(&top, "verdict") {
+        None | Some("null") => "(not finished)".to_string(),
+        Some(v) => {
+            let e = object_entries(v)?;
+            match get_str(&e, "kind")?.as_str() {
+                "clean" => "CLEAN".to_string(),
+                "stopped" => "STOPPED".to_string(),
+                "failed" => format!("FAILED ({})", get_str(&e, "detail").unwrap_or_default()),
+                "error" => format!("ERROR ({})", get_str(&e, "detail").unwrap_or_default()),
+                other => format!("?{other}"),
+            }
+        }
+    };
+    let score = match get_raw(&top, "score") {
+        None | Some("null") => {
+            if workload == "stress-ng-cpu" {
+                "(stability only)".to_string()
+            } else {
+                "(no score)".to_string()
+            }
+        }
+        Some(s) => {
+            let e = object_entries(s)?;
+            match get_str(&e, "kind")?.as_str() {
+                "7zip" => format!("rating={} MIPS", get_raw(&e, "rating_mips").unwrap_or("?")),
+                "y-cruncher" => {
+                    let tests = get_raw(&e, "tests").unwrap_or("[]");
+                    let mut parts = Vec::new();
+                    for item in array_items(tests).unwrap_or_default() {
+                        let t = object_entries(item)?;
+                        let tag = get_str(&t, "tag").unwrap_or_default();
+                        let passed = get_raw(&t, "passed") == Some("true");
+                        let bps: f64 = get_raw(&t, "bits_per_sec")
+                            .and_then(|v| v.parse().ok())
+                            .unwrap_or(0.0);
+                        parts.push(format!(
+                            "{tag}={} {:.2} Gb/s",
+                            if passed { "pass" } else { "FAIL" },
+                            bps / 1e9
+                        ));
+                    }
+                    parts.join("  ")
+                }
+                "c2c" => {
+                    let pairs = get_raw(&e, "pairs").unwrap_or("[]");
+                    let items = array_items(pairs).unwrap_or_default();
+                    let mut ns_vals = Vec::new();
+                    for item in &items {
+                        let p = object_entries(item)?;
+                        ns_vals.push(
+                            get_raw(&p, "ns")
+                                .and_then(|v| v.parse::<f64>().ok())
+                                .unwrap_or(0.0),
+                        );
+                    }
+                    if ns_vals.is_empty() {
+                        "0 pairs".to_string()
+                    } else {
+                        let lo = ns_vals.iter().cloned().fold(f64::INFINITY, f64::min);
+                        let hi = ns_vals.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
+                        format!("{} pairs min={:.1} max={:.1} ns", ns_vals.len(), lo, hi)
+                    }
+                }
+                "dram" => {
+                    let levels = get_raw(&e, "levels").unwrap_or("[]");
+                    let items = array_items(levels).unwrap_or_default();
+                    let mut parts = Vec::new();
+                    for item in &items {
+                        let l = object_entries(item)?;
+                        let kb = get_raw(&l, "kb").unwrap_or("?");
+                        let ns: f64 = get_raw(&l, "ns")
+                            .and_then(|v| v.parse().ok())
+                            .unwrap_or(0.0);
+                        parts.push(format!("{kb}K={ns:.1} ns"));
+                    }
+                    parts.join(" ")
+                }
+                other => format!("?{other}"),
+            }
+        }
+    };
+    Some(format!(
+        "{:<18} {:<8} {:<24} verdict={}",
+        workload, isa, score, verdict
+    ))
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -230,8 +491,12 @@ mod tests {
     }
 
     fn sample_report() -> Report {
-        let mut r = Report::new(&test_dir("sample_report"), "y-cruncher-avx2", "60s cores=0-15 FFTv4")
-            .unwrap();
+        let mut r = Report::new(
+            &test_dir("sample_report"),
+            "y-cruncher-avx2",
+            "60s cores=0-15 FFTv4",
+        )
+        .unwrap();
         r.finish(
             Verdict::Clean,
             Score::YCruncher {
@@ -269,9 +534,14 @@ mod tests {
     fn json_verdict_variants() {
         assert_eq!(json_verdict(&None), "null");
         assert_eq!(json_verdict(&Some(Verdict::Clean)), "{\"kind\":\"clean\"}");
-        assert_eq!(json_verdict(&Some(Verdict::Stopped)), "{\"kind\":\"stopped\"}");
-        assert!(json_verdict(&Some(Verdict::Failed("oops".to_string()))).contains("\"kind\":\"failed\""));
-        assert!(json_verdict(&Some(Verdict::Failed("oops".to_string()))).contains("\"detail\":\"oops\""));
+        assert_eq!(
+            json_verdict(&Some(Verdict::Stopped)),
+            "{\"kind\":\"stopped\"}"
+        );
+        assert!(json_verdict(&Some(Verdict::Failed("oops".to_string())))
+            .contains("\"kind\":\"failed\""));
+        assert!(json_verdict(&Some(Verdict::Failed("oops".to_string())))
+            .contains("\"detail\":\"oops\""));
     }
 
     #[test]
@@ -302,8 +572,16 @@ mod tests {
     fn json_score_ycruncher_round_trips() {
         let s = Score::YCruncher {
             tests: vec![
-                YCruncherTest { tag: "FFTv4".to_string(), passed: true, bits_per_sec: 8.94e8 },
-                YCruncherTest { tag: "BKT".to_string(), passed: true, bits_per_sec: 9.49e9 },
+                YCruncherTest {
+                    tag: "FFTv4".to_string(),
+                    passed: true,
+                    bits_per_sec: 8.94e8,
+                },
+                YCruncherTest {
+                    tag: "BKT".to_string(),
+                    passed: true,
+                    bits_per_sec: 9.49e9,
+                },
             ],
         };
         let j = json_score(&Some(s));
@@ -316,7 +594,8 @@ mod tests {
 
     #[test]
     fn finish_stores_score_and_verdict() {
-        let mut r = Report::new(&test_dir("finish_stores_score_and_verdict"), "test", "test").unwrap();
+        let mut r =
+            Report::new(&test_dir("finish_stores_score_and_verdict"), "test", "test").unwrap();
         r.finish(
             Verdict::Clean,
             Score::SevenZip {
@@ -334,7 +613,12 @@ mod tests {
 
     #[test]
     fn finish_keeps_none_score_for_summary() {
-        let mut r = Report::new(&test_dir("finish_keeps_none_score_for_summary"), "test", "test").unwrap();
+        let mut r = Report::new(
+            &test_dir("finish_keeps_none_score_for_summary"),
+            "test",
+            "test",
+        )
+        .unwrap();
         r.finish(Verdict::Clean, Score::None).unwrap();
         // Stored as Some(Score::None) so human_summary can distinguish
         // "stability only" from "not finished". JSON still renders null.
@@ -375,7 +659,13 @@ mod tests {
         assert!(written.contains("\"workload\":\"flush-test\""));
         assert!(written.contains("\"verdict\":{\"kind\":\"clean\"}"));
         assert!(written.contains("\"score\":null"));
-        assert!(r.path().file_name().unwrap().to_str().unwrap().contains("flush-test.json"));
+        assert!(r
+            .path()
+            .file_name()
+            .unwrap()
+            .to_str()
+            .unwrap()
+            .contains("flush-test.json"));
     }
 
     #[test]
@@ -410,7 +700,12 @@ mod tests {
 
     #[test]
     fn human_summary_for_stability_only() {
-        let mut r = Report::new(&test_dir("human_summary_for_stability_only"), "stress-ng-cpu", "60s").unwrap();
+        let mut r = Report::new(
+            &test_dir("human_summary_for_stability_only"),
+            "stress-ng-cpu",
+            "60s",
+        )
+        .unwrap();
         r.finish(Verdict::Clean, Score::None).unwrap();
         let s = human_summary(&r);
         assert!(s.contains("(stability only)"));
@@ -428,4 +723,115 @@ mod tests {
         assert!(!name.contains(':'));
         assert!(!name.contains(' '));
     }
-}
\ No newline at end of file
+
+    #[test]
+    fn summary_from_json_round_trips_ycruncher() {
+        let r = sample_report();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("y-cruncher-avx2"), "{s}");
+        assert!(s.contains("avx2"), "{s}");
+        assert!(s.contains("CLEAN"), "{s}");
+        assert!(s.contains("FFTv4=pass 0.89 Gb/s"), "{s}");
+    }
+
+    #[test]
+    fn summary_from_json_round_trips_7zip() {
+        let mut r = Report::new(&test_dir("summary_7zip"), "7zip-bench", "60s").unwrap();
+        r.finish(
+            Verdict::Clean,
+            Score::SevenZip {
+                rating_mips: 66259,
+                r_u_mips: 4653,
+                usage_pct: 1431,
+                compress_mips: 5346,
+                decompress_mips: 3961,
+            },
+        )
+        .unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("rating=66259 MIPS"), "{s}");
+        assert!(s.contains("mixed"), "{s}");
+    }
+
+    #[test]
+    fn summary_from_json_not_finished_and_stability_only() {
+        let r = Report::new(&test_dir("summary_not_finished"), "test", "60s").unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("(not finished)"), "{s}");
+        assert!(s.contains("(no score)"), "{s}");
+
+        let mut r = Report::new(&test_dir("summary_stability"), "stress-ng-cpu", "60s").unwrap();
+        r.finish(Verdict::Clean, Score::None).unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("(stability only)"), "{s}");
+        assert!(s.contains("CLEAN"), "{s}");
+    }
+
+    #[test]
+    fn summary_from_json_survives_special_chars_in_detail() {
+        let mut r = Report::new(&test_dir("summary_special"), "test", "60s").unwrap();
+        r.finish(
+            Verdict::Failed("bad: exit {1}, \"quoted\"".to_string()),
+            Score::None,
+        )
+        .unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("FAILED (bad: exit {1}, \"quoted\")"), "{s}");
+    }
+
+    #[test]
+    fn summary_from_json_rejects_junk() {
+        assert!(summary_from_json("not json").is_none());
+        assert!(summary_from_json("{}").is_none()); // no workload key
+    }
+
+    #[test]
+    fn summary_from_json_round_trips_c2c() {
+        let mut r = Report::new(&test_dir("summary_c2c"), "c2c-latency", "30s").unwrap();
+        r.finish(
+            Verdict::Clean,
+            Score::C2c {
+                pairs: vec![
+                    crate::workload::C2cPair {
+                        cpu_a: 0,
+                        cpu_b: 1,
+                        ns_per_hop: 28.02,
+                    },
+                    crate::workload::C2cPair {
+                        cpu_a: 0,
+                        cpu_b: 2,
+                        ns_per_hop: 43.83,
+                    },
+                ],
+            },
+        )
+        .unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("2 pairs min=28.0 max=43.8 ns"), "{s}");
+        assert!(s.contains("CLEAN"), "{s}");
+    }
+
+    #[test]
+    fn summary_from_json_round_trips_dram() {
+        let mut r = Report::new(&test_dir("summary_dram"), "dram-latency", "30s").unwrap();
+        r.finish(
+            Verdict::Clean,
+            Score::Dram {
+                levels: vec![
+                    crate::workload::DramLevel {
+                        size_kb: 32,
+                        ns_per_access: 4.536,
+                    },
+                    crate::workload::DramLevel {
+                        size_kb: 65536,
+                        ns_per_access: 67.51,
+                    },
+                ],
+            },
+        )
+        .unwrap();
+        let s = summary_from_json(&r.to_json()).unwrap();
+        assert!(s.contains("32K=4.5 ns"), "{s}");
+        assert!(s.contains("65536K=67.5 ns"), "{s}");
+    }
+}
diff --git a/alderbench/src/run.rs b/alderbench/src/run.rs
index 838bba3..4a8d36e 100644
--- a/alderbench/src/run.rs
+++ b/alderbench/src/run.rs
@@ -27,7 +27,11 @@ use crate::workload::{self, Score, Verdict, Workload};
 pub enum TickOutcome {
     /// A workload was just started this tick. `idx` is the workload index,
     /// `name` is its name, `duration_secs` is the planned run length.
-    WorkloadStarted { idx: usize, name: String, duration_secs: u64 },
+    WorkloadStarted {
+        idx: usize,
+        name: String,
+        duration_secs: u64,
+    },
     /// The workload exited (either it self-finished or the duration timer
     /// elapsed and we stopped it). The verdict + score + report path are
     /// included for the TUI's result table + stdout summary.
@@ -109,9 +113,7 @@ impl Run {
             workloads,
             skipped,
             current: 0,
-            state: State::Cooldown {
-                remaining: 0,
-            },
+            state: State::Cooldown { remaining: 0 },
             state_start: Instant::now(),
             report: None,
             loops_left,
@@ -270,7 +272,11 @@ impl Run {
             return Ok(TickOutcome::SweepDone);
         }
         let spec = &self.cfg.workloads[self.current];
-        let report = Report::new(&self.cfg.report_dir, &spec.name, &self.workload_params(spec))?;
+        let report = Report::new(
+            &self.cfg.report_dir,
+            &spec.name,
+            &self.workload_params(spec),
+        )?;
         self.report = Some(report);
         let w = self.workloads[self.current].as_mut().unwrap();
         w.start()?;
@@ -329,7 +335,8 @@ impl Run {
                 // Skip any leading None entries (defensive — shouldn't
                 // happen since construction skips them, but the same
                 // loop the constructor relies on).
-                while self.current < self.workloads.len() && self.workloads[self.current].is_none() {
+                while self.current < self.workloads.len() && self.workloads[self.current].is_none()
+                {
                     self.current += 1;
                 }
                 // If the whole workload list was None (shouldn't happen),
@@ -434,8 +441,10 @@ mod tests {
     /// Build a Run with mock workloads in place of `from_spec`. Bypasses
     /// the real constructor so we don't need real binaries.
     fn run_with_mocks(cfg: Config, mocks: Vec<MockWorkload>) -> Run {
-        let workloads: Vec<Option<Box<dyn Workload>>> =
-            mocks.into_iter().map(|m| Some(Box::new(m) as Box<dyn Workload>)).collect();
+        let workloads: Vec<Option<Box<dyn Workload>>> = mocks
+            .into_iter()
+            .map(|m| Some(Box::new(m) as Box<dyn Workload>))
+            .collect();
         let loops_left = cfg.loop_count;
         Run {
             cfg,
@@ -454,13 +463,36 @@ mod tests {
     fn sweep_runs_all_workloads_then_done() {
         let _ = std::fs::remove_dir_all("/tmp/opencode/run-tests");
         let cfg = mock_cfg(vec![
-            WorkloadSpec { name: "mock-a".to_string(), duration_secs: 60, cores: "0-15".to_string() },
-            WorkloadSpec { name: "mock-b".to_string(), duration_secs: 60, cores: "0-15".to_string() },
-        ]);
-        let mut run = run_with_mocks(cfg, vec![
-            MockWorkload { name: "mock-a".to_string(), ticks_until_exit: 3, ticks_seen: 0, started: false, stopped: false },
-            MockWorkload { name: "mock-b".to_string(), ticks_until_exit: 2, ticks_seen: 0, started: false, stopped: false },
+            WorkloadSpec {
+                name: "mock-a".to_string(),
+                duration_secs: 60,
+                cores: "0-15".to_string(),
+            },
+            WorkloadSpec {
+                name: "mock-b".to_string(),
+                duration_secs: 60,
+                cores: "0-15".to_string(),
+            },
         ]);
+        let mut run = run_with_mocks(
+            cfg,
+            vec![
+                MockWorkload {
+                    name: "mock-a".to_string(),
+                    ticks_until_exit: 3,
+                    ticks_seen: 0,
+                    started: false,
+                    stopped: false,
+                },
+                MockWorkload {
+                    name: "mock-b".to_string(),
+                    ticks_until_exit: 2,
+                    ticks_seen: 0,
+                    started: false,
+                    stopped: false,
+                },
+            ],
+        );
         let mut started = 0;
         let mut finished = 0;
         let mut done = false;
@@ -496,13 +528,22 @@ mod tests {
         // Verify: a never-exiting mock + a long duration → the run loop
         // never returns WorkloadFinished, no matter how many ticks.
         let _ = std::fs::remove_dir_all("/tmp/opencode/run-tests");
-        let mut cfg = mock_cfg(vec![
-            WorkloadSpec { name: "never-exits".to_string(), duration_secs: 0, cores: "0-15".to_string() },
-        ]);
+        let mut cfg = mock_cfg(vec![WorkloadSpec {
+            name: "never-exits".to_string(),
+            duration_secs: 0,
+            cores: "0-15".to_string(),
+        }]);
         cfg.loop_count = 1;
-        let mut run = run_with_mocks(cfg, vec![
-            MockWorkload { name: "never-exits".to_string(), ticks_until_exit: 1000, ticks_seen: 0, started: false, stopped: false },
-        ]);
+        let mut run = run_with_mocks(
+            cfg,
+            vec![MockWorkload {
+                name: "never-exits".to_string(),
+                ticks_until_exit: 1000,
+                ticks_seen: 0,
+                started: false,
+                stopped: false,
+            }],
+        );
         // Tick once to start (Cooldown → Running).
         let _ = run.tick().unwrap();
         // Manually rewind state_start so elapsed is huge (well past any
@@ -529,7 +570,11 @@ mod tests {
         let mut run = run_with_mocks(cfg, vec![]);
         run.workloads.push(None); // a skipped entry
         run.workloads.push(Some(Box::new(MockWorkload {
-            name: "x".to_string(), ticks_until_exit: 1, ticks_seen: 0, started: false, stopped: false,
+            name: "x".to_string(),
+            ticks_until_exit: 1,
+            ticks_seen: 0,
+            started: false,
+            stopped: false,
         })));
         assert_eq!(run.total_workloads(), 1);
     }
@@ -546,14 +591,37 @@ mod tests {
         // total) before SweepDone.
         let _ = std::fs::remove_dir_all("/tmp/opencode/run-tests");
         let mut cfg = mock_cfg(vec![
-            WorkloadSpec { name: "a".to_string(), duration_secs: 60, cores: "0-15".to_string() },
-            WorkloadSpec { name: "b".to_string(), duration_secs: 60, cores: "0-15".to_string() },
+            WorkloadSpec {
+                name: "a".to_string(),
+                duration_secs: 60,
+                cores: "0-15".to_string(),
+            },
+            WorkloadSpec {
+                name: "b".to_string(),
+                duration_secs: 60,
+                cores: "0-15".to_string(),
+            },
         ]);
         cfg.loop_count = 2;
-        let mut run = run_with_mocks(cfg, vec![
-            MockWorkload { name: "a".to_string(), ticks_until_exit: 2, ticks_seen: 0, started: false, stopped: false },
-            MockWorkload { name: "b".to_string(), ticks_until_exit: 2, ticks_seen: 0, started: false, stopped: false },
-        ]);
+        let mut run = run_with_mocks(
+            cfg,
+            vec![
+                MockWorkload {
+                    name: "a".to_string(),
+                    ticks_until_exit: 2,
+                    ticks_seen: 0,
+                    started: false,
+                    stopped: false,
+                },
+                MockWorkload {
+                    name: "b".to_string(),
+                    ticks_until_exit: 2,
+                    ticks_seen: 0,
+                    started: false,
+                    stopped: false,
+                },
+            ],
+        );
         let mut starts = 0;
         let mut done = false;
         for _ in 0..40 {
@@ -572,7 +640,11 @@ mod tests {
         }
         assert!(done, "should reach SweepDone");
         assert_eq!(starts, 4, "two workloads × two loops = 4 starts");
-        assert_eq!(run.loop_index(), 2, "loop_index should be 2 after one reset");
+        assert_eq!(
+            run.loop_index(),
+            2,
+            "loop_index should be 2 after one reset"
+        );
     }
 
     #[test]
@@ -582,13 +654,22 @@ mod tests {
         // be here forever in real life, but we cap the loop + check that
         // it's still going after several sweep completions).
         let _ = std::fs::remove_dir_all("/tmp/opencode/run-tests");
-        let mut cfg = mock_cfg(vec![
-            WorkloadSpec { name: "a".to_string(), duration_secs: 60, cores: "0-15".to_string() },
-        ]);
+        let mut cfg = mock_cfg(vec![WorkloadSpec {
+            name: "a".to_string(),
+            duration_secs: 60,
+            cores: "0-15".to_string(),
+        }]);
         cfg.loop_count = 0; // infinite
-        let mut run = run_with_mocks(cfg, vec![
-            MockWorkload { name: "a".to_string(), ticks_until_exit: 2, ticks_seen: 0, started: false, stopped: false },
-        ]);
+        let mut run = run_with_mocks(
+            cfg,
+            vec![MockWorkload {
+                name: "a".to_string(),
+                ticks_until_exit: 2,
+                ticks_seen: 0,
+                started: false,
+                stopped: false,
+            }],
+        );
         let mut starts = 0;
         let mut sweep_done = false;
         for _ in 0..100 {
@@ -603,6 +684,9 @@ mod tests {
         }
         assert!(!sweep_done, "infinite loop should never reach SweepDone");
         // Should have completed several loops (start count >> 1).
-        assert!(starts > 5, "infinite loop should have started many times, got {starts}");
+        assert!(
+            starts > 5,
+            "infinite loop should have started many times, got {starts}"
+        );
     }
-}
\ No newline at end of file
+}
diff --git a/alderbench/src/ui.rs b/alderbench/src/ui.rs
index 0282987..f5fe7ab 100644
--- a/alderbench/src/ui.rs
+++ b/alderbench/src/ui.rs
@@ -2,8 +2,11 @@
 //!  - **Picker** (home screen): choose Stability / Bench / Custom / q to quit.
 //!  - **Editor** (custom preset): toggle workloads, edit duration/cores/
 //!    cooldown/loop count, reorder, then Start to launch the sweep.
-//!  - **Sweep**: consume `run::Run::tick()` every `poll_ms`, render the
-//!    current workload + progress + a results table. q/Esc quits.
+//!  - **Sweep**: consume `run::Run::tick()` + render the current
+//!    workload + progress + a results table. While a workload runs the
+//!    loop heartbeat-sleeps (≤15s, ~6s past the estimate) instead of
+//!    ticking at poll_ms — keeps CPU off single-core workloads while the
+//!    panel stays live. q/Esc quits.
 //!
 //! NO sensor meters — aldermon owns sensors. alderbench records scores.
 
@@ -12,11 +15,11 @@ use std::time::Duration;
 
 use crossterm::event::{self, Event, KeyCode};
 use ratatui::{
-    Frame,
     layout::{Constraint, Direction, Layout, Rect},
     style::{Color, Modifier, Style},
     text::{Line, Span},
     widgets::{Block, Borders, Cell, List, ListItem, Paragraph, Row, Table},
+    Frame,
 };
 
 use crate::config::{Config, WorkloadSpec};
@@ -78,9 +81,15 @@ impl EditorField {
 // ---------------------------------------------------------------------------
 
 const PICKER_ROWS: &[(&str, &str)] = &[
-    ("Stability", "stress-ng + y-cruncher SSE/AVX2, 5 min each, looped 1 hr"),
+    (
+        "Stability",
+        "stress-ng + y-cruncher SSE/AVX2, 5 min each, looped 1 hr",
+    ),
     ("Bench", "7z + y-cruncher SSE/AVX2, 60 s each, single pass"),
-    ("Core-cycle", "single-core per-core stability, 16 cores x 2 passes"),
+    (
+        "Core-cycle",
+        "single-core per-core stability, 16 cores x 2 passes",
+    ),
     ("Custom", "tune the workload list + params yourself"),
 ];
 
@@ -112,21 +121,18 @@ pub fn run_tui(cfg: Config) {
                 }
             };
             // While a workload is RUNNING (not cooldown), don't re-draw
-            // every tick — the render loop steals CPU from the workload,
-            // which on single-core runs extends the wall time enough to
-            // cause false Stopped verdicts. The user can see CPU load via
-            // aldermon. We only re-draw on state transitions (workload
-            // started / finished / sweep done) + during cooldown (when
-            // the CPU is idle anyway).
+            // every tick — the render loop steals CPU from the workload.
+            // Session 6 killed the 2s poll cadence for this reason (it
+            // stretched a 10s single-core y-cruncher run past 60s). The
+            // compromise below: sleep on a heartbeat (compute_sleep_secs,
+            // capped at ~15s), draw once per wake. The panel's elapsed
+            // stays live + overrun states become visible, while the TUI
+            // spends ~0% CPU (one diff-draw per 15s).
             let should_draw = match &outcome {
                 TickOutcome::WorkloadStarted { .. } => true,
                 TickOutcome::WorkloadFinished { .. } => true,
                 TickOutcome::SweepDone => true,
-                TickOutcome::Idle => {
-                    // Draw during cooldown (CPU is idle) but NOT during
-                    // running (CPU is busy with the workload).
-                    app.run.is_cooldown() || app.run.is_done()
-                }
+                TickOutcome::Idle => app.run.is_cooldown() || app.run.is_done(),
             };
             let is_done = matches!(outcome, TickOutcome::SweepDone);
             // Compute the sleep duration BEFORE we potentially move `app`
@@ -142,10 +148,9 @@ pub fn run_tui(cfg: Config) {
                 mode = Mode::Done(finished);
             }
             if !should_draw {
-                // While the workload is running, sleep for the ENTIRE
-                // estimated duration + a generous margin. One long sleep
-                // means the TUI process is genuinely idle (not scheduling)
-                // while the workload runs — no CPU contention.
+                // Heartbeat sleep. Wake on timeout (then fall through to
+                // the draw below so elapsed updates) or on a key (q/Esc
+                // cancels; anything else also falls through to a draw).
                 if event::poll(Duration::from_secs(sleep_secs)).unwrap_or(false) {
                     if let Ok(Event::Key(key)) = event::read() {
                         if key.kind == crossterm::event::KeyEventKind::Press
@@ -155,7 +160,6 @@ pub fn run_tui(cfg: Config) {
                         }
                     }
                 }
-                continue;
             }
         }
 
@@ -175,32 +179,26 @@ pub fn run_tui(cfg: Config) {
             continue;
         }
         match &mut mode {
-            Mode::Picker(state) => {
-                match key.code {
-                    KeyCode::Up | KeyCode::Char('k') => state.cursor = state.cursor.saturating_sub(1),
-                    KeyCode::Down | KeyCode::Char('j') => {
-                        state.cursor = (state.cursor + 1).min(PICKER_ROWS.len() - 1)
-                    }
-                    KeyCode::Enter => {
-                        match state.cursor {
-                            0 => mode = Mode::Sweep(App::new(presets::stability())),
-                            1 => mode = Mode::Sweep(App::new(presets::bench())),
-                            2 => mode = Mode::Sweep(App::new(presets::stability_core_cycle(60))),
-                            3 => mode = Mode::Editor(EditorState::from_cfg(default_cfg.clone())),
-                            _ => {}
-                        }
-                    }
-                    KeyCode::Char('q') | KeyCode::Esc => break,
-                    _ => {}
+            Mode::Picker(state) => match key.code {
+                KeyCode::Up | KeyCode::Char('k') => state.cursor = state.cursor.saturating_sub(1),
+                KeyCode::Down | KeyCode::Char('j') => {
+                    state.cursor = (state.cursor + 1).min(PICKER_ROWS.len() - 1)
                 }
-            }
-            Mode::Editor(state) => {
-                match editor_key(state, key.code) {
-                    EditorAction::Start => mode = Mode::Sweep(App::new(state.cfg.clone())),
-                    EditorAction::Cancel => mode = Mode::Picker(PickerState { cursor: 3 }),
-                    EditorAction::None => {}
-                }
-            }
+                KeyCode::Enter => match state.cursor {
+                    0 => mode = Mode::Sweep(App::new(presets::stability())),
+                    1 => mode = Mode::Sweep(App::new(presets::bench())),
+                    2 => mode = Mode::Sweep(App::new(presets::stability_core_cycle(60))),
+                    3 => mode = Mode::Editor(EditorState::from_cfg(default_cfg.clone())),
+                    _ => {}
+                },
+                KeyCode::Char('q') | KeyCode::Esc => break,
+                _ => {}
+            },
+            Mode::Editor(state) => match editor_key(state, key.code) {
+                EditorAction::Start => mode = Mode::Sweep(App::new(state.cfg.clone())),
+                EditorAction::Cancel => mode = Mode::Picker(PickerState { cursor: 3 }),
+                EditorAction::None => {}
+            },
             Mode::Sweep(app) => {
                 // The tick already ran at the top of the loop (before
                 // draw + poll) so the sweep advances even without input.
@@ -242,7 +240,11 @@ fn print_summary(app: &App) {
     for r in &app.results {
         println!(
             "[{}] {} {:<8} {:<24} verdict={:?}",
-            r.idx, r.name, r.isa_class, format_score_short(&r.score), r.verdict
+            r.idx,
+            r.name,
+            r.isa_class,
+            format_score_short(&r.score),
+            r.verdict
         );
         println!("  report: {}", r.report_path);
     }
@@ -307,8 +309,15 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
         KeyCode::Left | KeyCode::Right => {
             // Cycle field within the current row (only for workload rows).
             if state.cursor < state.cfg.workloads.len() {
-                let idx = EditorField::PER_ROW.iter().position(|f| *f == state.field).unwrap_or(0);
-                let dir = if matches!(key, KeyCode::Left) { -1i32 } else { 1 };
+                let idx = EditorField::PER_ROW
+                    .iter()
+                    .position(|f| *f == state.field)
+                    .unwrap_or(0);
+                let dir = if matches!(key, KeyCode::Left) {
+                    -1i32
+                } else {
+                    1
+                };
                 let n = EditorField::PER_ROW.len() as i32;
                 let next = ((idx as i32 + dir).rem_euclid(n)) as usize;
                 state.field = EditorField::PER_ROW[next];
@@ -316,7 +325,8 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
         }
         KeyCode::Enter => {
             // Enter on a workload row's Name field toggles inclusion (cycles
-            // through available workload names). On Duration/Cores/Cooldown/
+            // through available workload names). On Cores it cycles the
+            // preset ranges (0-15 / 0-11 / 12-15). On Duration/Cooldown/
             // LoopCount it starts numeric editing. On a "Start" pseudo-row
             // (cursor == workloads.len() + globals.len()) it launches.
             let total_rows = state.cfg.workloads.len() + EditorField::GLOBALS.len() + 1; // +Start
@@ -335,7 +345,21 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
                         w.name = presets::AVAILABLE_WORKLOADS[next].to_string();
                     }
                 }
-                EditorField::Duration | EditorField::Cores | EditorField::Cooldown | EditorField::LoopCount => {
+                EditorField::Cores => {
+                    // Cycle through the preset core ranges instead of
+                    // free-text editing. Unknown current values (e.g. a
+                    // hand-edited conf range) restart at the first option.
+                    if let Some(w) = state.cfg.workloads.get_mut(state.cursor) {
+                        w.cores = match presets::CORES_OPTIONS.iter().position(|c| *c == w.cores) {
+                            Some(idx) => {
+                                presets::CORES_OPTIONS[(idx + 1) % presets::CORES_OPTIONS.len()]
+                            }
+                            None => presets::CORES_OPTIONS[0],
+                        }
+                        .to_string();
+                    }
+                }
+                EditorField::Duration | EditorField::Cooldown | EditorField::LoopCount => {
                     let current = editor_current_value(state);
                     state.editing = Some(current);
                 }
@@ -367,8 +391,9 @@ fn editor_key(state: &mut EditorState, key: KeyCode) -> EditorAction {
             state.cfg.workloads.swap(state.cursor, state.cursor - 1);
             state.cursor -= 1;
         }
-        KeyCode::Char('>') if state.cursor < state.cfg.workloads.len()
-            && state.cursor + 1 < state.cfg.workloads.len() =>
+        KeyCode::Char('>')
+            if state.cursor < state.cfg.workloads.len()
+                && state.cursor + 1 < state.cfg.workloads.len() =>
         {
             state.cfg.workloads.swap(state.cursor, state.cursor + 1);
             state.cursor += 1;
@@ -552,7 +577,9 @@ fn draw_picker(f: &mut Frame, state: &PickerState) {
         .map(|(i, (name, desc))| {
             let cursor = if i == state.cursor { ">" } else { " " };
             let style = if i == state.cursor {
-                Style::default().add_modifier(Modifier::BOLD).fg(Color::Cyan)
+                Style::default()
+                    .add_modifier(Modifier::BOLD)
+                    .fg(Color::Cyan)
             } else {
                 Style::default()
             };
@@ -564,22 +591,26 @@ fn draw_picker(f: &mut Frame, state: &PickerState) {
             ]))
         })
         .collect();
-    let list = List::new(items)
-        .block(
-            Block::default()
-                .borders(Borders::ALL)
-                .border_style(Style::default().fg(Color::DarkGray))
-                .title(Span::styled(" alderbench — choose a sweep ", Style::default().fg(Color::Cyan))),
-        );
+    let list = List::new(items).block(
+        Block::default()
+            .borders(Borders::ALL)
+            .border_style(Style::default().fg(Color::DarkGray))
+            .title(Span::styled(
+                " alderbench — choose a sweep ",
+                Style::default().fg(Color::Cyan),
+            )),
+    );
     f.render_widget(list, area);
     // Footer with keybinds.
-    let footer = Line::from(vec![
-        Span::styled(
-            "↑↓ select  Enter start  q quit",
-            Style::default().fg(Color::DarkGray),
-        ),
-    ]);
-    let footer_area = Rect { y: area.bottom().saturating_sub(1), height: 1, ..area };
+    let footer = Line::from(vec![Span::styled(
+        "↑↓ select  Enter start  q quit",
+        Style::default().fg(Color::DarkGray),
+    )]);
+    let footer_area = Rect {
+        y: area.bottom().saturating_sub(1),
+        height: 1,
+        ..area
+    };
     f.render_widget(Paragraph::new(footer), footer_area);
 }
 
@@ -625,7 +656,9 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
                     let editing = state.editing.as_deref().unwrap_or(&text);
                     Cell::from(Span::styled(
                         format!("[{editing}]"),
-                        Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
+                        Style::default()
+                            .fg(Color::Yellow)
+                            .add_modifier(Modifier::BOLD),
                     ))
                 } else {
                     Cell::from(Span::styled(text, row_style))
@@ -659,22 +692,34 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
         Block::default()
             .borders(Borders::ALL)
             .border_style(Style::default().fg(Color::DarkGray))
-            .title(Span::styled(" workloads ", Style::default().fg(Color::Cyan))),
+            .title(Span::styled(
+                " workloads ",
+                Style::default().fg(Color::Cyan),
+            )),
     );
     f.render_widget(table, chunks[1]);
 
     // Globals (cooldown + loop count).
     let global_cell = |field: EditorField, label: &str, value: String| -> Line {
-        let cursor = if state.cursor == state.cfg.workloads.len() + EditorField::GLOBALS.iter().position(|f| *f == field).unwrap() {
+        let cursor = if state.cursor
+            == state.cfg.workloads.len()
+                + EditorField::GLOBALS
+                    .iter()
+                    .position(|f| *f == field)
+                    .unwrap()
+        {
             ">"
         } else {
             " "
         };
-        let val_span = if state.cursor >= state.cfg.workloads.len()
-            && state.field == field
-        {
+        let val_span = if state.cursor >= state.cfg.workloads.len() && state.field == field {
             let editing = state.editing.as_deref().unwrap_or(&value);
-            Span::styled(format!("[{editing}]"), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD))
+            Span::styled(
+                format!("[{editing}]"),
+                Style::default()
+                    .fg(Color::Yellow)
+                    .add_modifier(Modifier::BOLD),
+            )
         } else {
             Span::raw(value)
         };
@@ -684,12 +729,27 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
             val_span,
         ])
     };
-    let globals = [global_cell(EditorField::Cooldown, "cooldown", format!("{}s", state.cfg.cool_secs)),
-        global_cell(EditorField::LoopCount, "loop count", state.cfg.loop_count.to_string())];
+    let globals = [
+        global_cell(
+            EditorField::Cooldown,
+            "cooldown",
+            format!("{}s", state.cfg.cool_secs),
+        ),
+        global_cell(
+            EditorField::LoopCount,
+            "loop count",
+            state.cfg.loop_count.to_string(),
+        ),
+    ];
     for (i, line) in globals.iter().enumerate() {
         f.render_widget(
             Paragraph::new(line.clone()),
-            Rect { x: chunks[2].x, y: chunks[2].y + i as u16, width: chunks[2].width, height: 1 },
+            Rect {
+                x: chunks[2].x,
+                y: chunks[2].y + i as u16,
+                width: chunks[2].width,
+                height: 1,
+            },
         );
     }
 
@@ -700,7 +760,9 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
         " "
     };
     let start_style = if state.cursor == state.cfg.workloads.len() + EditorField::GLOBALS.len() {
-        Style::default().add_modifier(Modifier::BOLD).fg(Color::Green)
+        Style::default()
+            .add_modifier(Modifier::BOLD)
+            .fg(Color::Green)
     } else {
         Style::default().fg(Color::DarkGray)
     };
@@ -714,12 +776,10 @@ fn draw_editor(f: &mut Frame, state: &EditorState) {
 
     // Footer with keybinds.
     f.render_widget(
-        Paragraph::new(Line::from(vec![
-            Span::styled(
-                "j/k row  ←→ field  Enter edit/toggle  a add  d del  </> reorder  Esc back",
-                Style::default().fg(Color::DarkGray),
-            ),
-        ])),
+        Paragraph::new(Line::from(vec![Span::styled(
+            "j/k row  ←→ field  Enter edit/toggle  a add  d del  </> reorder  Esc back",
+            Style::default().fg(Color::DarkGray),
+        )])),
         chunks[4],
     );
 }
@@ -753,7 +813,10 @@ fn draw_header(f: &mut Frame, area: Rect, app: &App) {
         format!("loop {loop_idx}/{loop_total}")
     };
     let title = if app.run.is_done() {
-        format!("alderbench — sweep complete: {} workloads", app.results.len())
+        format!(
+            "alderbench — sweep complete: {} workloads",
+            app.results.len()
+        )
     } else if current == 0 {
         format!("alderbench — starting (0/{total})  {loop_str}")
     } else {
@@ -801,8 +864,16 @@ fn draw_current(f: &mut Frame, area: Rect, app: &App) {
     };
 
     let isa = report::isa_class(&spec.name);
-    let state_label = if app.run.is_cooldown() { "cooldown" } else { "running" };
-    let state_color = if app.run.is_cooldown() { Color::Blue } else { Color::Green };
+    let state_label = if app.run.is_cooldown() {
+        "cooldown"
+    } else {
+        "running"
+    };
+    let state_color = if app.run.is_cooldown() {
+        Color::Blue
+    } else {
+        Color::Green
+    };
     let elapsed = app.run.elapsed_secs();
 
     let inner = block.inner(area);
@@ -817,30 +888,38 @@ fn draw_current(f: &mut Frame, area: Rect, app: &App) {
         } else {
             0.0
         };
-        [
+        vec![
             Line::from(vec![
-                Span::styled(format!("{:<20}", spec.name), Style::default().add_modifier(Modifier::BOLD)),
+                Span::styled(
+                    format!("{:<20}", spec.name),
+                    Style::default().add_modifier(Modifier::BOLD),
+                ),
                 Span::raw("  "),
                 Span::styled(format!("isa={:<8}", isa), Style::default().fg(Color::Cyan)),
                 Span::raw("  "),
                 Span::styled(state_label, Style::default().fg(state_color)),
             ]),
-            Line::from(vec![
-                Span::styled(
-                    format!("elapsed {:>5.1}s  remaining {:>5.1}s", elapsed, remaining),
-                    Style::default().fg(Color::DarkGray),
-                ),
-            ]),
+            Line::from(vec![Span::styled(
+                format!("elapsed {:>5.1}s  remaining {:>5.1}s", elapsed, remaining),
+                Style::default().fg(Color::DarkGray),
+            )]),
             progress_bar(progress, inner.width),
         ]
     } else {
         // Running: the duration is a display-only estimate ("this will
         // probably take X seconds"). No progress bar — the workload runs
-        // until it self-exits or the user quits. The user can see CPU
-        // load via aldermon.
-        [
+        // until it self-exits or the user quits. Past the estimate the
+        // label goes yellow + says so (the 7z-overrun "is it hung?"
+        // UAT complaint): the sweep is not stalled, the workload just
+        // runs to the end of its fixed passes. The user can see CPU load
+        // via aldermon.
+        let overrun = elapsed >= spec.duration_secs as f64 + 2.0;
+        let mut lines = vec![
             Line::from(vec![
-                Span::styled(format!("{:<20}", spec.name), Style::default().add_modifier(Modifier::BOLD)),
+                Span::styled(
+                    format!("{:<20}", spec.name),
+                    Style::default().add_modifier(Modifier::BOLD),
+                ),
                 Span::raw("  "),
                 Span::styled(format!("isa={:<8}", isa), Style::default().fg(Color::Cyan)),
                 Span::raw("  "),
@@ -852,16 +931,31 @@ fn draw_current(f: &mut Frame, area: Rect, app: &App) {
                     Style::default().fg(Color::DarkGray),
                 ),
                 Span::raw("  "),
-                Span::styled(
-                    format!("elapsed {:>5.1}s", elapsed),
-                    Style::default(),
-                ),
+                Span::styled(format!("elapsed {:>5.1}s", elapsed), Style::default()),
             ]),
             Line::from(Span::styled(
-                "running... (watch CPU load in aldermon, press q to cancel)",
-                Style::default().fg(Color::DarkGray),
+                if overrun {
+                    format!(
+                        "past ~{}s estimate — running to completion (self-terminating; q cancels)",
+                        spec.duration_secs
+                    )
+                } else {
+                    "running... (watch CPU load in aldermon, press q to cancel)".to_string()
+                },
+                Style::default().fg(if overrun {
+                    Color::Yellow
+                } else {
+                    Color::DarkGray
+                }),
             )),
-        ]
+        ];
+        if overrun {
+            lines.push(Line::from(Span::styled(
+                "7z runs fixed benchmark passes — overrunning the estimate is normal.",
+                Style::default().fg(Color::DarkGray),
+            )));
+        }
+        lines
     };
     for (i, line) in lines.iter().enumerate() {
         if (i as u16) >= inner.height {
@@ -869,7 +963,12 @@ fn draw_current(f: &mut Frame, area: Rect, app: &App) {
         }
         f.render_widget(
             Paragraph::new(line.clone()),
-            Rect { x: inner.x, y: inner.y + i as u16, width: inner.width, height: 1 },
+            Rect {
+                x: inner.x,
+                y: inner.y + i as u16,
+                width: inner.width,
+                height: 1,
+            },
         );
     }
 }
@@ -884,11 +983,18 @@ fn progress_bar(ratio: f64, width: u16) -> Line<'static> {
     let mut spans = vec![Span::raw("[")];
     for i in 0..bar_w {
         let ch = if i < fill { "█" } else { "·" };
-        let color = if i < fill { Color::Green } else { Color::DarkGray };
+        let color = if i < fill {
+            Color::Green
+        } else {
+            Color::DarkGray
+        };
         spans.push(Span::styled(ch.to_string(), Style::default().fg(color)));
     }
     spans.push(Span::raw("] "));
-    spans.push(Span::styled(label, Style::default().add_modifier(Modifier::BOLD)));
+    spans.push(Span::styled(
+        label,
+        Style::default().add_modifier(Modifier::BOLD),
+    ));
     Line::from(spans)
 }
 
@@ -926,7 +1032,10 @@ fn draw_results(f: &mut Frame, area: Rect, app: &App) {
             Cell::from(Span::raw(format!("{}", r.idx))),
             Cell::from(Span::raw(r.name.clone())),
             Cell::from(Span::raw(r.isa_class.to_string())),
-            Cell::from(Span::styled(vlabel, Style::default().fg(vcolor).add_modifier(Modifier::BOLD))),
+            Cell::from(Span::styled(
+                vlabel,
+                Style::default().fg(vcolor).add_modifier(Modifier::BOLD),
+            )),
             Cell::from(Span::raw(format_score_short(&r.score))),
         ])
     });
@@ -960,16 +1069,23 @@ fn draw_footer(f: &mut Frame, area: Rect, _app: &App) {
 // Helpers
 // ---------------------------------------------------------------------------
 
-/// Compute the sleep duration for the "workload running, no re-draw"
-/// path. Sleeps for the remaining estimated time + a 5s margin, so the
-/// TUI process is genuinely idle (not scheduling) while the workload
-/// runs. After the sleep, the next tick will detect the workload has
-/// exited (or still running, in which case we sleep again).
+/// Compute the heartbeat sleep for the "workload running, don't poll at
+/// 250ms" path: remaining estimate + 5s margin, capped at 15s so the
+/// panel's elapsed refreshes even on long runs (session 6's freeze
+/// complaint), and settling at ~6s once the estimate is overrun (so
+/// self-exiting workloads like 7z get detected within a short window
+/// after their final pass). The 15s cap keeps the TUI at ~0% CPU — the
+/// failure mode that forced the original one-long-sleep design was the
+/// 2s poll cadence stealing CPU from single-core runs.
 fn compute_sleep_secs(app: &mut App) -> u64 {
     let elapsed = app.run.elapsed_secs();
-    let duration = app.run.current_spec().map(|s| s.duration_secs as f64).unwrap_or(60.0);
+    let duration = app
+        .run
+        .current_spec()
+        .map(|s| s.duration_secs as f64)
+        .unwrap_or(60.0);
     let remaining = (duration - elapsed).max(1.0);
-    remaining as u64 + 5
+    (remaining as u64 + 5).min(15)
 }
 
 fn format_score_short(s: &Score) -> String {
@@ -986,6 +1102,24 @@ fn format_score_short(s: &Score) -> String {
                 format!("{} tests", tests.len())
             }
         }
+        Score::C2c { pairs } => {
+            let ns: Vec<f64> = pairs.iter().map(|p| p.ns_per_hop).collect();
+            ns_range(&ns)
+        }
+        Score::Dram { levels } => {
+            let ns: Vec<f64> = levels.iter().map(|l| l.ns_per_access).collect();
+            ns_range(&ns)
+        }
+    }
+}
+
+fn ns_range(ns: &[f64]) -> String {
+    if ns.is_empty() {
+        "(no samples)".to_string()
+    } else {
+        let lo = ns.iter().cloned().fold(f64::INFINITY, f64::min);
+        let hi = ns.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
+        format!("{lo:.0}-{hi:.0} ns")
     }
 }
 
@@ -996,4 +1130,37 @@ fn verdict_badge(v: &Verdict) -> (&'static str, Color) {
         Verdict::Stopped => ("STOPPED", Color::Yellow),
         Verdict::Error(_) => ("ERROR", Color::Red),
     }
-}
\ No newline at end of file
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    fn editor_at_cores() -> EditorState {
+        let mut state = EditorState::from_cfg(Config::default());
+        state.cursor = 0;
+        state.field = EditorField::Cores;
+        state
+    }
+
+    #[test]
+    fn enter_on_cores_cycles_options_not_free_text() {
+        let mut state = editor_at_cores();
+        assert_eq!(state.cfg.workloads[0].cores, "0-15");
+        editor_key(&mut state, KeyCode::Enter);
+        assert_eq!(state.cfg.workloads[0].cores, "0-11");
+        assert!(state.editing.is_none());
+        editor_key(&mut state, KeyCode::Enter);
+        assert_eq!(state.cfg.workloads[0].cores, "12-15");
+        editor_key(&mut state, KeyCode::Enter);
+        assert_eq!(state.cfg.workloads[0].cores, "0-15");
+    }
+
+    #[test]
+    fn enter_on_cores_unknown_value_restarts_at_first() {
+        let mut state = editor_at_cores();
+        state.cfg.workloads[0].cores = "3,7".to_string();
+        editor_key(&mut state, KeyCode::Enter);
+        assert_eq!(state.cfg.workloads[0].cores, "0-15");
+    }
+}
diff --git a/alderbench/src/workload.rs b/alderbench/src/workload.rs
index 92488c0..9db9a52 100644
--- a/alderbench/src/workload.rs
+++ b/alderbench/src/workload.rs
@@ -62,6 +62,30 @@ pub enum Score {
     /// the verdict but per-test so a partial run still records what
     /// completed. For the configured FFTv4-only sweep there's one entry.
     YCruncher { tests: Vec<YCruncherTest> },
+    /// Core-to-core cache-line latency. One entry per pinned logical-cpu
+    /// pair (upper triangle of the configured cores range);
+    /// `ns_per_hop` is the average time for one cache-line handoff.
+    C2c { pairs: Vec<C2cPair> },
+    /// Memory latency via pointer-chase. One entry per buffer size
+    /// (L1/L2/L3/DRAM buckets); `ns_per_access` is the average
+    /// dependent-load latency at that working-set size.
+    Dram { levels: Vec<DramLevel> },
+}
+
+/// One c2c pair measurement.
+#[derive(Clone, Debug, PartialEq)]
+pub struct C2cPair {
+    pub cpu_a: usize,
+    pub cpu_b: usize,
+    pub ns_per_hop: f64,
+}
+
+/// One dram-latency working-set measurement.
+#[derive(Clone, Debug, PartialEq)]
+pub struct DramLevel {
+    /// Chase buffer size, KiB.
+    pub size_kb: u64,
+    pub ns_per_access: f64,
 }
 
 /// One y-cruncher algorithm result.
@@ -244,7 +268,10 @@ impl Workload for StressNg {
         "stress-ng-cpu"
     }
     fn params(&self) -> String {
-        format!("{}s cores={} --cpu --cpu-method all", self.duration_secs, self.cores)
+        format!(
+            "{}s cores={} --cpu --cpu-method all",
+            self.duration_secs, self.cores
+        )
     }
     fn start(&mut self) -> io::Result<()> {
         let mut cmd = Command::new("stress-ng");
@@ -430,9 +457,15 @@ impl Workload for YCruncher {
                 if status.success() && !failed_text {
                     Verdict::Clean
                 } else if failed_text {
-                    Verdict::Failed(format!("y-cruncher self-check failed (variant={})", self.variant))
+                    Verdict::Failed(format!(
+                        "y-cruncher self-check failed (variant={})",
+                        self.variant
+                    ))
                 } else {
-                    Verdict::Failed(format!("y-cruncher exit {} (variant={})", status, self.variant))
+                    Verdict::Failed(format!(
+                        "y-cruncher exit {} (variant={})",
+                        status, self.variant
+                    ))
                 }
             }
             Ok(None) => Verdict::Error("no child".to_string()),
@@ -575,25 +608,42 @@ fn taskset_command<P: AsRef<std::ffi::OsStr>>(cores: &str, program: P) -> Comman
 }
 
 /// Build a Workload from a config spec. Returns None for unknown names
-/// (the run loop skips them with a warning) or names that aren't wired
-/// yet (c2c-latency, dram-latency, minecraft-server — deferred).
+/// or names that aren't wired yet (minecraft-server — deferred), so the
+/// run loop skips them with a warning.
 pub fn from_spec(
     spec: &crate::config::WorkloadSpec,
     bin_dir: &std::path::Path,
 ) -> Option<Box<dyn Workload>> {
     match spec.name.as_str() {
-        "stress-ng-cpu" => Some(Box::new(StressNg::new(spec.duration_secs, spec.cores.clone()))),
-        "y-cruncher-sse" => {
-            Some(Box::new(YCruncher::sse(spec.duration_secs, spec.cores.clone(), bin_dir.to_path_buf())))
-        }
+        "stress-ng-cpu" => Some(Box::new(StressNg::new(
+            spec.duration_secs,
+            spec.cores.clone(),
+        ))),
+        "y-cruncher-sse" => Some(Box::new(YCruncher::sse(
+            spec.duration_secs,
+            spec.cores.clone(),
+            bin_dir.to_path_buf(),
+        ))),
         "y-cruncher-avx2" => Some(Box::new(YCruncher::avx2(
             spec.duration_secs,
             spec.cores.clone(),
             bin_dir.to_path_buf(),
         ))),
-        "7zip-bench" => Some(Box::new(SevenZip::new(spec.duration_secs, spec.cores.clone()))),
-        // Deferred — hand-rolled in-process workloads, next chunk.
-        "c2c-latency" | "dram-latency" | "minecraft-server" => None,
+        "7zip-bench" => Some(Box::new(SevenZip::new(
+            spec.duration_secs,
+            spec.cores.clone(),
+        ))),
+        // In-process hand-rolled workloads (see crate::inproc).
+        "c2c-latency" => Some(crate::inproc::c2c_latency(
+            spec.duration_secs,
+            spec.cores.clone(),
+        )),
+        "dram-latency" => Some(crate::inproc::dram_latency(
+            spec.duration_secs,
+            spec.cores.clone(),
+        )),
+        // Still deferred — needs PaperMC API verification.
+        "minecraft-server" => None,
         _ => None,
     }
 }
@@ -692,7 +742,11 @@ fn parse_ycruncher_scores(stdout: &str) -> Vec<YCruncherTest> {
         if tests.iter().any(|t| t.tag == tag) {
             continue;
         }
-        tests.push(YCruncherTest { tag, passed, bits_per_sec });
+        tests.push(YCruncherTest {
+            tag,
+            passed,
+            bits_per_sec,
+        });
     }
     tests
 }
@@ -719,18 +773,24 @@ fn parse_7zip_score(stdout: &str) -> Option<Score> {
             let Some((left, right)) = rest.split_once('|') else {
                 continue;
             };
-            let lc: Vec<u64> = left.split_whitespace().filter_map(|s| s.parse().ok()).collect();
-            let rc: Vec<u64> = right.split_whitespace().filter_map(|s| s.parse().ok()).collect();
+            let lc: Vec<u64> = left
+                .split_whitespace()
+                .filter_map(|s| s.parse().ok())
+                .collect();
+            let rc: Vec<u64> = right
+                .split_whitespace()
+                .filter_map(|s| s.parse().ok())
+                .collect();
             if lc.len() >= 4 && rc.len() >= 4 {
-                avr = Some((
-                    lc[0], lc[1], lc[2], lc[3],
-                    rc[0], rc[1], rc[2], rc[3],
-                ));
+                avr = Some((lc[0], lc[1], lc[2], lc[3], rc[0], rc[1], rc[2], rc[3]));
             }
         } else if line.starts_with("Tot:") {
             // Tot: <usage> <r_u> <rating>
             let rest = line.strip_prefix("Tot:").unwrap_or("").trim();
-            let nums: Vec<u64> = rest.split_whitespace().filter_map(|s| s.parse().ok()).collect();
+            let nums: Vec<u64> = rest
+                .split_whitespace()
+                .filter_map(|s| s.parse().ok())
+                .collect();
             if nums.len() >= 3 {
                 tot = Some((nums[0], nums[1], nums[2]));
             }
@@ -815,20 +875,32 @@ mod tests {
     }
 
     #[test]
-    fn from_spec_rejects_deferred_workloads() {
-        // c2c-latency / dram-latency / minecraft-server are in scope but
-        // not yet implemented — from_spec returns None so the run loop
-        // can skip them with a warning rather than panic.
-        for name in ["c2c-latency", "dram-latency", "minecraft-server"] {
+    fn from_spec_wires_in_process_names() {
+        for name in ["c2c-latency", "dram-latency"] {
             let spec = crate::config::WorkloadSpec {
                 name: name.to_string(),
                 duration_secs: 30,
                 cores: "0-15".to_string(),
             };
-            assert!(from_spec(&spec, std::path::Path::new("bin")).is_none(), "{name} should be None");
+            let w = from_spec(&spec, std::path::Path::new("bin"));
+            assert!(w.is_some(), "{name} should be wired");
+            assert_eq!(w.unwrap().name(), name);
         }
     }
 
+    #[test]
+    fn from_spec_rejects_deferred_workloads() {
+        // minecraft-server is in scope but not yet implemented (needs
+        // PaperMC API verification) — from_spec returns None so the run
+        // loop can skip it with a warning rather than panic.
+        let spec = crate::config::WorkloadSpec {
+            name: "minecraft-server".to_string(),
+            duration_secs: 30,
+            cores: "0-15".to_string(),
+        };
+        assert!(from_spec(&spec, std::path::Path::new("bin")).is_none());
+    }
+
     // --- score parser tests ---
 
     /// Real y-cruncher FFTv4 stdout (single-algorithm run). Has ANSI
@@ -927,7 +999,14 @@ Test Finished. Waiting for threads to terminate...
         let cmd = taskset_command("0", "/bin/true");
         assert_eq!(cmd.get_program(), std::path::Path::new("taskset"));
         let args: Vec<_> = cmd.get_args().collect();
-        assert_eq!(args, [std::ffi::OsStr::new("-c"), std::ffi::OsStr::new("0"), std::ffi::OsStr::new("/bin/true")]);
+        assert_eq!(
+            args,
+            [
+                std::ffi::OsStr::new("-c"),
+                std::ffi::OsStr::new("0"),
+                std::ffi::OsStr::new("/bin/true")
+            ]
+        );
         // Also accepts a PathBuf (y-cruncher's canonicalized variant path).
         let path = std::path::PathBuf::from("/bin/true");
         let cmd = taskset_command("3", &path);
@@ -940,7 +1019,11 @@ Test Finished. Waiting for threads to terminate...
     fn taskset_command_skips_wrapper_for_all_and_empty() {
         for full in ["0-15", "all", ""] {
             let cmd = taskset_command(full, "/bin/true");
-            assert_eq!(cmd.get_program(), std::path::Path::new("/bin/true"), "full={full:?} should skip wrapper");
+            assert_eq!(
+                cmd.get_program(),
+                std::path::Path::new("/bin/true"),
+                "full={full:?} should skip wrapper"
+            );
         }
     }
-}
\ No newline at end of file
+}
diff --git a/aldermon/README.md b/aldermon/README.md
index efa956c..569c625 100644
--- a/aldermon/README.md
+++ b/aldermon/README.md
@@ -76,4 +76,4 @@ doas usermod -aG adm $USER   # then re-login
 ## Status
 
 Working: TUI, config (+ system /etc default), RAPL power. Remaining
-notes in `to-do.md`.
\ No newline at end of file
+notes live in the local-only `.LLM_Memory/` (gitignored).
\ No newline at end of file
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..04c6a4e
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,3 @@
+## Alder Lake Tools
+
+Simple vCore monitoring and bench utilities for Intel 12th gen. Probably works for other machines, but I have a 12600K so that's what it's made for.