scheduler: run duppy on a pool of domains, retire its monad, and stop rescanning the event loop - #9
Open
toots wants to merge 1 commit into
Open
scheduler: run duppy on a pool of domains, retire its monad, and stop rescanning the event loop#9toots wants to merge 1 commit into
toots wants to merge 1 commit into
Conversation
…domains, retire its monad, and stop rescanning the event loop
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Mirror of savonet/liquidsoap#5359
Duppy queues were systhreads, and on OCaml 5 those all share domain 0's runtime lock, so they gave concurrency and no parallelism — adding queues subtracted throughput.
Duppy now spawns one dispatch domain per core plus one for the event loop, batching non-blocking tasks onto a domain and handing blocking ones an auxiliary thread inside it, so parking in a syscall frees the domain.
With domains in place the CPS monad OCaml 4 required is no longer needed: computations park on an effect and resume where they left off, so harbor, the telnet server and harbor output read and write their sockets as ordinary sequential code.
The event loop no longer rescans what it is waiting on. Descriptors are watched through a set the kernel keeps —
epollon Linux,kqueueon BSD,selectwhere neither exists — and tasks are found by descriptor and by deadline instead of by folding over all of them.Breaking:
settings.scheduler.generic_queues,fast_queuesandnon_blocking_queuesare removed — they counted threads that no longer exist — andsettings.scheduler.blocking_tasksreplaces them to bound blocking tasks; scripts setting the old ones must drop those lines. Requires OCaml 5.5.Measurements
8 cores, OCaml 5.5.0.
Parallelism. Scheduled work runs on every core instead of taking turns on one:
Streaming latency. A 20ms periodic thread — a clock's shape — measured against CPU-bound scheduler work. The old medians are one 50ms systhread tick per contender:
Waiting for descriptors.
pollrescans everything submitted to it; a kernel-held set does not:pollPollset.waitCost of one loop turn, against the number of tasks waiting on quiet sockets — the shape of a harbor with that many connected-but-silent clients:
Flat rather than linear, which is the point. The after column is one figure because it no longer varies; it is noisy between 31 and 50 µs because cross-domain dispatch now dominates the loop entirely, and it is worse than the old empty case for the same reason — the two are not measuring the same path once the loop stops being the bottleneck.
Degraded cases
A flood of non-blocking tasks on one domain used to starve everything else. Taking every ready immediate task on every round left blocking work unreachable whenever the ready list refilled as fast as it drained: 200 self-rearming tasks on one domain ran 2.6 million times in two seconds while a waiting blocking task never ran once. At four domains it was invisible, because the worker taking the batch hands the leftover blocking work to another worker and with one worker there is no other worker. A worker that just took a batch now takes a blocking task next when it has one and is under its cap, which bounds the wait to one batch per worker.
Batch length is still unbounded by design, so a task classified
Immediatethat does not return immediately is a latency bug the scheduler will not rescue.Clocks are unaffected by scheduler load, including on one core. They stay on domain 0 rather than joining the pool, so they never queue for a dispatch slot and compete only for CPU — arbitrated by the OS across domains instead of by OCaml's 50ms tick within one. Pinned to a single core, the periodic thread holds p50 0.03–0.08ms with up to 7 busy tasks.
Deletions
duppy.ml1141 → 800 lines,duppy.mli512 → 255, andduppy_stubs.cgone entirely: its only function backedba_write, which the sole harbor transport implemented asfailwith "Not implemented!"and nothing called.Duppy.Monad.MutexandConditionwent withServer.condition, which was exported but had no callers anywhere.Not here
Clocks keep their own threads and their own high-precision wait — they park inside
Pcm.writeiand a JACK semaphore, where no continuation can be captured, and the event loop's timer is coarser than theclock_nanosleepthey use today.Tests
src/modules/duppy/test/test_duppy.mlcovers cross-domain dispatch, batching, socket events, the blocking cap, draining on stop, effect resumption landing on a different domain than it started on, and the starvation case above.src/modules/stdlib-utils/test/test_pollset.mlcovers registration, readiness, level-triggering, timeouts, peer close, and the property the kernel-held set exists for: with 500 watched and one ready, a wait returns exactly one.LIQ_POLLSET_BACKEND=selectforces the fallback, so the backend Windows runs is exercised on Linux too — both it andepollpass the same tests, and duppy's own suite passes on both.Each check was falsified by reverting the fix and watching it go red. Harbor 5/5, language and regression tests pass, and the suite runs clean pinned to one core and under full CPU load. The telnet server, which has no automated test, was driven by hand.
kqueuecompiles nowhere reachable from here and is CI-only.Also fixes the logging thread dropping everything logged during shutdown, type names coming out empty in errors raised from a callback, and
time.zone.setsilently doing nothing once anything had read the local time.