feat(causal): timer-heap virtual time — deadlines chase injected delay (RFC 007)
Injected delays dilate virtual time for the workload, but timer deadlines stayed wall-anchored: a sleep or receive-timeout fired early in virtual terms, so timeout/retry behaviour sped up relative to the dilated world (v1 known gap #1). Every heap Entry now carries delay_stamp — the global delay ledger at (re-)queue time, cfg-gated on smarm-causal. pop_due converts any debt accrued since the stamp to wall time (tsc_hz) and shifts the effective deadline; a not-yet-due entry is re-queued at the shifted deadline with a fresh stamp, so it keeps chasing delay injected while it waits. seq is preserved across re-queues, keeping send_after cancellation identity intact (cancelled entries are discarded before any shift). Zero debt is byte-identical to the old path; peek_deadline may under-report, costing one spurious scheduler wake per injected chunk (documented). This also makes the park-gated resume credit *correct* rather than forgiving for sleepers: a sleeping actor now physically pays its debt by sleeping longer, so the on_resume fast-forward reflects real payment (sleeping_actor_pays_injected_delay pins this end-to-end through the runtime). New test hooks: inject_delay_cycles_for_test (deterministic ledger driver, eagerly TSC-calibrating so conversion never stalls a scheduler loop) and cycles_to_duration. The ledger is process-global, so the delta-sensitive causal tests now serialize on a shared test mutex — they were racy under the parallel test harness before this, in principle.
This commit is contained in:
@@ -397,6 +397,20 @@ mod inner {
|
||||
end();
|
||||
}
|
||||
|
||||
/// Test support: grow the global delay ledger directly, as if target-site
|
||||
/// samples had attributed `cycles` — deterministic driver for the timer
|
||||
/// virtual-time tests. Calibrates the TSC eagerly so conversion later
|
||||
/// never stalls a scheduler loop.
|
||||
pub fn inject_delay_cycles_for_test(cycles: u64) {
|
||||
let _ = tsc_hz();
|
||||
GLOBAL_DELAY.fetch_add(cycles, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// Convert ledger cycles to wall time at the measured TSC rate.
|
||||
pub fn cycles_to_duration(cycles: u64) -> Duration {
|
||||
Duration::from_secs_f64(cycles as f64 / tsc_hz())
|
||||
}
|
||||
|
||||
/// Controller parameters: which speedups to try per site, and the
|
||||
/// experiment/cooldown windows.
|
||||
pub struct ExperimentPlan {
|
||||
|
||||
+69
-13
@@ -102,6 +102,12 @@ pub struct Entry {
|
||||
seq: u64,
|
||||
pub pid: Pid,
|
||||
pub reason: Reason,
|
||||
/// RFC 007 virtual time: the global delay ledger reading when this entry
|
||||
/// was (re-)queued. `pop_due` shifts the effective deadline by any delay
|
||||
/// injected since, so timers dilate together with the causally-delayed
|
||||
/// workload instead of firing early in virtual terms.
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
delay_stamp: u64,
|
||||
}
|
||||
|
||||
impl PartialEq for Entry {
|
||||
@@ -166,7 +172,14 @@ impl Timers {
|
||||
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 } }));
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -183,7 +196,14 @@ impl Timers {
|
||||
pub fn insert(&mut self, deadline: Instant, pid: Pid, reason: Reason) {
|
||||
let seq = self.next_seq;
|
||||
self.next_seq = self.next_seq.wrapping_add(1);
|
||||
self.heap.push(Reverse(Entry { deadline, seq, pid, reason }));
|
||||
self.heap.push(Reverse(Entry {
|
||||
deadline,
|
||||
seq,
|
||||
pid,
|
||||
reason,
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
delay_stamp: crate::causal::global_delay_cycles(),
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -210,22 +230,58 @@ impl Timers {
|
||||
/// one is silently dropped here (its `seq` was already removed from
|
||||
/// `armed` by [`cancel`](Self::cancel)). Returning it removes it from
|
||||
/// `armed`, so a later `cancel` of a fired timer reports `false`.
|
||||
///
|
||||
/// RFC 007 virtual time (feature `smarm-causal`): before an entry fires,
|
||||
/// any global delay injected since it was (re-)queued is added to its
|
||||
/// deadline; an entry whose *effective* deadline hasn't passed is pushed
|
||||
/// back with the shifted deadline and a fresh stamp, so it keeps chasing
|
||||
/// delay injected while it waits. Consequences, both benign:
|
||||
/// [`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.
|
||||
pub fn pop_due(&mut self, now: Instant) -> Vec<Entry> {
|
||||
let mut out = Vec::new();
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
let global = crate::causal::global_delay_cycles();
|
||||
while let Some(r) = self.heap.peek() {
|
||||
if r.0.deadline <= now {
|
||||
let entry = match self.heap.pop() {
|
||||
Some(e) => e.0,
|
||||
None => panic!("smarm: timer heap pop after peek returned None (core corrupt)"),
|
||||
};
|
||||
if matches!(entry.reason, Reason::Send { .. }) && !self.armed.remove(&entry.seq) {
|
||||
// Cancelled before it came due: discard, do not deliver.
|
||||
continue;
|
||||
}
|
||||
out.push(entry);
|
||||
} else {
|
||||
if r.0.deadline > now {
|
||||
break;
|
||||
}
|
||||
#[allow(unused_mut)]
|
||||
let mut entry = match self.heap.pop() {
|
||||
Some(e) => e.0,
|
||||
None => panic!("smarm: timer heap pop after peek returned None (core corrupt)"),
|
||||
};
|
||||
if matches!(entry.reason, Reason::Send { .. }) && !self.armed.contains(&entry.seq) {
|
||||
// Cancelled before it came due: discard, do not deliver.
|
||||
// (Checked before any shift so a cancelled entry is never
|
||||
// re-queued just to be discarded later.)
|
||||
continue;
|
||||
}
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
{
|
||||
let debt = global.saturating_sub(entry.delay_stamp);
|
||||
if debt > 0 {
|
||||
let shifted = entry
|
||||
.deadline
|
||||
.checked_add(crate::causal::cycles_to_duration(debt))
|
||||
.unwrap_or(entry.deadline);
|
||||
if shifted > now {
|
||||
// Not due in virtual time: re-queue at the shifted
|
||||
// deadline, stamped, keeping `seq` (and thus `Send`
|
||||
// cancellation identity) intact.
|
||||
entry.deadline = shifted;
|
||||
entry.delay_stamp = global;
|
||||
self.heap.push(Reverse(entry));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if matches!(entry.reason, Reason::Send { .. }) {
|
||||
self.armed.remove(&entry.seq);
|
||||
}
|
||||
out.push(entry);
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user