96 lines
6 KiB
Markdown
96 lines
6 KiB
Markdown
# Fibers — green threads on the wovm shard scheduler
|
||
|
||
> Research note (2026-08-08) feeding story iteration 11. Expands
|
||
> [the blue-green vision §3](../blue-green-vm/00-vision.md) with the
|
||
> kernel's-eye evidence and the precedent survey. Prose only; the spec and
|
||
> plan follow the brainstorming → writing-plans path when the iteration
|
||
> starts.
|
||
|
||
## Why the kernel cannot do this for us
|
||
|
||
Reference: [Threads and the OS kernel's view](https://learn.padho.ai/wiki/threads-and-the-os-kernels-view).
|
||
The facts that matter, condensed:
|
||
|
||
- A Linux thread IS a `task_struct`: own TID, own kernel stack (8–16 KiB),
|
||
own scheduling slot; `clone()` flags decide what is shared. There is no
|
||
cheaper kernel thread to ask for.
|
||
- Costs per thread: ~8 MiB stack VMA, ~9 KiB `task_struct`,
|
||
~20 µs creation, 1–3 µs per context switch. Measured against a userspace
|
||
runtime spawn (~58 ns): **~342× creation cost, ~70× memory**.
|
||
- CFS keys a red-black tree by `vruntime` — O(log N) per pick. At tens of
|
||
thousands of runnable tasks the scheduling slice approaches the
|
||
context-switch cost and the kernel scheduler becomes the bottleneck
|
||
before application code does.
|
||
- M:N threading died *in the kernel* (NPTL, 2003) and was reborn in
|
||
userspace (goroutines, BEAM processes, Tokio tasks, Loom virtual
|
||
threads) — because only the runtime knows its own blocking points and
|
||
can keep per-task state tiny.
|
||
|
||
Conclusion the industry already reached and we adopt: **one kernel task
|
||
per core** (plan 09 / iteration 8's pinned shard — already doctrine),
|
||
**userspace tasks above it**. The kernel schedules cores; the runtime
|
||
schedules work.
|
||
|
||
## Precedent survey — who has green threads, and how
|
||
|
||
| Runtime | Task state | Preemption | I/O integration | Lesson for wovm |
|
||
| --- | --- | --- | --- | --- |
|
||
| **Erlang/BEAM** | interpreter state per process, private heap | **reduction budget** (~2k reductions, checked at calls) | park on scheduler, poll set | the closest shape: interpreter = scheduler; deterministic, signal-free preemption |
|
||
| **Go** | native stack, 8 KiB grown by copy | async preemption via signals (since 1.14) + safepoints | netpoller parks goroutines | stack copying needs precise pointer maps — heavy machinery; signals are what the vision explicitly avoids |
|
||
| **Java Loom** | continuation frames on heap, unmounted from carrier | cooperative at yield points | blocking calls in the JDK park the virtual thread | "same blocking API, runtime parks underneath" — exactly our stdlib posture |
|
||
| **Tokio/Rust** | stackless state machines (`async fn`) | cooperative at `.await` | reactor + waker | rejected surface: writeonce has **no async/await keyword** (systems-track spec Part 2); function coloring is the disease |
|
||
| **Lua** | coroutine = own Lua stack (interpreter state) | none (pure cooperative) | up to the host | proof that interpreter-state fibers are nearly free; but no preemption = one hot loop starves the shard |
|
||
| **boost.context / libco** | native stack + hand-rolled register switch | none | none | what we do NOT need: wovm executes bytecode, so no native stack switching, no asm, no guard pages |
|
||
|
||
## The wovm design (vision §3, confirmed by this survey)
|
||
|
||
A fiber is **the execution state the VM already isolates**: register-window
|
||
stack + frame stack + pc. Making that per-fiber instead of per-VM turns the
|
||
interpreter into a scheduler almost for free — the BEAM/Lua insight, minus
|
||
Lua's starvation problem:
|
||
|
||
- **Preemption by reduction budget** — the dispatch loop decrements a
|
||
counter per instruction (or per call/back-edge); at zero the fiber parks
|
||
and the next runnable one is picked. No signals, no safepoint asm, no
|
||
stack copying; deterministic and debuggable. (Erlang has run this design
|
||
in telecom production for three decades.)
|
||
- **Park on I/O** — a blocking stdlib builtin on a server shard hands its
|
||
fd to the shard's epoll/io_uring loop and parks the fiber; the completion
|
||
resumes it. Same typed builtins, blocking look, no coloring — the
|
||
systems-track "one API, two execution disciplines" doctrine gains its
|
||
third discipline: program mode blocks the thread, server shards park the
|
||
fiber, the source text is identical.
|
||
- **Ownership fits** — a fiber owns its objects like a shard owns its heap;
|
||
cross-fiber sends move ownership exactly like iteration 8's cross-shard
|
||
sends (same rule, cheaper path: same heap, no copy). `@gc` references
|
||
stay shard-local either way, so the per-shard cycle collector (staged to
|
||
iteration 8) needs fiber stacks as additional roots — the one real
|
||
collector interaction to spec.
|
||
- **Cost target** — fiber creation is an arena allocation of a small
|
||
context (hundreds of bytes, the ~58 ns class), park/resume is a pointer
|
||
swap in the dispatch loop; thousands of fibers per shard where the
|
||
kernel tops out at hundreds of threads per core.
|
||
|
||
## What must be specified before implementation (open questions)
|
||
|
||
1. Surface: `spawn` returns what — a fiber handle, an actor address, or
|
||
nothing (fire-and-forget)? Iteration 8's `spawn` and mailbox surface
|
||
should be the same word; fibers refine its granularity.
|
||
2. Reduction budget size and where it is checked (per instruction vs per
|
||
call/back-edge) — measure both in the interpreter before choosing.
|
||
3. Parked-fiber lifetime: what drops a fiber blocked forever (shard
|
||
shutdown, blue-green drain)? Unwinding a parked fiber must run its drop
|
||
maps — same machinery as trap unwind (OOP spec §6).
|
||
4. Fairness: run queue is FIFO per shard in v1; priorities/timers are a
|
||
later capability (the recipe-box rule — no framework policy in the
|
||
runtime).
|
||
5. Program mode: stays fiber-free in v1 (one thread, blocking legal) or
|
||
gains the same scheduler? Default: fiber-free — log-watcher needs none.
|
||
|
||
## Doctrine check
|
||
|
||
Kernel primitives only (epoll/io_uring already ours; no new syscalls
|
||
needed) · no signals · no async/await keyword · ownership moves, never
|
||
shares · per-shard everything (heap, GC, now run queue) · plain
|
||
diagnostics (a starved-fiber warning names the hot function via the line
|
||
table). No principle bends.
|