From c2ae71a6faf307e5661a4a6bcd73ac17f6ef657a Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Tue, 20 Oct 2026 02:07:00 -0500 Subject: [PATCH] Bound full-batch poller drains to 48 per poll phase A full batch of 64 events polled again without returning to the event loop. When synchronous socket callbacks kept 64 events ready on every drain, timers and setImmediate callbacks never ran. The poll phase now drains at most 48 full batches, the count libuv's uv__io_poll uses, and the budget carries across the reopen that lets microtasks run, so the remaining events wait for the next loop iteration. --- packages/runtime/src/scr_async.c | 13 +++++++++++-- packages/runtime/src/scr_net.c | 8 +++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/runtime/src/scr_async.c b/packages/runtime/src/scr_async.c index de48d6b61..2ab35e85d 100644 --- a/packages/runtime/src/scr_async.c +++ b/packages/runtime/src/scr_async.c @@ -4153,13 +4153,19 @@ bool scr_loop_has_ready(void) { return scr_ready_len > 0 || scr_nt_head != NULL; * drains the poller at most once between check phases. I/O that becomes * ready while this iteration's callbacks run (a loopback connect started * by one of them) waits for the next iteration, so immediates queued in - * the meantime fire first — Node's poll → check order. */ + * the meantime fire first — Node's poll → check order. A full batch polls + * again without waiting, at most SCR_LOOP_IO_BATCHES times per phase (the + * count libuv's uv__io_poll uses); later events wait for the next + * iteration, so timers and immediates still run while sockets stay busy. */ +enum { SCR_LOOP_IO_BATCHES = 48 }; static SCR_TL bool scr_loop_io_open = true; +static SCR_TL int scr_loop_io_batches = 0; bool scr_loop_io_take(void) { bool open = scr_loop_io_open; scr_loop_io_open = false; return open; } +bool scr_loop_io_more(void) { return ++scr_loop_io_batches < SCR_LOOP_IO_BATCHES; } void scr_loop_io_reopen(void) { scr_loop_io_open = true; } /* The process.nextTick queue to exhaustion, ticks queued meanwhile @@ -4668,7 +4674,10 @@ static bool scr_loop_run_pass(ScrPromise *top_level, bool first_checkpoint) { * had nothing to run). A check phase deferred for pending microtasks * keeps it closed, so the next iteration reaches the check phase * before polling again. */ - if (check_ran || scr_pending_immediates == 0) scr_loop_io_open = true; + if (check_ran || scr_pending_immediates == 0) { + scr_loop_io_open = true; + scr_loop_io_batches = 0; + } } return rejection_failed; } diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 9684aebe9..f12840d91 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -4909,6 +4909,7 @@ static int scr_net_pollfd(void) { } bool scr_loop_io_take(void); +bool scr_loop_io_more(void); void scr_loop_io_reopen(void); static void scr_net_dispatch(void) { @@ -4917,8 +4918,9 @@ static void scr_net_dispatch(void) { return; } /* One poller drain per loop iteration (scr_loop_io_take); a full batch - * keeps draining, like libuv's poll phase. Deferred emits (the sweep) - * still run on every call. */ + * keeps draining within the phase's batch budget (scr_loop_io_more), like + * libuv's poll phase. Deferred emits (the sweep) still run on every + * call. */ bool may_drain = scr_loop_io_take(); for (;;) { scr_net_sweep(); @@ -4928,7 +4930,7 @@ static void scr_net_dispatch(void) { int n = 0; if (may_drain) { n = scrp_drain(scr_net_poller, evs, 64); - may_drain = n == 64; + may_drain = n == 64 && scr_loop_io_more(); } for (int i = 0; i < n; i++) { void *udata = evs[i].udata;