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
+40 -15
View File
@@ -108,6 +108,13 @@ pub struct Entry {
/// workload instead of firing early in virtual terms.
#[cfg(feature = "smarm-causal")]
delay_stamp: u64,
/// RFC 007: a wall-anchored entry opts out of the virtual-time shift —
/// its deadline is honoured in wall time regardless of injected delay.
/// Used by the causal controller's own measurement/cooldown sleeps so
/// experiment windows keep a fixed wall length; ordinary workload timers
/// stay virtual (`false`).
#[cfg(feature = "smarm-causal")]
wall: bool,
}
impl PartialEq for Entry {
@@ -158,6 +165,19 @@ impl Timers {
self.insert(deadline, pid, Reason::Sleep { epoch });
}
/// Insert a *wall-anchored* `Sleep` timer: fires at `deadline` in wall
/// time even while causal profiling (feature `smarm-causal`) is injecting
/// virtual delay — it never chases the delay ledger. Without the feature
/// this is identical to [`insert_sleep`](Self::insert_sleep).
///
/// Intended for measurement machinery (the causal controller's window and
/// cooldown sleeps, TSC calibration) whose durations *define* wall time
/// rather than participate in the workload. Workload code should use the
/// ordinary virtual-anchored timers.
pub fn insert_sleep_wall(&mut self, deadline: Instant, pid: Pid, epoch: u32) {
self.push(deadline, pid, Reason::Sleep { epoch }, true);
}
/// Arm a cancellable `send_after` timer: run `fire` at `deadline` unless
/// [`cancel`](Self::cancel)led first. `pid` is informational only (the
/// destination, or who armed it — useful for introspection); it is *not*
@@ -169,18 +189,8 @@ impl Timers {
pid: Pid,
fire: Box<dyn FnOnce() + Send>,
) -> TimerId {
let seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
self.armed.insert(seq);
self.heap.push(Reverse(Entry {
deadline,
seq,
pid,
reason: Reason::Send { fire },
#[cfg(feature = "smarm-causal")]
delay_stamp: crate::causal::global_delay_cycles(),
}));
TimerId(seq)
self.armed.insert(self.next_seq);
TimerId(self.push(deadline, pid, Reason::Send { fire }, false))
}
/// Cancel an armed `send_after` timer. Returns `true` if the timer was
@@ -192,8 +202,18 @@ impl Timers {
self.armed.remove(&id.0)
}
/// Insert an arbitrary timer entry.
/// Insert an arbitrary (virtual-anchored) timer entry.
pub fn insert(&mut self, deadline: Instant, pid: Pid, reason: Reason) {
self.push(deadline, pid, reason, false);
}
/// Common insertion path. `wall` selects the RFC 007 anchor (see
/// [`insert_sleep_wall`](Self::insert_sleep_wall)); it is accepted — and
/// ignored — without the `smarm-causal` feature so callers don't fork.
/// Returns the entry's `seq`.
fn push(&mut self, deadline: Instant, pid: Pid, reason: Reason, wall: bool) -> u64 {
#[cfg(not(feature = "smarm-causal"))]
let _ = wall;
let seq = self.next_seq;
self.next_seq = self.next_seq.wrapping_add(1);
self.heap.push(Reverse(Entry {
@@ -203,7 +223,10 @@ impl Timers {
reason,
#[cfg(feature = "smarm-causal")]
delay_stamp: crate::causal::global_delay_cycles(),
#[cfg(feature = "smarm-causal")]
wall,
}));
seq
}
pub fn is_empty(&self) -> bool {
@@ -239,7 +262,9 @@ impl Timers {
/// [`peek_deadline`](Self::peek_deadline) may under-report (raw deadline
/// earlier than effective), costing at most one spurious scheduler wake
/// per injected chunk; and a shift never converts wall time — with zero
/// debt the path is byte-identical to the featureless one.
/// debt the path is byte-identical to the featureless one. Wall-anchored
/// entries ([`insert_sleep_wall`](Self::insert_sleep_wall)) are exempt
/// from the shift and always fire at their raw deadline.
pub fn pop_due(&mut self, now: Instant) -> Vec<Entry> {
let mut out = Vec::new();
#[cfg(feature = "smarm-causal")]
@@ -260,7 +285,7 @@ impl Timers {
continue;
}
#[cfg(feature = "smarm-causal")]
{
if !entry.wall {
let debt = global.saturating_sub(entry.delay_stamp);
if debt > 0 {
let shifted = entry