Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 205 additions & 0 deletions patches/libuv/win-hrtimer.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
High-resolution event-loop timeouts on Windows.

GetQueuedCompletionStatusEx's ms timeout rounds to the system clock tick
(~15.6ms by default), so a 1ms uv_timer fires ~15ms late unless another
process has raised the tick rate. On Win10 1803+, arm a
CREATE_WAITABLE_TIMER_HIGH_RESOLUTION waitable timer for the deadline and
associate it with the loop's IOCP via NtAssociateWaitCompletionPacket; the
kernel posts a NULL-overlapped completion when it fires, which the dequeue
path already treats as a pure wakeup. GQCS itself then blocks with INFINITE.
On older Windows the probe fails and uv__poll keeps the plain GQCS ms wait.
--- a/src/uv-common.h
+++ b/src/uv-common.h
@@ -437,6 +437,10 @@ struct uv__loop_internal_fields_s {
struct uv__iou iou;
void* inv; /* used by uv__platform_invalidate_fd() */
#endif /* __linux__ */
+#ifdef _WIN32
+ void* hrtimer; /* CREATE_WAITABLE_TIMER_HIGH_RESOLUTION or NULL */
+ void* hrtimer_pkt; /* NtCreateWaitCompletionPacket handle or NULL */
+#endif /* _WIN32 */
};

#if defined(_WIN32)
--- a/src/win/winapi.h
+++ b/src/win/winapi.h
@@ -4657,9 +4657,36 @@ typedef NTSTATUS (NTAPI *sNtQueryInformationProcess)
ULONG Length,
PULONG ReturnLength);

+typedef NTSTATUS (NTAPI *sNtCreateWaitCompletionPacket)
+ (PHANDLE WaitCompletionPacketHandle,
+ ACCESS_MASK DesiredAccess,
+ PVOID ObjectAttributes);
+
+typedef NTSTATUS (NTAPI *sNtAssociateWaitCompletionPacket)
+ (HANDLE WaitCompletionPacketHandle,
+ HANDLE IoCompletionHandle,
+ HANDLE TargetObjectHandle,
+ PVOID KeyContext,
+ PVOID ApcContext,
+ NTSTATUS IoStatus,
+ ULONG_PTR IoStatusInformation,
+ PBOOLEAN AlreadySignaled);
+
+typedef NTSTATUS (NTAPI *sNtCancelWaitCompletionPacket)
+ (HANDLE WaitCompletionPacketHandle,
+ BOOLEAN RemoveSignaledPacket);
+
/*
* Kernel32 headers
*/
+#ifndef CREATE_WAITABLE_TIMER_MANUAL_RESET
+# define CREATE_WAITABLE_TIMER_MANUAL_RESET 0x00000001
+#endif
+
+#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION
+# define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002
+#endif
+
#ifndef FILE_SKIP_COMPLETION_PORT_ON_SUCCESS
# define FILE_SKIP_COMPLETION_PORT_ON_SUCCESS 0x1
#endif
@@ -4813,6 +4840,9 @@ extern sNtQueryVolumeInformationFile pNtQueryVolumeInformationFile;
extern sNtQueryDirectoryFile pNtQueryDirectoryFile;
extern sNtQuerySystemInformation pNtQuerySystemInformation;
extern sNtQueryInformationProcess pNtQueryInformationProcess;
+extern sNtCreateWaitCompletionPacket pNtCreateWaitCompletionPacket;
+extern sNtAssociateWaitCompletionPacket pNtAssociateWaitCompletionPacket;
+extern sNtCancelWaitCompletionPacket pNtCancelWaitCompletionPacket;

/* Powrprof.dll function pointer */
extern sPowerRegisterSuspendResumeNotification pPowerRegisterSuspendResumeNotification;
--- a/src/win/winapi.c
+++ b/src/win/winapi.c
@@ -35,6 +35,9 @@ sNtQueryVolumeInformationFile pNtQueryVolumeInformationFile;
sNtQueryDirectoryFile pNtQueryDirectoryFile;
sNtQuerySystemInformation pNtQuerySystemInformation;
sNtQueryInformationProcess pNtQueryInformationProcess;
+sNtCreateWaitCompletionPacket pNtCreateWaitCompletionPacket;
+sNtAssociateWaitCompletionPacket pNtAssociateWaitCompletionPacket;
+sNtCancelWaitCompletionPacket pNtCancelWaitCompletionPacket;

/* Powrprof.dll function pointer */
sPowerRegisterSuspendResumeNotification pPowerRegisterSuspendResumeNotification;
@@ -70,6 +73,9 @@ void uv__winapi_init(void) {
sNtQueryDirectoryFile pNtQueryDirectoryFile;
sNtQuerySystemInformation pNtQuerySystemInformation;
sNtQueryInformationProcess pNtQueryInformationProcess;
+ sNtCreateWaitCompletionPacket pNtCreateWaitCompletionPacket;
+ sNtAssociateWaitCompletionPacket pNtAssociateWaitCompletionPacket;
+ sNtCancelWaitCompletionPacket pNtCancelWaitCompletionPacket;
sPowerRegisterSuspendResumeNotification pPowerRegisterSuspendResumeNotification;
sProcessPrng pProcessPrng;
sSetWinEventHook pSetWinEventHook;
@@ -133,6 +139,15 @@ void uv__winapi_init(void) {
uv_fatal_error(GetLastError(), "GetProcAddress");
}

+ u.proc = GetProcAddress(ntdll_module, "NtCreateWaitCompletionPacket");
+ pNtCreateWaitCompletionPacket = u.pNtCreateWaitCompletionPacket;
+
+ u.proc = GetProcAddress(ntdll_module, "NtAssociateWaitCompletionPacket");
+ pNtAssociateWaitCompletionPacket = u.pNtAssociateWaitCompletionPacket;
+
+ u.proc = GetProcAddress(ntdll_module, "NtCancelWaitCompletionPacket");
+ pNtCancelWaitCompletionPacket = u.pNtCancelWaitCompletionPacket;
+
powrprof_module = LoadLibraryExA("powrprof.dll",
NULL,
LOAD_LIBRARY_SEARCH_SYSTEM32);
--- a/src/win/core.c
+++ b/src/win/core.c
@@ -224,6 +224,35 @@
}


+/* Per-loop: create a high-res waitable timer + wait-completion packet so
+ * uv__poll can wake at sub-ms precision (see the hrtimer arm there). On
+ * pre-Win10-1803 either the Nt* pointers or the HIGH_RESOLUTION flag are
+ * absent; hrtimer stays NULL and uv__poll keeps its plain GQCS ms wait. */
+static void uv__hrtimer_init(uv__loop_internal_fields_t* lfields) {
+ HANDLE pkt;
+ if (pNtCreateWaitCompletionPacket == NULL ||
+ pNtAssociateWaitCompletionPacket == NULL ||
+ pNtCancelWaitCompletionPacket == NULL)
+ return;
Comment thread
robobun marked this conversation as resolved.
Outdated
+ lfields->hrtimer = CreateWaitableTimerExW(
+ NULL,
+ NULL,
+ CREATE_WAITABLE_TIMER_MANUAL_RESET |
+ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ SYNCHRONIZE | TIMER_QUERY_STATE | TIMER_MODIFY_STATE);
+ if (lfields->hrtimer == NULL)
+ return;
+ pkt = NULL;
+ if (!NT_SUCCESS(pNtCreateWaitCompletionPacket(&pkt, GENERIC_ALL, NULL)) ||
+ pkt == NULL) {
+ CloseHandle(lfields->hrtimer);
+ lfields->hrtimer = NULL;
+ return;
+ }
+ lfields->hrtimer_pkt = pkt;
+}
+
+
int uv_loop_init(uv_loop_t* loop) {
uv__loop_internal_fields_t* lfields;
struct heap* timer_heap;
@@ -300,6 +329,8 @@
if (err)
goto fail_async_init;

+ uv__hrtimer_init(lfields);
+
return 0;

fail_async_init:
@@ -367,6 +398,12 @@
loop->timer_heap = NULL;

lfields = uv__get_internal_fields(loop);
+ if (lfields->hrtimer_pkt != NULL) {
+ pNtCancelWaitCompletionPacket(lfields->hrtimer_pkt, FALSE);
+ CloseHandle(lfields->hrtimer_pkt);
+ }
+ if (lfields->hrtimer != NULL)
+ CloseHandle(lfields->hrtimer);
uv_mutex_destroy(&lfields->loop_metrics.lock);
uv__free(lfields);
loop->internal_fields = NULL;
@@ -463,6 +500,34 @@
*/
lfields->current_timeout = timeout;

+ /* Arm the high-res waitable timer and associate it with this loop's IOCP;
+ * it posts a NULL-overlapped completion (already treated as a pure wakeup
+ * below) so GQCS can block with INFINITE and skip its ~15.6ms-tick ms wait.
+ * >100ms deadlines don't need sub-tick precision; skip the 3 syscalls. */
+ if (timeout - 1 < 100 && lfields->hrtimer != NULL) {

Check warning on line 179 in patches/libuv/win-hrtimer.patch

View check run for this annotation

Claude / Claude Code Review

Accidental revert of explicit timeout bounds from commit 6a019a06

Commit 2757b1fd ("drop process-level probe") accidentally reverted the explicit-bounds change from 6a019a06 — this line is back to `timeout - 1 < 100` instead of `timeout > 0 && timeout <= 100`. Since `timeout` is `DWORD` the two are semantically identical (0−1 wraps to UINT_MAX, INFINITE−1 stays huge), so there's zero runtime effect, but you presumably want the explicit form restored since you dedicated a whole commit to it.
Comment thread
robobun marked this conversation as resolved.
Outdated
+ LARGE_INTEGER due;
+ BOOLEAN signaled;
+ due.QuadPart = -(LONGLONG) timeout * 10000; /* relative, 100ns units */
+ signaled = FALSE;
+ /* STATUS_PENDING => packet is mid-delivery; re-associate would fail,
+ * so skip the arm this round and let the GQCS ms timeout apply. */
+ if (pNtCancelWaitCompletionPacket(lfields->hrtimer_pkt, TRUE)
+ != STATUS_PENDING &&
+ SetWaitableTimer(lfields->hrtimer, &due, 0, NULL, NULL, FALSE) &&
+ NT_SUCCESS(pNtAssociateWaitCompletionPacket(lfields->hrtimer_pkt,
+ loop->iocp,
+ lfields->hrtimer,
+ NULL,
+ NULL,
+ 0,
+ 0,
+ &signaled))) {
+ /* AlreadySignaled => timer fired between SetWaitableTimer and the
+ * associate; the packet is already queued so GQCS won't block. */
+ timeout = signaled ? 0 : INFINITE;
+ }
+ }
+
success = GetQueuedCompletionStatusEx(loop->iocp,
overlappeds,
ARRAY_SIZE(overlappeds),
11 changes: 10 additions & 1 deletion scripts/build/deps/libuv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,16 @@ export const libuv: Dependency = {
// an in-process loopback fetch().abort() can fall into. To upstream:
// send to libuv/libuv with the wepoll/ReactOS references in the patch
// comment as the rationale.
patches: ["patches/libuv/win-poll-rearm-before-callback.patch", "patches/libuv/win-poll-abort-with-disconnect.patch"],
//
// win-hrtimer: GQCS's ms timeout rounds to the ~15.6ms system tick, so
// arm a CREATE_WAITABLE_TIMER_HIGH_RESOLUTION waitable timer and post it
// to the IOCP via NtAssociateWaitCompletionPacket (Go runtime's recipe,
// golang/go#44343). Pre Win10 1803: probe fails, behavior is unchanged.
patches: [
"patches/libuv/win-poll-rearm-before-callback.patch",
"patches/libuv/win-poll-abort-with-disconnect.patch",
"patches/libuv/win-hrtimer.patch",
],

build: () => ({
kind: "direct",
Expand Down
38 changes: 38 additions & 0 deletions test/js/web/timers/setTimeout.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,44 @@ it("clearTimeout with a numeric id is a no-op after a timeout promoted to an int
expect(exitCode).toBe(0);
});

it("setTimeout(1) is not quantized to the ~15.6ms Windows system tick", async () => {
// Subprocess so no other in-process work has raised the Windows tick
// resolution; median of 50 so a single scheduler hiccup on a busy CI
// runner does not fail the assertion.
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
const samples = [];
for (let i = 0; i < 50; i++) {
const t0 = process.hrtime.bigint();
await new Promise(r => setTimeout(r, 1));
samples.push(Number(process.hrtime.bigint() - t0) / 1e6);
}
samples.sort((a, b) => a - b);
const median = samples[samples.length >> 1];
process.stdout.write(JSON.stringify({ median, min: samples[0] }));
`,
],
env: bunEnv,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
const filteredStderr = stderr
.split("\n")
.filter(l => l && !l.startsWith("WARNING: ASAN interferes"))
.join("\n");
expect(filteredStderr).toBe("");
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const { median, min } = JSON.parse(stdout);
// Before: median ~15.6ms. After: median ~1-2ms. 8ms splits the two with
// plenty of headroom for CI jitter. Also assert we never fire early.
expect(median).toBeLessThan(8);
expect(min).toBeGreaterThanOrEqual(1);
expect(exitCode).toBe(0);
});

it("timer heap clock is monotonic, not wall-clock", () => {
// The clock that schedules setTimeout/setInterval deadlines must be monotonic
// (boot-relative) on every platform so NTP steps / user clock changes can't
Expand Down
Loading