perf_hooks: implement eventLoopUtilization() - #34514
Closed
cirospaciari wants to merge 2 commits into
Closed
Conversation
eventLoopUtilization() returned hardcoded zeros and worker.performance's was a
notImplemented stub, so test-worker-eventlooputil did not fail — it HUNG
FOREVER, spinning on `if (elu().idle <= 0) return setTimeout(r, 5)`.
The loop already knew when it was about to park (`will_idle_inside_event_loop`),
so the accounting is two clock reads on ticks that were going to sleep anyway; a
busy tick pays nothing. libuv's own idle metrics cover the Windows path, enabled
with uv_loop_configure(UV_METRICS_IDLE_TIME) as node does unconditionally.
Two things this got wrong first, both worth recording:
`us_internal_loop_data_t` is us_loop_t's FIRST member and is MIRRORED in Rust
(src/uws_sys/InternalLoopData.rs). Adding a field without the mirror shifted
num_polls, so us_loop_run_bun_tick took its `num_polls == 0` early return and the
loop stopped parking — no compile error, and it read as an architectural wall
until a printf of num_polls showed 1 vs 0.
A counter only folded in when a park ENDS reads stale mid-park, which
over-reports active (49.8 vs the required 50). libuv has the same problem and
solves it the same way: publish the park's entry time and let the reader add the
in-progress interval (uv_metrics_idle_time, uv-common.c:1042).
The read order — idle, then now — and the unguarded divisions both match node:
eventLoopUtilization(u, u) yields NaN there, verified on v26.3.0, so collapsing
it to 0 would diverge. The shared math lives in internal/perf/event_loop_utilization
exactly as node shares it between perf_hooks and worker_threads.
Also fixes MessagePort listeners being called with `this === undefined` where
node passes the port; injectFakeEmitter's wrapper had the receiver and dropped
it. Worker is unaffected (real EventEmitter, already correct).
test-worker-eventlooputil: hung -> 10/10, byte-identical to node, clean under
BUN_JSC_validateExceptionChecks. perf_hooks 8 pass/0 fail; worker_threads
unchanged at its 2 known failures. Matches node on main-script ({0,0,0} before
the loop turns), 2-arg identical (NaN), and no-arg (0 < utilization < 1).
Known divergence: node reports {0,0,0} during synchronous main-script evaluation
because its loopStart milestone is still unset; Bun's loop_start is fixed at VM
init. Modelling that needs node's real milestone, not a proxy — iteration_nr
looks like one but is wrong, since a worker's script runs after its loop starts
and the main script runs before.
Collaborator
|
Updated 4:13 PM PT - Jul 17th, 2026
@autofix-ci[bot], your commit b24e939 is building: |
Contributor
|
Found 2 issues this PR may fix:
🤖 Generated with Claude Code |
Member
Author
|
Folding this into #34424 — three stacked PRs was more confusing than it was worth. Same commits, same tests, one review. |
Contributor
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
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.
Stacked on #34509 → #34424. Review those first; this PR is the last commit only.
performance.eventLoopUtilization()returned hardcoded{idle:0, active:0, utilization:0}andworker.performance's was anotImplementedstub. Sotest-worker-eventlooputildidn't fail — it hung forever, spinning onif (elu().idle <= 0) return setTimeout(r, 5).Design
The loop already knew when it was about to park (
will_idle_inside_event_loop), so the accounting is two clock reads on ticks that were going to make a sleeping syscall anyway — a busy tick pays nothing. Windows gets it from libuv's own metrics, enabled withuv_loop_configure(UV_METRICS_IDLE_TIME)(node does this unconditionally:node.cc:1587,node_worker.cc:175). Cross-thread reads are safe by construction: the counter is atomic,loop_startis fixed before the VM is published, andvm_lockonly closes the TOCTOU onvmitself.Two bugs worth recording
1.
us_internal_loop_data_tisus_loop_t's FIRST member and is MIRRORED in Rust (src/uws_sys/InternalLoopData.rs). Adding a field without updating the mirror shiftednum_polls, sous_loop_run_bun_ticktook itsnum_polls == 0early return and the loop stopped parking. No compile error. I twice concluded this was an architectural wall before a one-line printf ofnum_polls(1 without the field, 0 with it) named it. Anyone touching that struct should know.2. A counter only folded in when a park ends reads stale mid-park, over-reporting active — 49.8 against the required 50. libuv has the identical problem and solves it the identical way: publish the park's entry time and let the reader add the in-progress interval (
uv_metrics_idle_time,uv-common.c:1042-1056).node parity (verified against the v26.3.0 binary)
{0,0,0}{0,0,0}utilizationNaN0 < utilization < 1The unguarded divisions are deliberate — node returns NaN for a zero total, so collapsing it to 0 would diverge. Read order is idle-then-now, matching node. The shared math lives in
internal/perf/event_loop_utilization, exactly as node shares it between perf_hooks and worker_threads.Also fixed
MessagePort listeners were invoked with
this === undefinedwhere node passes the port —injectFakeEmitter's wrapper had the receiver and dropped it. Scoped to MessagePort (its only caller);Workeruses a real EventEmitter and was already correct.Verification
test-worker-eventlooputil: hung → 10/10, byte-identical to node, clean underBUN_JSC_validateExceptionChecks. perf_hooks 8 pass/0 fail. worker_threads unchanged at its 2 known failures. Windows target compiles.Known divergence: node reports
{0,0,0}during synchronous main-script evaluation because its loopStart milestone is still unset; Bun'sloop_startis fixed at VM init. Modelling that needs node's real milestone, not a proxy — I triediteration_nrand it's wrong, because a worker's script runs after its loop starts while the main script runs before, and it regressed the test 10/10 → 0/10.