Event-driven macOS run loop: sleep-until-input instead of a 60 Hz poll - #17
Merged
Conversation
The macOS drain now sleeps in CFRunLoopRunInMode with a timeout instead of spin-polling at 60Hz: a native event wakes it instantly (returnAfterSourceHandled) and idle ticks just time out. A new AdaptiveBlockingPump drives it, resetting the timeout to a small minimum while input flows and backing off exponentially to a maximum when idle. Idle CPU on a window drops ~10x (~2.5% to ~0.2%) with no added input latency; main-process JS timers run at up to the max (~125ms) granularity while idle, which renderer rAF and IPC avoid by riding the native event path. A true libuv/CFRunLoop embedding (Electron-style) is not reachable from bun:ffi: Bun's loop is uSockets not libuv and its tick/wakeup symbols are not exported (oven-sh/bun#18546). Linux and Windows keep the existing cooperative pump.
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.
What
The macOS cooperative pump polled AppKit at a fixed 60 Hz (
CFRunLoopRunInMode(…, timeout=0)on a 16 mssetInterval). This replaces that with an event-driven, adaptive loop:CFRunLoopRunInMode(default, timeout, returnAfterSourceHandled)— a native UI event returns it instantly, idle ticks just time out.AdaptiveBlockingPumpdrives it: timeout snaps to a small min (8 ms) while events flow (responsive) and backs off exponentially to a max (125 ms) when idle (deep sleep). It yields to Bun's loop between drains so JS timers/microtasks/IO still run.Measured on macOS (a real packaged calculator)
app.quit()clean exitThe one honest trade-off: while the UI is idle, main-process JS timers run at up to ~125 ms granularity. Renderer
requestAnimationFrame(nativeCADisplayLink) and IPC are unaffected — they ride the native event path and keep the pump at min while active.Why not a true libuv/CFRunLoop embedding (Electron-style)?
Investigated thoroughly against the Bun + Electron sources. It is not reachable from pure
bun:ffitoday: Bun's main loop is uSockets (us_loop_t), not libuv; the exporteduv_*are NAPI stubs (uv_default_loop()panics); and the real tick/wakeup symbols (us_loop_run_bun_tick,us_wakeup_loop) are not exported (oven-sh/bun#18546). Blocking a native loop on the JS thread is also unsafe (JSC heap-access toggling). So this adaptive pump is the best event-driven behavior achievable while keeping bunmaska's single-pumped-thread identity. If Bun later exports those primitives, full integration becomes straightforward.Scope
macOS only by design — Linux and Windows keep the existing
CooperativePumpuntouched. New:AdaptiveBlockingPump+ 7 unit tests; the macOS drain returns a handled-source signal. Validate green (1630 tests, 0 fail).🤖 Generated with Claude Code