fix(causal): flush target-site samples at guard boundaries (RFC 007)

Samples were taken only when maybe_preempt's cold block happened to fire
in-site, so the interval between the last check and SiteGuard drop was
discarded on every site entry. Measured live on the 24-core box:
22-29us lost per entry, a constant attribution efficiency of ~0.93-0.94,
which under-reported every impact (+83.5% where theory says +100%; the
observed shortfall fits 1/(1-pct*eff)-1 at both 25% and 50%).

SiteGuard enter/drop now call site_transition(): leaving the target site
flushes the pending interval into the ledger (sample-only, never spins,
so safe under no-preempt regions); entering the target site re-arms the
sample clock so pre-site time is never attributed (the symmetric
over-attribution). Winner attribution is factored into attribute(),
shared by the cold check and the flush, with the same interval clamps.

Adds examples/causal_attrib_probe.rs (ground-truth in-site time vs
ledger attribution, the probe that confirmed the leak) and the
site_boundaries_flush_tail regression test (a site entry that never
hits a cold check must still be attributed). Also gates causal_probe
on smarm-causal in Cargo.toml - it never was, so featureless builds
of the examples were broken.
This commit is contained in:
Claude (sandbox)
2026-07-12 19:35:06 +00:00
parent 2668f4018f
commit d496914d40
4 changed files with 295 additions and 10 deletions
+56
View File
@@ -332,3 +332,59 @@ fn controller_produces_report() {
assert!(coz.contains("throughput-point\tname=items"), "coz: {coz}");
});
}
/// A target-site entry that never hits a cold causal check must still be
/// attributed: the guard boundaries flush the pending interval on exit and
/// re-arm the clock on entry. Without the exit flush, in-site time between
/// the last check and guard drop is silently discarded — measured live at
/// ~6-7% of all target time (eff 0.93), under-reporting every impact
/// (+83.5% where theory says +100%).
#[test]
fn site_boundaries_flush_tail() {
fn tsc() -> u64 {
// SAFETY: rdtsc has no preconditions on x86_64.
unsafe { core::arch::x86_64::_rdtsc() }
}
let injected = Arc::new(AtomicU64::new(0));
let spent = Arc::new(AtomicU64::new(0));
let inj_out = injected.clone();
let spent_out = spent.clone();
smarm::init(smarm::Config::exact(1)).run(move || {
// Experiment is live before the actor ever enters the site.
smarm::causal::begin_experiment_for_test("tail-site", 50);
let base = smarm::causal::global_delay_cycles();
let a = smarm::spawn(move || {
let t0 = tsc();
{
let _g = smarm::causal_site!("tail-site");
// Burn ~20M cycles with no allocations and no `check!()`:
// the amortised cold check can never fire, so only the
// guard-boundary flush can attribute this time.
let start = tsc();
while tsc().wrapping_sub(start) < 20_000_000 {
core::hint::spin_loop();
}
}
spent_out.store(tsc().wrapping_sub(t0), Ordering::Relaxed);
});
a.join().unwrap();
smarm::causal::end_experiment_for_test();
inj_out.store(
smarm::causal::global_delay_cycles() - base,
Ordering::Relaxed,
);
});
let injected = injected.load(Ordering::Relaxed);
let spent = spent.load(Ordering::Relaxed);
// At 50% the flush should attribute ≈ half the in-guard time; allow a
// generous band for guard overhead and clock skew.
assert!(
injected >= spent * 40 / 100 && injected <= spent * 60 / 100,
"boundary flush attributed {injected} of {spent} spent cycles (want ~50%)"
);
}