GC pacing and the "unit of work" question for CLIs — some data #34216
VenkatKwest
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
A few days ago Jarred posted that one of Bun's most impactful memory optimizations is scheduling GC when the HTTP server finishes sending a response body, and asked what the equivalent is for CLIs (https://x.com/jarredsumner/status/2076636361814364659). I went digging through the runtime and ran some benchmarks on canary to see how much of the answer already exists. Sharing both in case it's useful.
A framing: the exit ledger is already a unit-of-work ledger
A process stays alive exactly as long as something is pending: an in-flight fetch, a running subprocess, a timer, unread stdin. The event loop already tracks this set — it's how the runtime decides when it may exit. Every decrement of that set is one unit of work finishing. The server hook is the special case where the pending thing is a request (mod.rs — the GC check sits right next to
pending_requests -= 1), and the fetch-completion hint from #34009 (FetchTasklet.rs) is the same case pointed the other direction.Two things from the thread fall out of this framing instead of needing separate rules:
The objection about the http-client version — "JS may still be executing" — is already answered by the shape of the mechanism in the tree: the completion sets a flag, and the heap is measured at the next event-loop park (GarbageCollectionController.rs), by which point the handler and its microtasks have run and the garbage exists to be seen.
What I measured
Two CLI-shaped loops on
1.4.0-canary.1+88403d981, Windows 11 x64:Bun.spawnsubprocesses each writing 2 MB to stdout, consumed and split the same wayEach ran with the GC controller in its default state and with
BUN_GC_TIMER_DISABLE=1, 3 runs per configuration after a discarded warmup. Peak RSS and peakheapStats().heapSizesampled every 5 ms plus at each iteration.Three observations:
IdleNotificationGC in 0.10 (2013), the release notes explained that "there are different degrees of 'idleness', and if you get it wrong, you can easily end up spending massive amounts of time collecting garbage when you'd least expect" — and what shipped was "latency is much more predictable and stable." That was a guessed signal being removed. A pending-set decrement is a real one, and it appears to buy the same predictability from the other direction.Also worth noting: the spawn loop — which has no completion hint — was paced just as tightly as the fetch loop that has one, so the park-time checks appear to carry CLI workloads fine today.
Caveats
One machine, Windows, three runs per configuration, a microbenchmark with uniform garbage. The determinism observation was consistent across every run I did, but it deserves replication on other platforms and at larger scales. Scripts below.
benchmark scripts (4 files)
server.ts:fetch-bench.ts:spawn-bench.ts:run-bench.ts:Beta Was this translation helpful? Give feedback.
All reactions