plan: roadmap refresh

This commit is contained in:
smarm
2026-06-13 18:58:42 +02:00
parent cc4000a165
commit b0c9685c89
+84 -29
View File
@@ -34,23 +34,15 @@ Commits `e5d1b3b`, `24b95c9`, `f6969e5`.
The run-queue shootout (harness `6d9f369`, 24-core sweep 124 schedulers,
report `bench_report_rq_shootout.html`) landed in **RFC 005's World 3**: the
three queue variants are within 1015% of each other in `rq_runtime` at every
scheduler count ≥ 4, on all three workloads. Striped's real micro-bench
dominance (24.8 M ops/s at 24t, 4.3× over mutex) does not propagate to the
runtime because the queue is not the hot path in any measured workload. USL
fits put σ at 5.57.6 (super-serial) for ping-pong: the ceiling is per-wake
latency through the park/unpark protocol, not queue push/pop.
scheduler count ≥ 4, on all three workloads.
Consequences:
- **`rq-mutex` stays the default** — simplest correct, no capacity
constraints, locking model already integrated with the preemption-disabled
queue-op invariant.
constraints, locking model already integrated.
- **Feature plumbing stays as is.** All three variants keep compiling in
every build; `rq-mpmc`/`rq-striped` remain selectable for benching.
- **Reopening is benchmark-driven only.** The report documents the
conditional upgrade paths if a future workload qualifies: mpmc for
message-passing-dominant loads at N ≤ 8 (1.3× lower σ, 0.16 µs ping-pong
at 1t); striped for high-contention balanced push/pop at N ≥ 16 — neither
is a scheduler workload as measured.
message-passing-dominant loads at N ≤ 8; striped for high-contention balanced push/pop at N ≥ 16. Neither is a scheduler workload as measured.
- **Effort redirects to the wake path**: RFC 005 (billed as a latency patch,
per its own World 3 framing), RFC 004, and eventually per-switch cost.
@@ -94,6 +86,7 @@ it's a Config knob, not a feature rebuild). Same sweep as the rq shootout.
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.
### 3. Spinning workers (RFC 004)
Bounded spin-before-park for idle schedulers, killing the ~100 µs
@@ -101,6 +94,7 @@ Bounded spin-before-park for idle schedulers, killing the ~100 µs
`spin_budget_cycles` (0 recovers today's behaviour) and `max_spinners`
(default N/2); one new atomic `n_spinning`. No run-queue changes — lands on
the frozen `rq-mutex` substrate, independent of the slot mechanically.
Credit for the idea: Dennis Gustafsson - [Parallelizing the physics solver BSC 2025](https://www.youtube.com/watch?v=Kvsvd67XUKw).
### 4. RFC 004 bench + interaction pass
Re-run the sweep with spinning enabled, slot on and off. The known
@@ -112,8 +106,52 @@ shared instead" check. In scope only if the data demands it.
---
## Later
### Highest priority
#### 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
first. The point is that urus pubsub collapses into a pg consumer (`subscribe` =
join, `broadcast` = send-to-members) instead of being a bespoke mechanism, and the
same group set reads two ways — fan-out (all members) vs discovery/pool (one
member), with different netsplit consequences. Urus shipped pubsub/channels predate this
and want reframing on top of it. Foundational, so early in the post-v0.9 stack.
Needs an RFC.
### Unwakeable idle sleep when io is absent (terminal-wake residual)
#### 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.
#### send_after / cancel_timer
Message-delivery timer on the existing min-heap (`timer.rs`): deliver a value to a
channel at a deadline, cancellable. Unlocks the gen_server idioms with no clean
expression today — heartbeat, debounce, retry backoff, session expiry. Small;
#### Introspection — process_info / get_state / tree dump
`trace.rs` is the seed. What an actor is parked on, queue depth, stack size; a
gen_server state snapshot; a supervision-tree walk. The native edge over tokio —
actors already carry pid, name, parent where tokio tasks are anonymous — so it
costs little and differentiates a lot. Needs an RFC.
#### Worker pool behaviour
Supervised, interchangeable workers with restart semantics over a shared inbox
(poolboy / NimblePool shape) — distinct from connection pools (bb8/deadpool), which
pool *resources*, not *supervised processes*. Sits on `supervisor.rs` +
`gen_server.rs`. Needs an RFC.
### Medium Priority
#### Demand-driven pipelines — GenStage / Broadway shape
Supervised producer/consumer stages where consumers signal demand upstream, with
batching, ack, partitioning. The clearest thing hex has and crates.io lacks (stream
combinators and bounded channels are not a supervised demand-contract stage graph),
and the natural fit for ingestion-shaped workloads. Builds on channels + gen_server
+ supervisor. Needs an RFC.
#### Unwakeable idle sleep when io is absent (terminal-wake residual)
The `(Some(deadline), None)` idle branch — timers pending, io subsystem never
initialized — blocks in `thread::sleep` with no wake mechanism at all. The
terminal wake (writes the wake pipe at AllDone) cannot reach it: no io, no
@@ -124,33 +162,49 @@ turns idle into periodic wakeups), or (b) park the branch on a condvar/futex
the AllDone path signals — and at that point consider making the condvar the
idle primitive for the no-io runtime generally (a cross-thread unpark could
signal it too, see below). Decide before any no-io deployment.
### Cross-thread unpark
#### Cross-thread unpark
`RuntimeInner::enqueue` does not wake idle sibling schedulers — only io
completions write the wake pipe. Mid-flight this is masked (the enqueuing
thread is awake and eats the work itself), but it costs parallelism: work
enqueued by a busy thread waits until the sibling's idle poll times out.
Candidate from the urus chunk-2 session; needs bench evidence (does the
shared-queue handoff latency actually show up?) before a mechanism is picked.
### 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.
### Unbounded / configurable-bounded actor count
enqueued by a busy thread waits until the sibling's idle poll times out. Needs bench evidence (does the shared-queue handoff latency actually show up?) before a mechanism is picked.
#### Unbounded / configurable-bounded actor count
Fixed slab with a loud assert (`Config::max_actors(n)`, default 16 384).
Revisit with a segmented slab (array of `AtomicPtr<Segment>`, doubling segment
sizes, append-only) once the cap is actually hit. Do not let it calcify.
### arm-port validation & merge
#### arm-port validation & merge
`arm-port` branch carries an AAPCS64 context-switch backend, never run on
hardware. Build + run full test suite on an aarch64 device; check
`chained_spawn` / `yield_many` bench medians; merge and update README.
### Low priority
#### gen_statem — postponement + state timeouts only
A thin layer over gen_server, not a new behaviour. The (state, event) dispatch
matrix is free from the type system and not worth porting. The two mechanisms that
are: event **postponement** (defer events in the wrong state, replay on transition
— selective receive, codified) and **state timeouts** (auto-cancel on state
change). Device-connection FSMs are the canonical use. Wants send_after underneath.
Needs an RFC.
#### Clustering — distribution epic
Sequenced deliberately after v0.9 and the per-switch-cost spike. A fat stack of
RFCs, not one. Spine settled in discussion; decisions still open:
- **Explicit remote boundary, never transparency.** Serialization colours *edges*
(channel types), not functions — local edges stay zero-copy `Send`, only remote
edges take a `RemoteRef<T: Serialize + DeserializeOwned>`. No hidden latency when
a peer migrates; the refactor is visible by construction.
- **One binary, role as runtime config** (`ROLE=… REGION=… SEEDS=…`); a build-hash
handshake enforces same-binary type identity and sidesteps cross-version type
agreement. Roles select which supervision subtree mounts.
- **Distributed pg falls out of local pg + a membership/gossip layer**, and
distributed pubsub falls out of that for free; per-member metadata (region, load)
enables fly-style nearest-member routing.
- **Migratable gen_servers** as a sub-layer: only behaviours migrate (a raw actor's
stack is opaque; a gen_server *between callbacks* is just its `State`), gated by
`Serialize` bounds + an `on_arrive` reacquire hook, addressed by name not pid. The
BEAM can't do this — leaning on the behaviour layer is what buys it. Requires `State` to be serializable, so should probaby spec a `trait MigratableGenServer: GenServer where Self::State: Migratable` , or something to that extent, so we can lean on the type system to make sure we don't accidentally make state that cannot be serialised.
- **CRDT presence** is the high-value, genuinely-hard layer above distributed pg,
kept *out* of the pg primitive (the pg2 strong-consistency lesson). Furthest out. Can maybe defer to rust ecosystem
---
@@ -202,3 +256,4 @@ Agent working on urus (see same git server as smarm) reported a lazily spawned a
`RawMutex` guards all disable preemption for their span.
- **`run()` is single-thread** (`Config::exact(1)`); tests rely on deterministic
single-thread ordering. Multi-thread via `runtime::init(Config…)`.