Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
235 changes: 235 additions & 0 deletions patches/libuv/win-hrtimer.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
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, fall back to timeBeginPeriod(1).
--- 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
@@ -36,9 +36,18 @@
#include "heap-inl.h"
#include "req-inl.h"

+/* winmm.dll: timeBeginPeriod. windows.h under WIN32_LEAN_AND_MEAN omits it. */
+__declspec(dllimport) UINT __stdcall timeBeginPeriod(UINT uPeriod);
+#pragma comment(lib, "winmm.lib")
+
/* uv_once initialization guards */
static uv_once_t uv_init_guard_ = UV_ONCE_INIT;

+/* Non-zero when CREATE_WAITABLE_TIMER_HIGH_RESOLUTION and the
+ * Nt*WaitCompletionPacket family are both available (Win10 1803+). When zero
+ * the process falls back to timeBeginPeriod(1). */
+static int uv__have_hrtimer;
+

#if defined(_DEBUG) && (defined(_MSC_VER) || defined(__MINGW64_VERSION_MAJOR))
/* Our crt debug report handler allows us to temporarily disable asserts
@@ -221,6 +230,53 @@ static void uv__init(void) {

/* Initialize system wakeup detection */
uv__init_detect_system_wakeup();
+
+ /* Probe high-res waitable timer + wait-completion-packet support so
+ * uv__poll can wake at sub-ms precision (see the hrtimer arm there).
+ * Pre Win10 1803 falls back to raising the system tick to 1ms. */
+ uv__have_hrtimer = 0;
+ if (pNtCreateWaitCompletionPacket != NULL &&
+ pNtAssociateWaitCompletionPacket != NULL &&
+ pNtCancelWaitCompletionPacket != NULL) {
+ HANDLE h = CreateWaitableTimerExW(
+ NULL,
+ NULL,
+ CREATE_WAITABLE_TIMER_MANUAL_RESET |
+ CREATE_WAITABLE_TIMER_HIGH_RESOLUTION,
+ SYNCHRONIZE | TIMER_QUERY_STATE | TIMER_MODIFY_STATE);
+ if (h != NULL) {
+ uv__have_hrtimer = 1;
+ CloseHandle(h);
+ }
+ }
+ if (!uv__have_hrtimer)
+ timeBeginPeriod(1);
+}
+
+
+/* Per-loop: create the high-res waitable timer + wait-completion packet and
+ * stash them on the loop's internal fields. hrtimer stays NULL on failure,
+ * which uv__poll treats as "use the plain GQCS ms timeout". */
+static void uv__hrtimer_init(uv__loop_internal_fields_t* lfields) {
+ HANDLE pkt;
+ if (!uv__have_hrtimer)
+ 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;
}


Comment thread
robobun marked this conversation as resolved.
Outdated
@@ -300,6 +356,8 @@ int uv_loop_init(uv_loop_t* loop) {
if (err)
goto fail_async_init;

+ uv__hrtimer_init(lfields);
+
return 0;

fail_async_init:
@@ -367,6 +425,12 @@ void uv__loop_close(uv_loop_t* loop) {
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 +527,27 @@ static void uv__poll(uv_loop_t* loop, DWORD timeout) {
*/
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) {
Comment thread
robobun marked this conversation as resolved.
Outdated
+ LARGE_INTEGER due;
+ due.QuadPart = -(LONGLONG) timeout * 10000; /* relative, 100ns units */
+ pNtCancelWaitCompletionPacket(lfields->hrtimer_pkt, TRUE);
+ if (SetWaitableTimer(lfields->hrtimer, &due, 0, NULL, NULL, FALSE) &&
+ NT_SUCCESS(pNtAssociateWaitCompletionPacket(lfields->hrtimer_pkt,
+ loop->iocp,
+ lfields->hrtimer,
+ NULL,
+ NULL,
+ 0,
+ 0,
+ NULL))) {
+ timeout = 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); fall back to timeBeginPeriod(1) on older Windows.
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
40 changes: 40 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,46 @@ 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 () => {
// Windows' default clock tick is ~15.6ms; without a high-resolution timer
// driving the IOCP wait (or timeBeginPeriod), setTimeout(cb, 1) fires ~15ms
// late on an idle box. Run in a subprocess so no other in-process work has
// raised the resolution, and take the median 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