feat(causal): ledger audit — decompose the @50 injection deficit (RFC 007)
Measure-only counters for the deficit hunt (~23ms short per 700ms window at 50% on the bottleneck site; superlinear vs 25%). Nothing here changes injection or absorption; the sweep decides the fix. Buckets, windowed per cell into new ExperimentResult fields (audit snapshot taken at end() — spin/attribution freeze there, forgiveness does not): - spin_absorbed / park_forgiven: where owed delay actually went. Spin during a 0% cell is the baseline-contamination signature — leftover debt from a prior window being paid in a later one (checks gate on the experiment word, so cooldowns pay nothing and debt carries over). - drop_park / drop_yield (+counts): the deschedule path flushes no sample tail — an in-target-site park or yield silently loses [last sample -> now]; on_resume re-arms before the actor runs again. New on_deschedule hook in all three intent arms (real park; explicit/ slice-expiry yield; requeued park counts as yield — it never blocked). Slice-expiry yields sample at the descheduling checkpoint, so a fat yield bucket points at explicit yield_now or requeued parks. - discard_overmax (+count, in would-be delta terms so columns compare against injected_cycles) / discard_unarmed: the attribute() clamps, previously silent. LedgerCounters + ledger_counters() expose cumulative totals (tests, run-level prints); render_ledger_audit() is the per-cell companion to render_summary, which stays byte-identical (pinned). ExperimentResult now derives Default so literals survive future audit-field growth. Tests: +7 (spin counted, forgiveness counted, in-site park drop, in-site yield drop, overmax discard, 0%-window leftover absorption — synthesized deterministically via inject_delay_cycles_for_test with no experiment active — and the audit render). 22/22 causal.
This commit is contained in:
+257
@@ -261,6 +261,7 @@ fn impact_from_synthetic_results() {
|
||||
duration: secs(1.0),
|
||||
deltas: vec![("units".into(), 1000)],
|
||||
injected_cycles: 0,
|
||||
..Default::default()
|
||||
},
|
||||
// Bottleneck at 50%: raw count unchanged, 0.5s injected
|
||||
// -> rate 1000/0.5 = 2x baseline -> +100%.
|
||||
@@ -270,6 +271,7 @@ fn impact_from_synthetic_results() {
|
||||
duration: secs(1.0),
|
||||
deltas: vec![("units".into(), 1000)],
|
||||
injected_cycles: cycles(0.5),
|
||||
..Default::default()
|
||||
},
|
||||
ExperimentResult {
|
||||
site: "overlapped".into(),
|
||||
@@ -277,6 +279,7 @@ fn impact_from_synthetic_results() {
|
||||
duration: secs(1.0),
|
||||
deltas: vec![("units".into(), 1000)],
|
||||
injected_cycles: 0,
|
||||
..Default::default()
|
||||
},
|
||||
// Overlapped at 50%: everyone else (incl. the real bottleneck)
|
||||
// slowed, count drops with the divisor -> ~0% impact.
|
||||
@@ -286,6 +289,7 @@ fn impact_from_synthetic_results() {
|
||||
duration: secs(1.0),
|
||||
deltas: vec![("units".into(), 500)],
|
||||
injected_cycles: cycles(0.5),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
let bn = impact_pct(&results, "bottleneck", 50, "units").unwrap();
|
||||
@@ -652,3 +656,256 @@ fn wall_send_after_cancels() {
|
||||
"cancelled wall send_after delivered"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Ledger audit (RFC 007 deficit hunt) — measure-only counters that decompose
|
||||
// where injected delay is born, paid, forgiven, and where would-be
|
||||
// attribution is silently lost. Behaviour under test is accounting only:
|
||||
// none of these tests assert a change in injection or absorption itself.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// A never-parking bystander's spin absorption lands in the audit's
|
||||
/// `spin_absorbed_cycles` — the "paid for real" bucket.
|
||||
#[test]
|
||||
fn audit_counts_spin_absorption() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_t = stop.clone();
|
||||
let target = smarm::spawn(move || {
|
||||
let _g = smarm::causal_site!("audit-spin-site");
|
||||
while !stop_t.load(Ordering::Relaxed) {
|
||||
smarm::check!();
|
||||
}
|
||||
});
|
||||
let stop_b = stop.clone();
|
||||
let bystander = smarm::spawn(move || {
|
||||
while !stop_b.load(Ordering::Relaxed) {
|
||||
smarm::check!();
|
||||
}
|
||||
});
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let g0 = smarm::causal::global_delay_cycles();
|
||||
smarm::causal::begin_experiment_for_test("audit-spin-site", 50);
|
||||
smarm::sleep(Duration::from_millis(150));
|
||||
smarm::causal::end_experiment_for_test();
|
||||
let injected = smarm::causal::global_delay_cycles() - g0;
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
target.join().unwrap();
|
||||
bystander.join().unwrap();
|
||||
|
||||
let spun = c1.spin_absorbed_cycles - c0.spin_absorbed_cycles;
|
||||
assert!(injected > 0, "experiment injected nothing");
|
||||
assert!(
|
||||
spun >= injected / 2,
|
||||
"audit missed spin absorption: {spun} of {injected} injected"
|
||||
);
|
||||
// Payers are bounded by the debt that exists; 2x headroom covers the
|
||||
// main actor's incidental checkpoint payments.
|
||||
assert!(
|
||||
spun <= injected.saturating_mul(2),
|
||||
"audit over-counted spin: {spun} of {injected} injected"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// A wake after a real park waives the sleeper's accrued debt; the waiver is
|
||||
/// tallied in `park_forgiven_cycles` (Coz's blocked-thread rule, now visible).
|
||||
#[test]
|
||||
fn audit_counts_park_forgiveness() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_t = stop.clone();
|
||||
let target = smarm::spawn(move || {
|
||||
let _g = smarm::causal_site!("audit-forgive-site");
|
||||
while !stop_t.load(Ordering::Relaxed) {
|
||||
smarm::check!();
|
||||
}
|
||||
});
|
||||
// Parks across the whole injection window; owes everything at wake.
|
||||
let sleeper = smarm::spawn(|| smarm::sleep(Duration::from_millis(120)));
|
||||
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let g0 = smarm::causal::global_delay_cycles();
|
||||
smarm::causal::begin_experiment_for_test("audit-forgive-site", 50);
|
||||
smarm::sleep(Duration::from_millis(60));
|
||||
smarm::causal::end_experiment_for_test();
|
||||
let injected = smarm::causal::global_delay_cycles() - g0;
|
||||
sleeper.join().unwrap();
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
target.join().unwrap();
|
||||
|
||||
let forgiven = c1.park_forgiven_cycles - c0.park_forgiven_cycles;
|
||||
assert!(injected > 0, "experiment injected nothing");
|
||||
assert!(
|
||||
forgiven >= injected / 2,
|
||||
"audit missed park forgiveness: {forgiven} of {injected} injected"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Parking while *inside* the target site drops the sample tail
|
||||
/// `[last sample -> park]` — nothing flushes it, and `on_resume` re-arms the
|
||||
/// clock. The audit counts that loss without changing it.
|
||||
#[test]
|
||||
fn audit_counts_park_drop_inside_target_site() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
smarm::causal::begin_experiment_for_test("audit-park-drop-site", 50);
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let h = smarm::spawn(|| {
|
||||
let _g = smarm::causal_site!("audit-park-drop-site");
|
||||
smarm::check!(); // arm an in-site sample interval
|
||||
smarm::sleep(Duration::from_millis(20)); // real park, in-guard
|
||||
});
|
||||
h.join().unwrap();
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
smarm::causal::end_experiment_for_test();
|
||||
|
||||
assert!(
|
||||
c1.drop_park_n > c0.drop_park_n,
|
||||
"in-site park recorded no drop event"
|
||||
);
|
||||
assert!(
|
||||
c1.drop_park_cycles > c0.drop_park_cycles,
|
||||
"in-site park drop carried no cycles"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// An explicit yield inside the target site loses its tail the same way; it
|
||||
/// lands in the separate yield bucket so park- and yield-losses stay apart.
|
||||
#[test]
|
||||
fn audit_counts_yield_drop_inside_target_site() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
smarm::causal::begin_experiment_for_test("audit-yield-drop-site", 50);
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let h = smarm::spawn(|| {
|
||||
let _g = smarm::causal_site!("audit-yield-drop-site");
|
||||
smarm::check!(); // arm an in-site sample interval
|
||||
smarm::yield_now();
|
||||
smarm::check!();
|
||||
});
|
||||
h.join().unwrap();
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
smarm::causal::end_experiment_for_test();
|
||||
|
||||
assert!(
|
||||
c1.drop_yield_n > c0.drop_yield_n,
|
||||
"in-site yield recorded no drop event"
|
||||
);
|
||||
assert!(
|
||||
c1.drop_yield_cycles > c0.drop_yield_cycles,
|
||||
"in-site yield drop carried no cycles"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// A single in-site interval past MAX_SAMPLE_CYCLES is discarded by the
|
||||
/// TSC-weirdness clamp; the audit counts the discard instead of losing it
|
||||
/// silently.
|
||||
#[test]
|
||||
fn audit_counts_overmax_discard() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
smarm::causal::begin_experiment_for_test("audit-overmax-site", 50);
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let h = smarm::spawn(|| {
|
||||
let _g = smarm::causal_site!("audit-overmax-site");
|
||||
// Stay in-site past MAX_SAMPLE_CYCLES (~33ms at 3GHz) without a
|
||||
// single checkpoint: no allocs, no check!. The guard-drop flush
|
||||
// then sees one huge interval and must discard it.
|
||||
let t0 = std::time::Instant::now();
|
||||
while t0.elapsed() < Duration::from_millis(60) {
|
||||
std::hint::spin_loop();
|
||||
}
|
||||
});
|
||||
h.join().unwrap();
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
smarm::causal::end_experiment_for_test();
|
||||
|
||||
assert!(
|
||||
c1.discard_overmax_n > c0.discard_overmax_n,
|
||||
"overmax interval recorded no discard event"
|
||||
);
|
||||
assert!(
|
||||
c1.discard_overmax_cycles > c0.discard_overmax_cycles,
|
||||
"overmax discard carried no cycles"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// Debt left outstanding when a window ends is paid during the NEXT active
|
||||
/// experiment — including a 0% baseline cell, whose bystander-chase branch is
|
||||
/// live at pct=0. Synthesized deterministically: grow the ledger with no
|
||||
/// experiment active (nobody pays), then watch a 0% window absorb it.
|
||||
#[test]
|
||||
fn audit_zero_pct_window_absorbs_leftover_debt() {
|
||||
let _s = serial();
|
||||
smarm::init(smarm::Config::exact(1)).run(|| {
|
||||
let hz = smarm::causal::tsc_hz();
|
||||
let stop = Arc::new(AtomicBool::new(false));
|
||||
let stop_b = stop.clone();
|
||||
let bystander = smarm::spawn(move || {
|
||||
while !stop_b.load(Ordering::Relaxed) {
|
||||
smarm::check!();
|
||||
}
|
||||
});
|
||||
|
||||
// No experiment active: checks early-return, debt sits unpaid.
|
||||
let debt = (hz * 0.020) as u64; // ~20ms
|
||||
smarm::causal::inject_delay_cycles_for_test(debt);
|
||||
smarm::sleep(Duration::from_millis(30));
|
||||
|
||||
let c0 = smarm::causal::ledger_counters();
|
||||
let g0 = smarm::causal::global_delay_cycles();
|
||||
smarm::causal::begin_experiment_for_test("audit-zero-pct-site", 0);
|
||||
smarm::sleep(Duration::from_millis(60));
|
||||
smarm::causal::end_experiment_for_test();
|
||||
let c1 = smarm::causal::ledger_counters();
|
||||
stop.store(true, Ordering::Relaxed);
|
||||
bystander.join().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
smarm::causal::global_delay_cycles(),
|
||||
g0,
|
||||
"a 0% window must inject nothing"
|
||||
);
|
||||
let spun = c1.spin_absorbed_cycles - c0.spin_absorbed_cycles;
|
||||
assert!(
|
||||
spun >= debt / 2,
|
||||
"leftover debt not absorbed in the 0% window: {spun} of {debt}"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/// The audit render lists one line per cell with the ledger columns.
|
||||
#[test]
|
||||
fn ledger_audit_renders_per_cell() {
|
||||
use smarm::causal::{render_ledger_audit, ExperimentResult};
|
||||
let results = vec![ExperimentResult {
|
||||
site: "audit-render-site".into(),
|
||||
speedup_pct: 50,
|
||||
duration: Duration::from_secs(1),
|
||||
deltas: vec![("units".into(), 10)],
|
||||
injected_cycles: 1_000_000,
|
||||
spin_absorbed_cycles: 900_000,
|
||||
park_forgiven_cycles: 50_000,
|
||||
drop_park_cycles: 30_000,
|
||||
drop_park_n: 2,
|
||||
drop_yield_cycles: 10_000,
|
||||
drop_yield_n: 1,
|
||||
discard_overmax_cycles: 5_000,
|
||||
discard_overmax_n: 1,
|
||||
discard_unarmed_n: 0,
|
||||
}];
|
||||
let s = render_ledger_audit(&results);
|
||||
assert!(s.contains("ledger audit"), "missing header: {s}");
|
||||
assert!(s.contains("audit-render-site"), "missing site line: {s}");
|
||||
assert!(s.contains("absorbed"), "missing absorbed column: {s}");
|
||||
assert!(s.contains("forgiven"), "missing forgiven column: {s}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user