josie / alder-tools

Fix y-cruncher Stopped verdict: add 2s grace to duration timer

y-cruncher's -D:T time limit includes setup time (allocation, topology
detection) — wall time is T + ~1s. The run loop's duration timer fired
at exactly T, just before y-cruncher self-exited, producing a false
Stopped verdict (and no score, since the Test Speed line hadn't printed
yet). Fix: only stop on the duration timer after T + 2s grace. If the
workload self-exits (is_running() returns false), finish immediately
with no grace — the grace only applies to workloads that overrun.

Same issue the smoke harness has a 1s grace for; the run loop didn't.
Verified live: y-cruncher-sse + y-cruncher-avx2 both now get Clean
verdicts + scores parsed (1.01 + 1.00 Gb/s) with 15s duration.

b7771c9b7f39ba2068b5a6c729789d1f66f6c048
josie <administrator@josie-c.com> · 2026-09-01T22:33 · browse files at this commit

parents: feb1f8f

diff --git a/src/run.rs b/src/run.rs
index f3c8114..2f79bf2 100644
--- a/src/run.rs
+++ b/src/run.rs
@@ -231,8 +231,19 @@ impl Run {
                     .as_mut()
                     .map(|w| w.is_running())
                     .unwrap_or(false);
-                if !running || elapsed >= Duration::from_secs(*duration_secs) {
-                    // Workload finished (on its own or we stop it now).
+                if !running {
+                    // Workload self-exited — finish immediately.
+                    return self.finish_current_workload();
+                }
+                // Workload still running. Stop it when the duration
+                // elapses, PLUS a grace period. Some workloads (y-cruncher)
+                // include setup time (allocation, topology detection)
+                // in their `-D:T` time limit, so their wall time is T + ~1s.
+                // Without the grace, the run loop's duration timer fires
+                // just before the workload self-exits → false Stopped
+                // verdict. The grace matches the smoke harness's 1s margin.
+                let grace = Duration::from_secs(2);
+                if elapsed >= Duration::from_secs(*duration_secs) + grace {
                     return self.finish_current_workload();
                 }
                 Ok(TickOutcome::Idle)
@@ -472,7 +483,7 @@ mod tests {
     #[test]
     fn stopped_verdict_when_duration_elapses() {
         // MockWorkload that never self-exits (ticks_until_exit huge);
-        // the run's duration timer must stop it → Stopped verdict.
+        // the run's duration timer + grace must stop it → Stopped verdict.
         let _ = std::fs::remove_dir_all("/tmp/opencode/run-tests");
         let cfg = mock_cfg(vec![
             WorkloadSpec { name: "never-exits".to_string(), duration_secs: 0, cores: "0-15".to_string() },
@@ -480,6 +491,13 @@ mod tests {
         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 the workload (Cooldown → Running).
+        let _ = run.tick().unwrap();
+        // Manually rewind state_start so elapsed exceeds duration (0) +
+        // the 2s grace. Without this the test would need to sleep 2s.
+        run.state_start = Instant::now()
+            .checked_sub(Duration::from_secs(3))
+            .unwrap_or(Instant::now());
         let mut verdict = None;
         for _ in 0..10 {
             if let TickOutcome::WorkloadFinished { verdict: v, .. } = run.tick().unwrap() {
@@ -487,7 +505,7 @@ mod tests {
                 break;
             }
         }
-        // duration_secs=0 → first Running tick sees elapsed >= 0 → stops.
+        // elapsed (3s) >= duration (0) + grace (2s) → stops.
         assert_eq!(verdict, Some(Verdict::Stopped));
     }