feat(causal): wall-anchored timers — controller windows keep fixed wall length (RFC 007)

New timer anchor: insert_sleep_wall / scheduler::sleep_wall (exported) opt a
Sleep entry out of the RFC 007 virtual-time shift, so it fires at its raw
deadline regardless of injected delay. Featureless config is unchanged (the
API exists but is identical to sleep).

The causal controller's window/cooldown sleeps and the tsc_hz calibration
sleep use it on the actor path (the OS-thread path was already wall). This
fixes the controller's own sleeps dilating under its own injection —
experiment windows stretched ~2x at 50% speedup (337ms -> 646ms injected/
window). Deltas were rate-normalized so results were unbiased; this fixes
sweep cost, not bias. The general wall-anchored-timer-semantics jar item
(user-facing opt-out) remains open; this lands the substrate.

Test: wall_timer_ignores_injected_delay — wall entry fires at raw deadline
while a virtual sibling in the same heap shifts. 13/13 causal, 34/34
binaries both feature configs.
This commit is contained in:
Claude (sandbox)
2026-07-13 07:44:51 +00:00
parent 04dbac1f4b
commit efbc254634
5 changed files with 103 additions and 18 deletions
+28
View File
@@ -461,6 +461,34 @@ fn timer_zero_debt_pops_at_raw_deadline() {
assert!(t.is_empty());
}
/// A wall-anchored timer (RFC 007 controller fix) fires at its raw deadline
/// regardless of injected delay, while a virtual sibling in the same heap
/// still shifts. This is what keeps the causal controller's experiment
/// windows a fixed wall length even under heavy injection.
#[test]
fn wall_timer_ignores_injected_delay() {
let _s = serial();
use smarm::pid::Pid;
use smarm::timer::Timers;
use std::time::Instant;
let hz = smarm::causal::tsc_hz();
let mut t = Timers::new();
let now = Instant::now();
t.insert_sleep_wall(now + Duration::from_millis(50), Pid::new(0, 0), 1);
t.insert_sleep(now + Duration::from_millis(50), Pid::new(1, 0), 1);
// 100ms of debt lands while both are pending.
smarm::causal::inject_delay_cycles_for_test((hz * 0.100) as u64);
// Just past the raw deadline: the wall entry fires, the virtual one is
// re-queued at its shifted deadline.
let due = t.pop_due(now + Duration::from_millis(60));
assert_eq!(due.len(), 1, "exactly the wall entry must fire at raw deadline");
assert_eq!(due[0].pid, Pid::new(0, 0));
assert!(!t.is_empty(), "virtual sibling must remain queued, shifted");
}
/// Re-queueing a shifted entry must preserve its identity: a `send_after`
/// cancelled *after* being shifted past its raw deadline must still cancel
/// (return true) and must never deliver.