Commits
interruptTask now walks an intrusive all-live-fibers list, reaching
fibers parked on io readiness, futex, timers, or await alike — the
'cancel lands on next natural wakeup' gap is closed. new test: cancel
a fiber parked in netRead on a silent socket; must return promptly
with error.Canceled. 16/16 on Debug+ReleaseSafe x macos+linux.
docs: adoption-guide.md — general guide for swapping Io.Threaded for
zio.ZioBackend in any project (zlay as worked example): when to adopt,
the swap, what-runs-where table, the four domain rules, gotchas
(readSliceShort, close-as-wakeup, shutdown order), rollout discipline,
honest maturity status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zio.ZioBackend wraps Io.Threaded, copies its vtable, and overrides:
- concurrent/await/cancel: tasks run as fibers on the sched loop; await
parks fibers or futex-blocks foreign threads (refcounted task, result
copied under acquire/release ordering); cancel wakes parked fibers via
cross-thread interrupt injection
- netConnectIp/netAccept/netRead/netWrite: non-blocking + park on
readiness; accept flips Threaded-created listeners non-blocking
- sleep: sched timer heap for fiber callers
- futexWait/futexWake: fiber-aware futex table + always-both-domains
wake — Io.Mutex/Condition/Queue now work for fiber callers unchanged
(proven by a fiber-vs-pool-thread mutex contention test)
- netLookup: spilled to an inner-Threaded thread; the calling fiber
parks in Queue.get via the fiber-aware futex, so DNS never stalls
the loop
sched grows the cross-thread machinery: self-pipe wake, injection
queue (spawn/futex-wake/interrupt), runForever, parkCurrent/sleepUntil,
gen-tagged futex waiter list.
lesson banked: a 'hang' during bring-up was the test using
readSliceShort as a message-read — it parks until the buffer fills
(short only at stream end). diagnosed end-to-end with lskq (live
kqueue knote dump): knotes armed, no data pending -> writer-side
protocol bug, not a runtime bug.
15/15 tests: Debug + ReleaseSafe, macos + linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zio.Sched: blocking-style io (read/writeAll/connectIp4/accept/sleepMs) for
fibers multiplexed on one os thread over the poller. one-shot event arming,
generation-tagged lazy timer heap, every park spurious-wakeup safe (io ops
retry; sleep re-parks until deadline; connect re-checks via getpeername),
finished fibers recycled through a free list so stale poller tokens can
never dereference freed memory.
two lessons banked along the way:
- fresh fiber frames must terminate the unwinder chain: fp=0 AND lr=0
(aarch64) / null return-address slot (x86_64). debug allocators capture
stack traces on alloc-from-fiber; a stale lr sent the unwinder into
garbage (segv at 0x8) only in tests that allocate on fiber stacks.
- never rely on closing an fd to wake a parked fiber — kqueue drops
filters on close and the wakeup is silently lost. shutdown paths use
timed waits (acceptTimeout / waitReadableTimeout) instead.
- x18 clobber is now os-conditional in the vendored switch: required
where allocatable (linux), warning-and-meaningless where reserved
(darwin/windows).
demo (bench/fiber_demo.zig, zig build demo): N tcp echo sessions, all
blocking-style, single thread. linux ReleaseSafe: 4,002 fibers,
~200k round-trips/s, os-threads=1.
10/10 tests, Debug + ReleaseSafe, macos + linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vendor std's fiber.zig and prove the primitive sound under ReleaseSafe on
macos-aarch64, linux-aarch64, linux-x86_64 with two one-word clobber fixes:
- .x30 -> .lr: LLVM < 23 silently drops x29/x30 inline-asm clobbers
written by xN name (llvm/llvm-project #167783); zig emits ~{x30}
verbatim, so LLVM parks live values in the link register across the
switch. proven by disassembly: IR contains ~{x30}, machine code keeps
the loop counter in x30 across the asm and reads back a return address.
- add .x18: reserved on darwin, general-purpose on linux — upstream's
omission lets linux codegen cache values in x18 across switches.
plus one harness fix (x86_64 jmp-entry needs rsp biased 8 mod 16).
gauntlet: 100k switches, 256 interleaved fibers, cross-thread migration —
7/7 everywhere, Debug and ReleaseSafe. this fully explains the april
production GPFs, the macos Dispatch crashes on 0.16/master, and upstream
#35712. full evidence chain in docs/fiber-gauntlet-2026-07-09.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benchmark methodology moves to docs/benchmarks.md (workload shape, what
runs, measurement caveats); README describes zio on its own terms and
links out. the fan-in ingest parameters keep their provenance in the
benchmarks doc rather than defining the project.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
poller mod() (epoll CTL_MOD / kqueue always-registered filters with
ENABLE|DISABLE), engine update(), non-blocking connect helpers. bench
readers now reassemble frames via per-conn ctx; --storm tears down and
re-establishes all conns simultaneously: 2,800 in 114.9 ms (linux aarch64
ReleaseSafe), 234.8 ms under rosetta x86_64 (correctness only).
also: engine test hung using drainWrite as send-once — pump semantics
never EAGAIN against a draining reader. writeOnce added, semantics
documented in the name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epoll/kqueue poller + per-fd handler engine (no fibers, ReleaseSafe-clean),
Zio hybrid skeleton (copied-vtable delegation over Io.Threaded), bench
--backend zio. linux ReleaseSafe, 2,800 conns: 6.9 MiB RSS vs 1,390 MiB,
19 threads vs 5,317, 188.8M frames/s vs 16.1M (batching caveat recorded
in docs/baselines.md).
ReleaseSafe caught a mixed atomic/non-atomic race in the first engine test
that Debug happily passed — the ReleaseSafe-first thesis earning its keep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scoreboard-first per the runtime plan (relay/docs/evented-runtime-plan.md):
zlay-shaped loopback ingest bench over std.Io, threaded reference numbers
recorded for linux (2,800 conns: 14.3M frames/s, 1.4 GiB RSS, 5,381 threads).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
interruptTask now walks an intrusive all-live-fibers list, reaching
fibers parked on io readiness, futex, timers, or await alike — the
'cancel lands on next natural wakeup' gap is closed. new test: cancel
a fiber parked in netRead on a silent socket; must return promptly
with error.Canceled. 16/16 on Debug+ReleaseSafe x macos+linux.
docs: adoption-guide.md — general guide for swapping Io.Threaded for
zio.ZioBackend in any project (zlay as worked example): when to adopt,
the swap, what-runs-where table, the four domain rules, gotchas
(readSliceShort, close-as-wakeup, shutdown order), rollout discipline,
honest maturity status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zio.ZioBackend wraps Io.Threaded, copies its vtable, and overrides:
- concurrent/await/cancel: tasks run as fibers on the sched loop; await
parks fibers or futex-blocks foreign threads (refcounted task, result
copied under acquire/release ordering); cancel wakes parked fibers via
cross-thread interrupt injection
- netConnectIp/netAccept/netRead/netWrite: non-blocking + park on
readiness; accept flips Threaded-created listeners non-blocking
- sleep: sched timer heap for fiber callers
- futexWait/futexWake: fiber-aware futex table + always-both-domains
wake — Io.Mutex/Condition/Queue now work for fiber callers unchanged
(proven by a fiber-vs-pool-thread mutex contention test)
- netLookup: spilled to an inner-Threaded thread; the calling fiber
parks in Queue.get via the fiber-aware futex, so DNS never stalls
the loop
sched grows the cross-thread machinery: self-pipe wake, injection
queue (spawn/futex-wake/interrupt), runForever, parkCurrent/sleepUntil,
gen-tagged futex waiter list.
lesson banked: a 'hang' during bring-up was the test using
readSliceShort as a message-read — it parks until the buffer fills
(short only at stream end). diagnosed end-to-end with lskq (live
kqueue knote dump): knotes armed, no data pending -> writer-side
protocol bug, not a runtime bug.
15/15 tests: Debug + ReleaseSafe, macos + linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zio.Sched: blocking-style io (read/writeAll/connectIp4/accept/sleepMs) for
fibers multiplexed on one os thread over the poller. one-shot event arming,
generation-tagged lazy timer heap, every park spurious-wakeup safe (io ops
retry; sleep re-parks until deadline; connect re-checks via getpeername),
finished fibers recycled through a free list so stale poller tokens can
never dereference freed memory.
two lessons banked along the way:
- fresh fiber frames must terminate the unwinder chain: fp=0 AND lr=0
(aarch64) / null return-address slot (x86_64). debug allocators capture
stack traces on alloc-from-fiber; a stale lr sent the unwinder into
garbage (segv at 0x8) only in tests that allocate on fiber stacks.
- never rely on closing an fd to wake a parked fiber — kqueue drops
filters on close and the wakeup is silently lost. shutdown paths use
timed waits (acceptTimeout / waitReadableTimeout) instead.
- x18 clobber is now os-conditional in the vendored switch: required
where allocatable (linux), warning-and-meaningless where reserved
(darwin/windows).
demo (bench/fiber_demo.zig, zig build demo): N tcp echo sessions, all
blocking-style, single thread. linux ReleaseSafe: 4,002 fibers,
~200k round-trips/s, os-threads=1.
10/10 tests, Debug + ReleaseSafe, macos + linux.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vendor std's fiber.zig and prove the primitive sound under ReleaseSafe on
macos-aarch64, linux-aarch64, linux-x86_64 with two one-word clobber fixes:
- .x30 -> .lr: LLVM < 23 silently drops x29/x30 inline-asm clobbers
written by xN name (llvm/llvm-project #167783); zig emits ~{x30}
verbatim, so LLVM parks live values in the link register across the
switch. proven by disassembly: IR contains ~{x30}, machine code keeps
the loop counter in x30 across the asm and reads back a return address.
- add .x18: reserved on darwin, general-purpose on linux — upstream's
omission lets linux codegen cache values in x18 across switches.
plus one harness fix (x86_64 jmp-entry needs rsp biased 8 mod 16).
gauntlet: 100k switches, 256 interleaved fibers, cross-thread migration —
7/7 everywhere, Debug and ReleaseSafe. this fully explains the april
production GPFs, the macos Dispatch crashes on 0.16/master, and upstream
#35712. full evidence chain in docs/fiber-gauntlet-2026-07-09.md.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
benchmark methodology moves to docs/benchmarks.md (workload shape, what
runs, measurement caveats); README describes zio on its own terms and
links out. the fan-in ingest parameters keep their provenance in the
benchmarks doc rather than defining the project.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
poller mod() (epoll CTL_MOD / kqueue always-registered filters with
ENABLE|DISABLE), engine update(), non-blocking connect helpers. bench
readers now reassemble frames via per-conn ctx; --storm tears down and
re-establishes all conns simultaneously: 2,800 in 114.9 ms (linux aarch64
ReleaseSafe), 234.8 ms under rosetta x86_64 (correctness only).
also: engine test hung using drainWrite as send-once — pump semantics
never EAGAIN against a draining reader. writeOnce added, semantics
documented in the name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
epoll/kqueue poller + per-fd handler engine (no fibers, ReleaseSafe-clean),
Zio hybrid skeleton (copied-vtable delegation over Io.Threaded), bench
--backend zio. linux ReleaseSafe, 2,800 conns: 6.9 MiB RSS vs 1,390 MiB,
19 threads vs 5,317, 188.8M frames/s vs 16.1M (batching caveat recorded
in docs/baselines.md).
ReleaseSafe caught a mixed atomic/non-atomic race in the first engine test
that Debug happily passed — the ReleaseSafe-first thesis earning its keep.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>