Short answer
No, the purescript-aff scheduler does not maintain a priority queue for fiber resumption. Fibers are resumed in roughly the order their continuations become runnable, and that order is ultimately decided by the JavaScript engine's microtask/macrotask queues, not by Aff. And Aff does not actively prevent event-loop starvation: a fiber that runs a long synchronous stretch never yields, so it monopolizes the single thread exactly like any other long-running JavaScript function. Starvation avoidance is your responsibility, via explicit yields.
What the scheduler actually does
Aff compiles each fiber into a chain of small continuations. When a fiber is forked or an async result arrives, the continuation is handed to the JavaScript runtime (via resolved promises / microtasks, and timer callbacks for delay). The scheduler then runs a fiber's queued steps until it hits an asynchronous boundary, a fork/join interaction, or an explicit yield, at which point control returns to the event loop and the next pending task runs.
Two consequences follow directly:
- Ordering is FIFO-ish, not prioritized. When several fibers resolve "simultaneously" (e.g., several timers fire in the same tick), their wake-up order follows the order their callbacks were enqueued. There is no documented guarantee you should build on beyond that, and it can differ subtly between Node and browsers because microtask flushing differs.
- Single-threaded means cooperative.
Ref updates are safe from data races precisely because only one fiber executes at a time — but only between yield points. Treat every Aff bind that can suspend as a potential interleaving point; code between two yields is effectively atomic.
Why latency spikes appear with many fibers
The common cause is not fiber count but uninterrupted work: a tight loop (parsing, folding a large array, encoding JSON) inside one fiber runs to completion before any other fiber or browser task gets a turn. Thousands of idle fibers are cheap; one compute-bound fiber is what stalls the loop.
Practical mitigation
- Break long computations into chunks and yield between them, e.g. process N items then
delay (Milliseconds 0.0) (or an equivalent explicit yield) so pending fibers and I/O callbacks run.
- Move genuinely CPU-heavy work off the JS thread (Node worker threads, browser Web Workers) rather than relying on Aff scheduling.
- Cap concurrency with a semaphore pattern (
AVars or a pooled fork) instead of forking unboundedly, so wake-up bursts stay small.
chunked :: forall a. Array a -> (a -> Aff Unit) -> Aff Unit
chunked xs f = go 0
where
go i = case xs !! i of
Nothing -> pure unit
Just x -> do
f x
when (i `mod` 50 == 0) (delay (Milliseconds 0.0))
go (i + 1)
Verify before trusting this
The internals above describe long-standing behavior, but the scheduler is not a stable public API and details may shift between purescript-aff releases — check the source of the version you depend on. To confirm the latency story in your environment, measure the event loop directly: in Node, sample performance.now() on a repeating setImmediate and compare max gap with and without yields in your hot loop; in a browser, watch the same via a requestAnimationFrame delta or the Performance panel. If your spikes persist after chunking, the missing diagnostic is whether the blocking work is actually in Aff at all — a native binding (JSON.stringify, a FFI call) won't yield no matter how you structure the Aff code around it.