win: high-resolution poll timeouts via waitable timer + IOCP - #9
Merged
Merged
Conversation
robobun
force-pushed
the
farm/3199e702/win-hrtimer
branch
2 times, most recently
from
July 20, 2026 14:39
e2596e5 to
b5c4eca
Compare
Author
|
Ran an adversarial review against this diff (8 probe dimensions, 2-vote refutation). Four concerns survived; all addressed in b5c4eca:
Also manually verified on Windows Server 2019: I/O-beats-timer stress (100/100 fetch wins, 0 early fires), timer-beats-I/O (30/30 timer wins at 1ms vs 10ms server), 8 parallel Worker loops (medians 1.19-1.56ms), |
robobun
force-pushed
the
farm/3199e702/win-hrtimer
branch
6 times, most recently
from
July 20, 2026 23:13
4bb29ad to
1824b45
Compare
GetQueuedCompletionStatusEx's ms timeout rounds to the system clock tick (~15.6 ms by default), so a 1 ms uv_timer fires ~15 ms late on an idle Windows box unless another process has raised the tick rate. On Win10 1803+ / Server 2019+, arm a CREATE_WAITABLE_TIMER_HIGH_RESOLUTION waitable timer for the poll deadline and associate it with the loop's IOCP via NtAssociateWaitCompletionPacket. When it fires the kernel posts a completion with lpOverlapped == NULL, which the dequeue loop already treats as a pure wakeup, and GQCS blocks with INFINITE so its coarse ms timeout never applies. This is the same approach Go's runtime uses (golang/go#44343). On older Windows the HIGH_RESOLUTION flag or the Nt* function pointers are absent; hrtimer stays NULL and uv__poll keeps the plain GQCS ms wait, so behavior there is unchanged. Measured on Windows Server 2019 (idle): setTimeout(cb, 1): 15.52 ms -> 1.41 ms setTimeout(cb, 5): 15.62 ms -> 5.24 ms setInterval(16): 28 ms -> 16.4 ms
robobun
force-pushed
the
farm/3199e702/win-hrtimer
branch
from
July 20, 2026 23:18
1824b45 to
bfcfd97
Compare
robobun
added a commit
to oven-sh/bun
that referenced
this pull request
Jul 20, 2026
oven-sh/libuv#9 landed the high-res poll timeouts on the bun branch, so drop the local patch file and bump LIBUV_COMMIT. This also pulls in the intervening Windows fs correctness fixes on that branch (oven-sh/libuv#7 and #8).
dylan-conway
pushed a commit
to oven-sh/bun
that referenced
this pull request
Jul 20, 2026
## What On an idle Windows box the `GetQueuedCompletionStatusEx` timeout rounds to the ~15.6 ms system clock tick, so `setTimeout(cb, 1)` and `Bun.sleep(1)` fire ~15 ms late unless another process happens to have raised the tick rate (the "works when Spotify is open" heisenbug). Node.js has the same behavior. ## How Bumps libuv to [oven-sh/libuv@9687330](oven-sh/libuv@9687330), which landed [oven-sh/libuv#9](oven-sh/libuv#9) (same approach Go's runtime uses, [golang/go#44343](golang/go#44343)): * At `uv__winapi_init`, dynamically load `NtCreateWaitCompletionPacket` / `NtAssociateWaitCompletionPacket` / `NtCancelWaitCompletionPacket` from ntdll. * Per loop, create a `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION` waitable timer + wait-completion-packet and stash them on `uv__loop_internal_fields_s` (Win10 1803+ / Server 2019+; on older Windows the handles stay NULL and `uv__poll` keeps its original GQCS ms wait, so behavior is unchanged). * In `uv__poll`, when `timeout > 0`: arm the waitable timer for the deadline, associate it with the loop's IOCP, and wait in GQCS with `INFINITE`. When the timer fires, the kernel posts a completion with `lpOverlapped == NULL`, which the existing dequeue loop already treats as a pure wakeup. The bump also pulls in the intervening Windows fs correctness fixes on the `bun` branch ([oven-sh/libuv#7](oven-sh/libuv#7), [#8](oven-sh/libuv#8)). ## Numbers (Windows Server 2019, idle) | | before | after | |--------------------|-----------|----------| | `setTimeout(cb,1)` | 15.52 ms | 1.41 ms | | `Bun.sleep(1)` | 15.62 ms | 1.08 ms | | `setTimeout(cb,5)` | 15.62 ms | 5.24 ms | | `setInterval(16)` | ~28 ms | 16.4 ms | `Bun.serve` hello-world throughput on Windows debug (oha, 10s, 50 concurrent): 11,484 req/s on this branch vs 11,434 req/s on main (noise). The added test in `setTimeout.test.js` measures the median of 50 `setTimeout(1)` samples in a subprocess and asserts it's under 8 ms (before: 15.6 ms; after: ~1.5 ms). Fixes #16714 Fixes #26965 <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test-only change; deferring to CI. <!-- robobun:evidence:end -->
liooil
pushed a commit
to liooil/poly
that referenced
this pull request
Aug 7, 2026
## What On an idle Windows box the `GetQueuedCompletionStatusEx` timeout rounds to the ~15.6 ms system clock tick, so `setTimeout(cb, 1)` and `Bun.sleep(1)` fire ~15 ms late unless another process happens to have raised the tick rate (the "works when Spotify is open" heisenbug). Node.js has the same behavior. ## How Bumps libuv to [oven-sh/libuv@9687330](oven-sh/libuv@9687330), which landed [oven-sh/libuv#9](oven-sh/libuv#9) (same approach Go's runtime uses, [golang/go#44343](golang/go#44343)): * At `uv__winapi_init`, dynamically load `NtCreateWaitCompletionPacket` / `NtAssociateWaitCompletionPacket` / `NtCancelWaitCompletionPacket` from ntdll. * Per loop, create a `CREATE_WAITABLE_TIMER_HIGH_RESOLUTION` waitable timer + wait-completion-packet and stash them on `uv__loop_internal_fields_s` (Win10 1803+ / Server 2019+; on older Windows the handles stay NULL and `uv__poll` keeps its original GQCS ms wait, so behavior is unchanged). * In `uv__poll`, when `timeout > 0`: arm the waitable timer for the deadline, associate it with the loop's IOCP, and wait in GQCS with `INFINITE`. When the timer fires, the kernel posts a completion with `lpOverlapped == NULL`, which the existing dequeue loop already treats as a pure wakeup. The bump also pulls in the intervening Windows fs correctness fixes on the `bun` branch ([oven-sh/libuv#7](oven-sh/libuv#7), [#8](oven-sh/libuv#8)). ## Numbers (Windows Server 2019, idle) | | before | after | |--------------------|-----------|----------| | `setTimeout(cb,1)` | 15.52 ms | 1.41 ms | | `Bun.sleep(1)` | 15.62 ms | 1.08 ms | | `setTimeout(cb,5)` | 15.62 ms | 5.24 ms | | `setInterval(16)` | ~28 ms | 16.4 ms | `Bun.serve` hello-world throughput on Windows debug (oha, 10s, 50 concurrent): 11,484 req/s on this branch vs 11,434 req/s on main (noise). The added test in `setTimeout.test.js` measures the median of 50 `setTimeout(1)` samples in a subprocess and asserts it's under 8 ms (before: 15.6 ms; after: ~1.5 ms). Fixes #16714 Fixes #26965 <!-- robobun:evidence:begin --> --- **no test proof** · iteration 1 · Platform-specific test-only change; deferring to CI. <!-- robobun:evidence:end -->
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.
GetQueuedCompletionStatusEx's ms timeout rounds to the system clock tick (~15.6 ms by default), so a 1 msuv_timerfires ~15 ms late on an idle Windows box unless another process has raised the tick rate.On Win10 1803+ / Server 2019+, arm a
CREATE_WAITABLE_TIMER_HIGH_RESOLUTIONwaitable timer for the poll deadline and associate it with the loop's IOCP viaNtAssociateWaitCompletionPacket. When it fires, the kernel posts a completion withlpOverlapped == NULL, which the dequeue loop already treats as a pure wakeup, and GQCS itself blocks withINFINITEso its coarse ms timeout never applies. This is the same approach Go's runtime uses (golang/go#44343).On older Windows, fall back to
timeBeginPeriod(1)once per process.Measured on Windows Server 2019 (idle) via Bun's
setTimeout:setTimeout(cb,1)setTimeout(cb,5)Companion Bun PR with the test + build-side patch: oven-sh/bun#34834