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:
@@ -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%)"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user