Pacing the pump's loop thread
A design note from the engineering record, moved out of ROADMAP.md on 2026-09-03 and kept as written. The roadmap itself now holds only the project's state; the reasoning lives here.
What the sweep measurement exposed: at c16 the pump's throughput depends on how long its loop thread spends per pass, in a way that an accidental 1.2 µs improved by 3%. An explicit pause before flushing a partial batch — spin N ns, optionally re-poll once and fold the new events' submits into the same datagram — is the deliberate form of that, and tunable where the sweep was not.
Measured the same day, as an experiment patch, and recorded rather
than built (bench/results/outbox-sweep/pacing/, fourteen arms, pump,
c16, stdlib asyncio, uvicorn asyncio within 58.3–59.3k on every one):
| pump's loop thread, per pass | rps @ cores |
|---|---|
| the sweep (today) | 59.95k @0.97 |
| no sweep, no pause | 58.70k @1.01 |
| spin 500 / 1000 / 2000 / 4000 ns before a partial flush only | 58.6–59.0k @0.97–1.01 |
the same, then wait(0) and fold the new events into the pass |
58.9–59.3k @1.05 |
| spin 1200 / 2000 ns on every pass | 60.04k / 60.13k @0.97 |
| the same, with the re-poll | 55.6–55.8k @1.08 |
Two things settled. The sweep's effect is a delay on every pass —
including the completion-only passes that write responses and park —
not on the pass that has submits to flush: pausing only before a
partial flush is too late, because by then the batch is whatever the
previous wait returned, while a pause after writing responses lets
the clients' next requests arrive before the loop parks. An explicit
per-pass spin of 1.2–2 µs reproduces the sweep to within noise, and no
longer pause improves on it. And the re-poll is simply worse: a nested
pass costs the loop thread more than a merged batch saves the executor.
So the pump keeps its pacing through the sweep it already runs, which
is neither prettier nor uglier than a spin and needs no knob; the
finding is that ~2% at c16 (and nothing at c256) is what per-pass
latency on the pump's loop thread is worth, and that it is already
collected.
The design, so it is not re-derived:
- Scope: unmounted single-executor ASGI only — the benchmark shape.
Every other topology stays on the pump. Behind
M0_INVERTED=1until the gate passes, so the A/B is one environment variable. - Submit: a defaulted
HTTPServicehook (direct_job(slot) -> Bool, default False — Phase 1 made adding one non-breaking). The loop still parks the request; on an executor lane it asks the handler first and sends the datagram only when it declines.WSGIHandlerin inverted mode answers by running the port's job branch, factored into one function shared with the port. - Complete: the port keeps parking responses; its per-iteration
_flushcallsservice_direct_completions[T,B](handler, backend, st, slots)— the per-slot body of_service_completions— instead of a datagram. The port grows the loop state's and backend's addresses; the backend is the platform one, noDetachingBackend, becausewait(0)never blocks attached. - The ordering rule that makes it safe: a stream's begin frame rides
the chunk channel and is drained by a PASS, while its head is a direct
completion.
_flushtherefore runs a pass first and completions second, or a head could precede its own begin frame — the recycled-slot hazard the 0.14.1 rules exist for. Deterministic on one thread. - Driver:
add_reader(kq_fd, _on_mojo)→port.pass_(), plus a 1 Hzcall_laterfor the idle sweep, the date cache and the heartbeats, which assume a wake per second. Acks and credit unchanged. - Shutdown — designed, not built, and deliberately skipped
(2026-08-29):
_run_shutdown's drain waits inbackend.wait, which inside an asyncio callback blocks the very tasks it waits for. The design was to poll the drain fromcall_laterpasses untilactive_count == 0or the 5 s budget, thenloop.stop(). The first cut runs the drain as it is, blocking, and the consequence is measured: with a 1.5 s request mid-await at SIGTERM the pump answers it at 1.50 s, the inversion at 5.30 s — the drain deadline, after which the shim runs the in-flight tasks to completion and the response goes out. Not dropped, but any stop grace under ~6 s drops it. The reshaping is half a day (split_run_shutdowninto prepare / step / finish asrun_event_loopwas split, drive the step from acall_latercadence) and buys that only under the flag, which nothing runs in production; it is the inversion's promotion bar, not 0.15.0 work. Until then an inverted server wants a stop grace of 10 s or more (docker stop's default), and the comment beside the flag inm0serve.mojosays so.