Skip to content

fix(timer): use monotonic QPC for Timespec::now on Windows - #34779

Merged
dylan-conway merged 4 commits into
mainfrom
farm/55b4f203/timespec-windows-monotonic
Jul 20, 2026
Merged

fix(timer): use monotonic QPC for Timespec::now on Windows#34779
dylan-conway merged 4 commits into
mainfrom
farm/55b4f203/timespec-windows-monotonic

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Problem

The timer heap inserts and drains setTimeout/setInterval/AbortSignal.timeout deadlines against Timespec::now() (src/runtime/timer/mod.rs). On unix this reads CLOCK_MONOTONIC, but on Windows Timespec::now_real() fell through to time::nano_timestamp(), which is GetSystemTimePreciseAsFileTime (wall clock):

> bun --expose-internals -e "console.log(require('bun:internal-for-testing').timerInternals.timerClockMs(), Date.now())"
1784526245451 1784526245451

So an NTP correction, manual clock change, or VM-suspend/restore step would stall every pending timer (backward step) or fire them all at once (forward step). The uv_timer wakeup is also armed against libuv's monotonic clock while the heap used wall time, so the two could disagree.

Fix

Route the Windows arm of Timespec::now_real() through the existing clock_gettime_monotonic QPC shim in c-bindings.cpp. This is the same monotonic clock that libuv (uv_hrtime), uSockets' sweep (io::Loop::update_timespec), performance.now() (std::time::Instant) and WTF::MonotonicTime::now() already use.

After:

> bun-debug --expose-internals -e "console.log(require('bun:internal-for-testing').timerInternals.timerClockMs(), Date.now(), require('os').uptime())"
448690 1784526554334 448.671

Caller audit

All Timespec::now / ms_from_now / since_now call sites were checked. Every site uses the value as a delta or for ordering against another Timespec::now-derived deadline (timer heap inserts, elapsed-time logging, debugger wait loops, DNS cache TTL comparison), so changing the Windows epoch from 1970 to boot is safe. Absolute-value uses:

  • Heap/CPU profiler default filenames ({timestamp}.{pid}): already boot-relative on unix, so Windows now matches.
  • InspectorHTTPServerAgent::notify_server_started (src/runtime/server/mod.rs): was sending Timespec::now().ms() while its sibling notify_server_stopped sends wall-clock milli_timestamp(). Switched to milli_timestamp() so DevTools receives start/stop on the same clock on every platform.

Cleanup

  • c-bindings.cpp: clock_gettime_monotonic's static LARGE_INTEGER ticksPerSec is now a C++11 thread-safe static (lambda initializer). This PR makes the shim reachable from multiple threads (Worker timer heaps, install threadpool) for the first time; the previous unsynchronized lazy-init was a benign-in-practice data race.
  • Removed the dead calibrated-TSC src/perf/hw_timer.rs (zero callers; docstring referenced a now_ns() that was never ported).
  • Replaced the stale "monotonic-ish rough tick / routes through getRoughTickCount" doc comment on Timespec::now with an accurate one.

Testing

Added a test asserting timerInternals.timerClockMs() is non-decreasing and < Date.now() / 2 (boot-relative vs epoch). Verified it fails on Windows canary and passes with this change on both Windows (a793f869b) and Linux. Full setTimeout.test.js suite passes on Windows debug.


no test proof · iteration 1 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/web/timers/setTimeout.test.js

The timer heap inserts and drains deadlines against Timespec::now().
On unix this reads CLOCK_MONOTONIC, but on Windows it fell through to
time::nano_timestamp() (GetSystemTimePreciseAsFileTime), which is
wall-clock. An NTP correction, user clock change, or VM-suspend step
would stall every pending timer (backward step) or fire them all at
once (forward step), and the uv_timer wakeup was armed against libuv's
monotonic clock while the heap used wall time.

Route the Windows arm through the existing clock_gettime_monotonic
QPC shim in c-bindings.cpp so the heap shares a clock with libuv
(uv_hrtime), uSockets' sweep (io::Loop::update_timespec) and
WTF::MonotonicTime::now().

All Timespec::now callers were audited: every site uses the value as
a delta or for ordering against another Timespec::now-derived
deadline, so changing the Windows epoch from 1970 to boot is safe.

Also drop the dead calibrated-TSC src/perf/hw_timer.rs (zero callers)
and fix the stale doc comment on Timespec::now.
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 12:30 AM PT - Jul 20th, 2026

@robobun, your commit 5b5512d is building: #76119

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Verified on Windows x64:

Before (canary 1.4.0-canary.1+bb095b301):

timerClockMs: 1784526245451
Date.now:     1784526245451

After (debug build of this branch, 5b5512dd9):

timerClockMs: 448690
Date.now:     1784526554334
uptime (s):   448.671

cargo check -p bun_core -p bun_perf -p bun_io clean on x86_64-pc-windows-msvc and aarch64-pc-windows-msvc. Full test/js/web/timers/setTimeout.test.js passes on Windows debug (31 pass, 1 todo). All review threads resolved.

Earlier CI runs had only unrelated failures: debian x64-asan complex-workspace.test.ts / test-http2-reset-flood.js (known main breaks), darwin-26 Tart VM boot failure (infra), and flaky test-repl-close.js that passed on retry. None touch Windows-gated code.

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Windows: awaiting the abort event of AbortSignal.timeout() hangs the test runner #33334 - AbortSignal.timeout() hangs on Windows; the wall-clock/monotonic clock mismatch in the timer heap could prevent timely event dispatch after the timer fires
  2. setInterval() timing way off on Windows #26965 - setInterval() timing ~75% off on Windows (28ms instead of 16ms); clock source disagreement could cause systematic drift
  3. setTimeout(fn, 0) slower to schedule on Windows #16714 - setTimeout(fn, 0) is 10-15x slower on Windows; heap/uv_timer clock mismatch could cause unnecessary delays for zero-delay timers

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #33334
Fixes #26965
Fixes #16714

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Monotonic timer clock

Layer / File(s) Summary
Windows monotonic clock source
src/bun_core/util.rs
Windows Timespec::now_real() now uses the clock_gettime_monotonic FFI shim, with documentation describing boot-relative time and fake timer overrides.
Timer heap clock regression coverage
test/js/web/timers/setTimeout.test.js
Adds coverage verifying timerInternals.timerClockMs() is positive and substantially below wall-clock time.

Hardware timer module removal

Layer / File(s) Summary
Remove hardware timer export
src/perf/lib.rs, src/perf/hw_timer.rs
Removes the public hw_timer module and its architecture-specific raw counter reader.

Possibly related PRs

  • oven-sh/bun#33359: Both changes directly modify how timer-heap timing is computed or used.
  • oven-sh/bun#33896: Both changes adjust the timer heap’s real-clock behavior alongside fake timers.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely summarizes the main Windows monotonic timer change.
Description check ✅ Passed It includes the problem, fix, audit, cleanup, and testing details, though it uses custom headings instead of the template’s exact ones.

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/bun_core/util.rs`:
- Around line 2321-2324: Handle the nonzero status from clock_gettime_monotonic
by exposing it as a typed Rust error rather than discarding it, then update the
call sites in src/bun_core/util.rs lines 5303-5311 and src/io/lib.rs lines
1096-1101 to propagate that error or apply an explicit fallback before
constructing Timespec; preserve successful timestamp behavior.

In `@test/js/web/timers/setTimeout.test.js`:
- Around line 535-537: Update the regression-test comment near the
setTimeout/setInterval monotonic-clock assertion to remove the explanatory prose
and retain only the tracked issue URL, following repository comment guidelines.
- Around line 534-542: Update the “timer heap clock is monotonic, not
wall-clock” test to read timerInternals.timerClockMs() twice around a bounded
event and assert the second reading is greater than or equal to the first. Keep
the existing positive and boot-relative wall-clock checks separate, without
relying on Date.now() to establish monotonicity.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b1dffba4-55f9-42dd-86c8-b381baa2c3e3

📥 Commits

Reviewing files that changed from the base of the PR and between 5c28061 and 22812d0.

📒 Files selected for processing (4)
  • src/bun_core/util.rs
  • src/perf/hw_timer.rs
  • src/perf/lib.rs
  • test/js/web/timers/setTimeout.test.js
💤 Files with no reviewable changes (2)
  • src/perf/hw_timer.rs
  • src/perf/lib.rs

Comment thread src/bun_core/util.rs Outdated
Comment thread test/js/web/timers/setTimeout.test.js
Comment thread test/js/web/timers/setTimeout.test.js

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Beyond the inline nits, I also checked: the #[cfg(not(unix))]#[cfg(windows)] narrowing on now_real() drops no supported target (Bun only ships linux/macos/windows); and hw_timer::read_counter has zero remaining references repo-wide, so the deletion is safe.

Extended reasoning...

Two nit-level findings are already posted inline. This note just records what else was examined and ruled out: (1) the cfg narrowing from not(unix) to windows could in principle leave a platform with no now_real() body, but Bun's supported targets are linux/macos/windows only, so nothing is dropped; (2) grepped all of src/ for hw_timer and read_counter — no hits, so the dead-file deletion is clean; (3) timerInternals.timerClockMs is an existing binding already exercised by test-timers-ordering.js, so the new test's import is not novel surface. Not approving because this changes the clock source underlying every Windows timer and the PR's own caller audit missed at least one absolute-value consumer — worth a human look.

Comment thread src/bun_core/util.rs
Comment thread src/bun_core/util.rs
…t strengthening

- c-bindings.cpp: make ticksPerSec a C++11 thread-safe static since
  Timespec::now() now calls clock_gettime_monotonic from multiple threads
  (Worker timer heaps, install threadpool). QueryPerformanceFrequency is
  documented to always succeed on Windows XP+, so the ENOTSUP path is
  unreachable but kept for belt-and-suspenders.
- server/mod.rs: notify_server_started now sends wall-clock
  milli_timestamp_allow_mocked_time() to match its notify_server_stopped
  sibling, so DevTools receives start/stop on the same clock base on
  every platform (was already mismatched on unix).
- setTimeout.test.js: add a second clock read and assert it is
  non-decreasing.
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Re the find-issues suggestions above: I tested all three and none are fixed by this PR.

Not adding Fixes lines.

Comment thread src/runtime/server/mod.rs Outdated
Align notify_server_started with its notify_server_stopped sibling on
both axes: wall-clock (vs monotonic) and real (vs fake-timers-mocked).
The inspector timestamps reach a real DevTools UI, not test assertions,
so respecting fake-timers here has no benefit and would disagree with
the stop event.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior review findings are addressed and the b3f41ce pass found nothing new, but this changes the clock base for the Windows timer heap (every setTimeout/setInterval/AbortSignal.timeout deadline), so a maintainer should sign off.

Checked this round: notify_server_started/notify_server_stopped now both send plain milli_timestamp() (same clock, same mocked-ness); the C++ ticksPerSec init is a proper C++11 thread-safe static; no remaining references to hw_timer/read_counter anywhere in src/; the #[cfg(not(unix))]#[cfg(windows)] narrowing leaves no supported target uncovered.

Extended reasoning...

Overview

Six files: the Windows arm of Timespec::now_real() in src/bun_core/util.rs now calls the QPC clock_gettime_monotonic shim instead of wall-clock nano_timestamp(); the shim in src/jsc/bindings/c-bindings.cpp gets a C++11 thread-safe static for ticksPerSec; src/runtime/server/mod.rs:3964 switches the inspector startTime to milli_timestamp() to match its notify_server_stopped sibling; the dead calibrated-TSC src/perf/hw_timer.rs module is deleted and de-registered from src/perf/lib.rs; and test/js/web/timers/setTimeout.test.js gains a monotonic + boot-relative assertion via timerInternals.timerClockMs().

Security risks

None. This is a clock-source selection change with no user-controlled input, no parsing, no auth/crypto surface.

Level of scrutiny

High. Timespec::now() is the reference clock for the timer heap (src/runtime/timer/mod.rs) and is read from dozens of call sites across install, DNS cache, lifecycle scripts, debugger wait loops, and profiler filenames. Changing its epoch on Windows from Unix-1970 to boot is correct (aligns with the unix CLOCK_MONOTONIC behavior and with libuv's uv_hrtime that arms the wakeup), but any missed absolute-value consumer would silently misbehave. My earlier pass found one such consumer (inspector startTime) that the PR's caller audit had missed; it's now fixed, and I found no others this round.

Other factors

All three of my prior inline findings are addressed and resolved: the ticksPerSec lazy-init race (fixed with a lambda-initialized magic static in a793f86), the inspector start/stop clock mismatch (a793f86), and the mocked-vs-real asymmetry between the sibling notifiers (b3f41ce — both now use plain milli_timestamp(), verified at mod.rs:3964/3978). CodeRabbit's QPF-failure concern was correctly declined (documented never-fails on XP+; debug_assert! matches src/io/lib.rs). I re-grepped src/ for hw_timer/read_counter — zero hits, deletion is safe. The #[cfg(not(unix))] → #[cfg(windows)] narrowing is fine since every Bun target is unix or windows. The new test's t1 < Date.now()/2 bound gives ~28 years of uptime headroom, well beyond any CI runner.

Deferring rather than approving because this is a platform-wide behavioral change to core timer infrastructure (critical code path per the approval guidelines), and Windows CI for b3f41ce (build #76075) is the actual proof — I can't run it here.

QueryPerformanceFrequency is documented to always succeed on Windows XP
and later, so the nonzero-return path was dead. Change the shim to void
and update both Rust extern declarations and call sites accordingly.
@dylan-conway
dylan-conway merged commit 86fc072 into main Jul 20, 2026
37 of 52 checks passed
@dylan-conway
dylan-conway deleted the farm/55b4f203/timespec-windows-monotonic branch July 20, 2026 07:40

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't find any issues this pass — all three concerns from my earlier reviews (thread-safe QPF init, inspector startTime clock, mocked-vs-real milli_timestamp sibling mismatch) are addressed, and the void-return signature change in 5b5512d is reflected at both Rust extern sites. Deferring to a human because this changes the timer-heap clock epoch for every setTimeout/setInterval on Windows and Windows CI hasn't reported yet.

Checked: #[cfg(unix)]/#[cfg(windows)] arms of now_real() are exhaustive for supported targets; hw_timer/read_counter have zero remaining references; notify_server_started/_stopped now agree on both the wall-clock and mocked axes; the < Date.now()/2 test bound holds for any uptime under ~28 years so it can't false-negative.

Extended reasoning...

Overview

Routes Windows Timespec::now_real() through the existing clock_gettime_monotonic QPC shim so the timer heap uses the same monotonic clock as libuv/uSockets/WTF instead of wall-clock GetSystemTimePreciseAsFileTime. Follow-up commits: made the shim's ticksPerSec a C++11 thread-safe static (a793f86), switched the inspector startTime to plain milli_timestamp() to match its _stopped sibling (b3f41ce), and dropped the now-unreachable ENOTSUP path by making the shim void-returning with both Rust extern decls updated (5b5512d). Also deletes the unused src/perf/hw_timer.rs and adds a timerClockMs() regression test.

Security risks

None. No user input, auth, crypto, or network parsing touched.

Level of scrutiny

High — this changes the clock epoch for every timer-heap insert/drain on Windows from 1970 to boot. The change itself is small and well-reasoned, but the caller audit already had one miss (inspector startTime) that surfaced in review, and the test evidence defers to Windows CI. A human should confirm CI is green on Windows before merge.

Other factors

All prior review threads (mine and CodeRabbit's) are resolved. Verified this pass: the int → void FFI signature change is consistent across c-bindings.cpp, src/bun_core/util.rs, and src/io/lib.rs; hw_timer has no remaining references anywhere in src/; the two #[cfg] arms in now_real() leave exactly one tail expression per supported target; and the new test's < Date.now()/2 bound (~28 years of uptime) correctly discriminates the pre-fix wall-clock reading without false positives.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants