The probe now prints eff+offcpu next to eff — attributed plus the
counted runnable off-CPU gaps over ground-truth in-site time — the
per-window check that the located mechanism accounts for the whole
residual (~1.00 = books closed, no remaining silent loss). Audit line
gains the offcpu column, same delta-terms convention as the lib
renderer. Header doc rewritten from hypothesis to resolution.
- causal_pipeline: SMARM_CAUSAL_AUDIT=1 appends render_ledger_audit()
after the summary; pinned summary format untouched.
- causal_attrib_probe: per-pct audit line (absorbed/forgiven/drops/
discards) under the existing eff line, so in-site-vs-attributed and
the loss buckets land in one place for the sweep.
1-core smoke (work + wide): books balance — absorbed = 2x injected and
forgiven = 2x injected, i.e. owed = injected x (N-1) with N=5 actors,
zero outstanding. drop park = 0 even in wide mode (the bottleneck's
queue is never empty, so its in-guard recv never parks); drop yield is
~1500 events but ~0.1ms per window, confirming slice-expiry yields
sample at their own checkpoint. Deficit decomposition needs the
parallel box.
Occupancy probe on 24 cores: δ = 0.3µs/item (0.1% of the serialized
path); wide guard leaves the @50% cell unchanged. The +84-vs-+100
shortfall is controller-side (injected 327ms of the ideal 350ms over
the 700ms window, plus ~3% real-rate dip during experiments), not
unguarded stage time. urus's ~70µs/request remainder remains the
guard-placement case; the occupancy probe discriminates the two.
SMARM_CAUSAL_MODE selects the reserve stage's guard placement:
work (default, unchanged) | wide (guard over recv+work+send, the whole
serialized per-item path) | occupancy (no experiments; per-segment
timing of reserve's loop at baseline, reporting the unguarded
remainder δ and the impact ceiling it implies).
Discriminates the two candidate explanations for the demo's +84-vs-+100
@50% shortfall: physical recv/send time outside the guard (occupancy
sees δ≈30µs, wide recovers ~2x) vs. injection-side credit loss
(occupancy sees δ≈0, wide caps at ~+84 too — guard cadence identical).
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.
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.
A `=> postpone` row (cast/call/info) defers the current event untouched,
to be replayed after the next real transition. `handle` is now two-phase:
a borrow-only postpone pre-pass that hands the event back as
`Step::Postponed(ev)`, then the existing consuming `match (state, event)`.
The loop owns a FIFO queue, drained in the new state ahead of further
intake; a replayed event may postpone again. A postponed `call` keeps its
Reply, so a later state answers it.
`handle` returns `Step` (Postponed / Transitioned / Stayed) so the loop
can see both deferral and transition without reading the state cell.
`Resolution::Postpone` is removed: postpone is a pre-dispatch routing
decision, not a consuming-dispatch outcome.
Add the two timeout flavours, both surfacing as ordinary events matched
in on-state arms:
- cx.state_timeout(d): fires a state_timeout event after d in the current
state, auto-reset by the loop on every real transition.
- cx.timeout(name, d): fires a timeout(name) event after d, surviving state
changes, keyed by name, with cx.cancel_timeout(name).
Both ride the existing timer min-heap via send_after_to onto a new per-loop
system channel, selected above the inbox so a fire can't be starved by inbox
traffic. A local-id stamp on each fire lets a reset/cancel that loses the race
discard a stale fire. The macro grows an info: clause and folds three internal
Ev variants (Info, StateTimeout, Timeout) alongside cast/call, with new row
keywords info / state_timeout / timeout. Unmatched info silently drops (the
gen_server default); state/named timeouts have no default, so a state that can
see one must handle it or the match is non-exhaustive.
Rename the hand-written expansion-target example fused -> expanded, retire the
deprecated Switch demo machine (its round-trip / enter / panic-down coverage
moves onto the timer machine), and refresh the macro docs to the door machine.
cargo build --all-targets warning-free; cargo test green.
- rename StatemRef/StatemCallError/StatemSendError -> GenStatem*
- move the inline unit test out of src; consolidate the Switch coverage
onto a single macro-driven harness in tests/gen_statem.rs
- drop the redundant hand-written Switch test machine and the two
untracked rejected-direction probes (succ_enums, typed_edges)
- rename examples statem_{fused,macro}.rs -> gen_statem_{fused,macro}.rs
- strip RFC/chunk/spike provenance and fix the mislabeled "throwaway"
example header and dead cross-references
Declarative macro_rules! that fuses the hand-written statem surface into
one invocation: emits the unified event enum, the machine struct + state
cell, start, the Machine impl (dispatch + stay/transition apply-tail), and
the enter dispatch. User keeps the meaningful types, the per-state
successor enums, and the free handler fns.
Pure sugar: every safety property is a property of the emitted code,
checked by rustc, so a declarative macro carries (almost) the proc-macro
guarantee set:
1. forgotten (state,event) pair -> E0004 (total match, no injected _)
2. conflicting row -> unreachable_patterns (macro self-denies; HARD only
in-crate, suppressed cross-crate by in_external_macro -- documented)
3. orphan handler -> dead_code (handlers are user free fns)
4. out-of-set target -> E0599 (per-state successor enums)
Hygiene: bodies can't see the macro's self/cx, so the caller names them
via `context(data, prev, cx)` (shared call-site hygiene).
No separate transitions{} block: the match IS the table, successor enums
ARE the per-state target sets (fused variant, diverges from RFC
edge-lint).
- examples/statem_macro.rs: Door machine via the macro (parallel to the
hand-written examples/statem_fused.rs; diff the two to see the delta).
- in-crate test exercises a machine + anchors the in-crate #2 guarantee.
Runtime support layer for gen_statem, built against existing public API
(channel + scheduler::spawn), sibling to gen_server. No macro: per review,
build the primitives first and hand-write the Switch example to evaluate
whether a statem! macro earns its place before committing to one.
- src/statem.rs: Machine trait (on_start/handle), Resolution<S> with
From<S>, Cx (on_unhandled), Reply<T> move-only reply handle, StatemRef
(send/call), spawn + inbox loop. Real time only; Postpone + Cx timeout
arming are in the type surface but not yet acted on (chunks 2-3).
- examples/statem_switch.rs: the RFC Switch machine hand-written against the
primitives, tagged USER vs MACRO to mark what a macro would generate.
Asserts the RFC end-state (flips=1, enters=3).
- tests/statem.rs: call/cast round-trip, enter-on-start/transition-not-stay,
panicking-handler -> Down.
Reply<T> included (the call helper needs a handle type; keeps the example
true to the RFC surface) but isolated and trivially removable if we drop it.
A runnable examples/observer.rs (required-features = ["observer"]) that
stands up a named service + two parked workers, starts the observer, and
renders a snapshot as a ps-style table and the parentage forest indented.
The observer appears in its own dump, caught running while it serves the
snapshot call — transport over the same read every consumer sees.
Complements the runnable doctest already on observer::start.
typed_actor and named_genserver land as written (the target-ergonomics spec):
identity-bound Pid<A> vs durable Name<M>, and a gen_server addressed by a
durable ServerName. worker_pool is reworked from the spec into a self-draining
program: workers retire on a sentinel and report their tally, and the
dispatcher waits the pool out — so it terminates on its own rather than leaning
on root-exit teardown, while still exercising the full typed surface
(spawn_addr / join / pick_as / members_as / dispatch) alongside the untyped
escape hatch (members / pick + send_dyn).
All three build unchanged-against-spec (typed_actor, named_genserver) and run
to completion.
A minimal Config::exact(1) program with two actors doing a fixed number of
yield_now() calls. No I/O/timers/channels, so the instruction stream stays
centered on the naked-asm switch. Frame-pointer-friendly. Intended for
single-threaded inspection under a debugger (e.g. llmdbg).