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
+12 -2
View File
@@ -457,7 +457,12 @@ mod inner {
if preempt::current_slot_ptr().is_null() {
std::thread::sleep(d);
} else {
crate::scheduler::sleep(d);
// Wall-anchored: the controller's window/cooldown sleeps
// *define* the experiment's wall length; letting them chase
// the delay it is itself injecting would stretch every window
// (observed ~2x at 50% speedup). Deltas are rate-normalized
// either way — this fixes cost, not bias.
crate::scheduler::sleep_wall(d);
}
}
// Calibrate before any window so report rendering never has to sleep.
@@ -527,7 +532,12 @@ mod inner {
if preempt::current_slot_ptr().is_null() {
std::thread::sleep(d);
} else {
crate::scheduler::sleep(d);
// Wall-anchored: calibration divides TSC delta by *wall*
// elapsed; a virtual sleep dilated by concurrent injection
// would still measure correctly (elapsed() is wall) but
// waste window time — and must never depend on the ledger
// it exists to convert.
crate::scheduler::sleep_wall(d);
}
(preempt::rdtsc().wrapping_sub(c0)) as f64 / t0.elapsed().as_secs_f64()
})