fix(rfc-004): gate spinning on max_spinners>0, killing wake-nobody futex_wake at N=1

`spinning` was `spin_budget_cycles > 0` alone. At N=1 the pool-size cap is 0, so
spinning was enabled while no scheduler could ever enlist: n_spinning pinned at 0,
so the enqueue submit-rule guard (n_spinning==0) fired a wake-nobody futex_wake on
every push. Now gated on `budget > 0 && max_spinners > 0`; a debug_assert guards
the invariant against regression. No behavioural change (a 0-cap runtime never
parks on the futex either way) — single_thread_spinning_is_inert and the AllDone
liveness test (shutdown_releases_pinned_parked_siblings) still pass.

switch_cost N=1 (1-core sandbox): p50 ~466ns -> ~186ns. The wake-nobody syscall
was ~60% of the round-trip in wall-clock terms; the handoff's "~12%" was perf
self-time, not latency share. ROADMAP per-switch section updated to fold in the
N=1 spike correction (shim hypothesis is many-core only). Dropped the throwaway
budget_probe bench (switch_cost reproduces every state via .spin_budget_cycles/
.max_spinners and with percentiles).
This commit is contained in:
smarm-agent
2026-06-14 20:01:02 +00:00
parent debcb4e33b
commit 2f011f6ce4
2 changed files with 54 additions and 10 deletions
+28 -5
View File
@@ -120,11 +120,34 @@ Needs an RFC.
#### Per-switch cost (context shims, epoch protocol)
The shootout's residual: per-wake latency is 0.160.18 µs at N=1 and
0.81.2 µs at N=8+, dominated by the context-switch shims and the epoch
protocol, not the queue. On current evidence this is the larger constant —
"the whole game" alongside the v0.9 work — but there is no spec yet. Needs a
profiling spike (where do the cycles actually go per park/unpark round-trip)
and then an RFC before it can be scheduled.
0.81.2 µs at N=8+. The N=1 profiling spike is **done** (`docs/perswitch-profile-n1.md`):
it **revises the old shim-first framing**. At N=1 the context shims + TLS sp
accessors are ~1% of self-time, not the dominant cost — the shim hypothesis was
always a *many-core* one (its only evidence was the N=1→N=8 jump, blamed on TLS
access mode and cross-core coherency on the sp/epoch words), and nothing at N=1
on one core can confirm or refute it. The N=1 round-trip is instead dominated by
the scheduler core (`schedule_loop` + run-queue, ~33%) plus a removable
wake-nobody `futex_wake`.
That `futex_wake` is now **fixed**: it was an interaction between the RFC 004
spin defaults and the N=1 pool-size cap of 0 — `spinning` was gated on
`budget > 0` alone, so at N=1 spinning was enabled while the cap forbade any
spinner from enlisting, pinning `n_spinning` at 0 and firing a wake-nobody
`futex_wake` on every enqueue. `spinning` is now gated on
`budget > 0 && max_spinners > 0` (a `debug_assert` guards the invariant). On the
1-core sandbox this cut N=1 p50 from ~466 ns to ~186 ns — the syscall was ~60%
of the round-trip in wall-clock terms (the handoff's "~12%" was perf *self-time*,
not latency share).
Two separable targets remain: **(a)** the N=1 floor — `schedule_loop` + run-queue,
the irreducible scheduler core, no obvious cheap win left; **(b)** the N→8 slope —
shim / TLS / coherency, which needs the 5900X (HW-counter profiling; this sandbox
has no PMU). Still "the whole game" alongside v0.9. Next code chunk is a `remote`
mode for `benches/switch_cost.rs` (wake straddling two schedulers) — writable in
the sandbox, only measurable on the box. Do **not** optimise the shim until the
N=8 HW-counter data confirms it is the cost. Then an RFC; the
`spinning`-gating fix above may also warrant a small RFC since it touches the
RFC-004-settled futex path.
#### send_after / cancel_timer
Message-delivery timer on the existing min-heap (`timer.rs`): deliver a value to a