diff --git a/ROADMAP.md b/ROADMAP.md index 6607119..2a9a232 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,20 +2,9 @@ ## Shipped (compacted — full cycle plans and deviation records live in git history) -Cycles before v0.7 (v0.4 actor primitives, v0.5 runtime decomposition & -pluggable run queue, v0.6 actor ergonomics): see `git log ROADMAP.md`. - -### v0.7 — select, on epoch-stamped consuming wakes ✅ -A 24-bit park-epoch packed into the slot word gives every wait an identity: -registrations carry `(pid, epoch)`, every successful wake **consumes** the -epoch, stale wakes die at one failed CAS. Subsumed the per-primitive wait -seqs (channel, mutex, timer); the only wildcard wake left is `request_stop` -(terminal). On top: `select`/`select_timeout` — ready-index wait over many -receivers, priority order, no cancellation pass. Loom theorems re-proved on -the new word. Notable deviations: `retire_wait` for the no-park exit path -(plan missed it); the Drop-guard deregistration was superseded by relaxed -single-receiver asserts; a closed arm is ready *forever* (documented gotcha). -Commits `4913835`…`00128f3`, `a0a93b6`. +Cycles before v0.8 (v0.4 actor primitives, v0.5 runtime decomposition & +pluggable run queue, v0.6 actor ergonomics, v0.7 select on epoch-stamped +consuming wakes): see `git log ROADMAP.md`. ### v0.8 — gen_server: handle_info / handle_down + io fd hygiene ✅ Spent `select` on the server loop: static info arms (`type Info`, @@ -27,6 +16,19 @@ entry permanently poisoned the fd). Deviation: the plain-inbox fast path narrowed; servers holding a `Watcher` select forever. Commits `e5d1b3b`, `24b95c9`, `f6969e5`. +### v0.9 — Wake-path latency ✅ +Attacked per-wake latency with the RFC 005 **wake slot**: a per-scheduler, +thread-local, capacity-one wake cache checked before the shared queue, pushed +only from actor context, slot-then-shared pop with the waker's residual slice +as the starvation bound (`slot_hits`/`slot_displacements`). Benched via the +slot on/off dimension of `rq_runtime` — ping-pong-pairs (win), yield-storm +(regression guard), spawn-storm (neutrality); results annotated in RFC 005. +The RFC 004 spinning-workers experiment, originally scoped here, was evaluated +and **excised** (not worth the code cost; preserved on branch +`rfc-004-spinning`). Also a false-sharing fix (`align(64)` on `SchedulerStats`) +and a termination wake for idle siblings. +Commits `2708042`, `37d9319`, `eddf3fe`. + --- ## Decision record — queue topology 🔒 CLOSED (2026-06-10) @@ -48,51 +50,7 @@ Consequences: --- -## v0.9 — Wake-path latency - -Goal: attack per-wake latency — the measured ceiling — with the two specified -mechanisms, each benched against a clean baseline before the next lands. -Order: RFC 005 first (the slot is measured against the just-benched idle -policy), then RFC 004 (measured against the slot-accepted baseline), then an -interaction pass. - -Full specs: `rfc_005-wake-slot.md`, `rfc_004-tunable-scheduler-idle-policy.md` -(artefacts.kalsbeek.dev). - -### 1. Wake slot (RFC 005) -Per-scheduler, thread-local, capacity-one wake cache, checked before the -shared queue. Runtime-selected via `Config { wake_slot: bool }`, default off -until accepted (one binary benches both arms). -- **Push policy:** slot-eligible iff the wake originates from actor context - (`current_pid().is_some()`). Scheduler-context wakes (timer/IO drain) and - spawns always go shared. -- **Displacement:** newest wake takes the slot, occupant pushed shared (Go - semantics); the branch swap is the fallback if benches look pathological. -- **Pop order:** slot, then shared. Slot-popped actors inherit the waker's - remaining timeslice — a handoff chain is bounded by one slice, so the - shared queue is consulted at least once per slice per scheduler (the - starvation bound, zero new counters). -- **Invariant care:** the slot push replaces `run_queue.push` at the tail of - the `Parked → Queued` CAS; at-most-once-enqueued holds verbatim (pid in - slot ⊕ shared queue). Stall blast radius grows by exactly one actor. -- Counters: `slot_hits`, `slot_displacements`. - -### 2. Slot shootout -Extend `rq_runtime`/`bench_rq.sh` with the slot on/off dimension (cheap — -it's a Config knob, not a feature rebuild). Same sweep as the rq shootout. -- **ping-pong-pairs** — target metric; expect the win, single- and - multi-scheduler. -- **yield-storm** — regression guard; yields never touch the slot, any delta - is pop-path overhead. -- **spawn-storm** — neutrality check; spawns bypass the slot by policy. -Acceptance flips the default on and re-baselines for RFC 004. -This is done, see results annotated in RFC005. - ---- - -## Later -### Highest priority -#### Process groups: the primitive pubsub & channels should have sat on +## Up next — Process groups: the primitive pubsub & channels should have sat on Context: urus is a webserver written on top of smarm to provide a testing target. A named pid→multiset map with monitor-backed removal: `registry.rs` generalised from name↔pid *bimap* to name→*multiset*, the death hook reused verbatim. Local @@ -103,6 +61,10 @@ member), with different netsplit consequences. Urus shipped pubsub/channels pred and want reframing on top of it. Foundational, so early in the post-v0.9 stack. Needs an RFC. +--- + +## Later +### Highest priority #### Per-switch cost (context shims, epoch protocol) The shootout's residual: per-wake latency is 0.16–0.18 µs at N=1 and 0.8–1.2 µs at N=8+, dominated by the context-switch shims and the epoch