feat(causal): native causal profiling behind smarm-causal (RFC 007 v1)
causal_site! scoped site guards per actor slot, progress! throughput points, and a Coz-style virtual-speedup engine hooked into maybe_preempt's amortized cold block: target-site samples grow a global delay ledger; bystanders spin-absorb their debt at the next causal check, with timeslice extension so injected delay is not charged against the slice. Resume credit (Coz's blocked-thread rule) is gated on a causal_parked slot bit set only by a real park: crediting on every resume made any yield-cadence actor delay-immune and every experiment inert (found live on a 24-core run — dead-flat deltas across all sites). Report normalization uses a measured TSC frequency (~50ms calibration on first use) instead of the crate-wide 3 GHz assumption, which uniformly inflated impact numbers on a 3.7 GHz box. impact_pct() is the machine-readable form of the summary for programmatic checks. examples/causal_pipeline.rs burns fixed *work* (calibrated LCG loop), not fixed wall time — a timed busy-wait absorbs injected delay into its own budget and reads as a no-op. Self-checking: exits nonzero if causal separation fails; skips the verdict below 4 cores. Validated on a 24-core box: reserve (true bottleneck) +29.3%@25/+83.5%@50; serialize and background-compaction ~0%. Known v1 gaps (jar): timer-heap deadlines unshifted, no-check!/no-alloc actors undelayable, multi-scheduler coherence best-effort Relaxed, off-CPU blame punted, Instant::now() uncorrected. Zero-cost with the feature off; clippy -D warnings clean both ways; full suite green with and without smarm-causal.
This commit is contained in:
@@ -445,6 +445,28 @@ pub(crate) struct Slot {
|
||||
/// and only when the `budget-accounting` feature is on (it costs two RDTSC
|
||||
/// per resume); stays 0 otherwise. Same single-writer Relaxed discipline.
|
||||
budget_cycles: AtomicU64,
|
||||
/// RFC 007 (`smarm-causal`) — id of the causal-profiling site this actor
|
||||
/// is currently inside (0 = none). Lives in the slot, not a thread-local,
|
||||
/// so it survives preemption and cross-scheduler migration. Written only
|
||||
/// by the actor itself (guard enter/exit on its own thread), read by that
|
||||
/// same thread in `maybe_preempt` — single-writer Relaxed, like `overruns`.
|
||||
/// Exists regardless of the feature; stays 0 without it.
|
||||
causal_site: AtomicU32,
|
||||
/// RFC 007 (`smarm-causal`) — virtual-speedup delay cycles this actor has
|
||||
/// absorbed (or been credited). Compared against the global ledger in
|
||||
/// `causal::check`; fast-forwarded on resume-from-park so blocked time
|
||||
/// absorbs delay for free (Coz's blocked-thread rule). Same single-writer
|
||||
/// discipline.
|
||||
causal_delay: AtomicU64,
|
||||
/// RFC 007 (`smarm-causal`) — true iff this actor's last deschedule was a
|
||||
/// real park (a successful `park_return`, not a slice yield and not the
|
||||
/// instant-wake re-queue). Consumed by `causal::on_resume`: only a wake
|
||||
/// from genuine blocking forgives outstanding virtual delay; a merely
|
||||
/// preempted (runnable) actor stays in debt and must pay by spinning.
|
||||
/// Starts true so a fresh spawn doesn't inherit the process's whole delay
|
||||
/// history. Written by the owning scheduler thread at the deschedule /
|
||||
/// resume boundary only — same single-writer discipline as `causal_delay`.
|
||||
causal_parked: AtomicBool,
|
||||
/// Cold lifecycle data. See [`SlotCold`].
|
||||
pub(crate) cold: RawMutex<SlotCold>,
|
||||
}
|
||||
@@ -459,6 +481,9 @@ impl Slot {
|
||||
overruns: AtomicU64::new(0),
|
||||
messages_received: AtomicU64::new(0),
|
||||
budget_cycles: AtomicU64::new(0),
|
||||
causal_site: AtomicU32::new(0),
|
||||
causal_delay: AtomicU64::new(0),
|
||||
causal_parked: AtomicBool::new(true),
|
||||
cold: RawMutex::new(SlotCold {
|
||||
actor: None,
|
||||
waiters: Vec::new(),
|
||||
@@ -547,6 +572,61 @@ impl Slot {
|
||||
self.overruns.store(0, Ordering::Relaxed);
|
||||
self.messages_received.store(0, Ordering::Relaxed);
|
||||
self.budget_cycles.store(0, Ordering::Relaxed);
|
||||
self.causal_site.store(0, Ordering::Relaxed);
|
||||
self.causal_delay.store(0, Ordering::Relaxed);
|
||||
// True, not false: the first resume of a fresh actor is a "wake" —
|
||||
// it must not owe the process's accumulated delay history.
|
||||
self.causal_parked.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// RFC 007 — mark that this actor's deschedule was a genuine park.
|
||||
/// Called from the scheduler's `YieldIntent::Park` branch on a successful
|
||||
/// `park_return` only.
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn set_causal_parked(&self) {
|
||||
self.causal_parked.store(true, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// RFC 007 — consume the parked marker at resume: returns whether the
|
||||
/// last deschedule was a real park, and clears it so the next resume
|
||||
/// defaults to "was runnable" unless the park branch says otherwise.
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn take_causal_parked(&self) -> bool {
|
||||
self.causal_parked.swap(false, Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// RFC 007 — current causal site id (0 = none). Single-writer: only the
|
||||
/// on-CPU actor's thread writes, via the site-guard enter/exit.
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn causal_site(&self) -> u32 {
|
||||
self.causal_site.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// RFC 007 — write the current causal site id (guard enter/exit).
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn set_causal_site(&self, id: u32) {
|
||||
self.causal_site.store(id, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// RFC 007 — absorbed/credited virtual-delay cycles.
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn causal_delay(&self) -> u64 {
|
||||
self.causal_delay.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
/// RFC 007 — set the absorbed-delay ledger (spin-absorb, credit, or the
|
||||
/// resume-path fast-forward). Single-writer per the resume protocol: the
|
||||
/// actor's own thread while on-CPU, the resuming scheduler thread at the
|
||||
/// resume boundary — never both at once.
|
||||
#[cfg_attr(not(feature = "smarm-causal"), allow(dead_code))]
|
||||
#[inline]
|
||||
pub(crate) fn set_causal_delay(&self, v: u64) {
|
||||
self.causal_delay.store(v, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
/// A pid's-eye snapshot of the slot. Cold paths re-read this under the
|
||||
@@ -1646,6 +1726,11 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
crate::preempt::reset_timeslice();
|
||||
}
|
||||
PREEMPTION_ENABLED.with(|c| c.set(true));
|
||||
// RFC 007: delay accrued while this actor was off-CPU is absorbed for
|
||||
// free (Coz's blocked-thread rule) — fast-forward its ledger and arm
|
||||
// the per-thread sample clock before it runs.
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
crate::causal::on_resume(slot);
|
||||
|
||||
crate::te!(crate::trace::Event::Resume(pid));
|
||||
unsafe { switch_to_actor() };
|
||||
@@ -1680,6 +1765,11 @@ fn schedule_loop(inner: &Arc<RuntimeInner>, slot_idx: usize) {
|
||||
}
|
||||
YieldIntent::Park => {
|
||||
if slot.word.park_return(gen) {
|
||||
// RFC 007: a real park — the eventual wake forgives
|
||||
// delay accrued while blocked (the instant-wake
|
||||
// re-queue below does NOT: that actor never blocked).
|
||||
#[cfg(feature = "smarm-causal")]
|
||||
slot.set_causal_parked();
|
||||
crate::te!(crate::trace::Event::Park(pid));
|
||||
} else {
|
||||
// An unpark landed in the prep-to-park window; the
|
||||
|
||||
Reference in New Issue
Block a user