diff --git a/src/bun_core/util.rs b/src/bun_core/util.rs index 840ab57a8034..2971d3a2e83f 100644 --- a/src/bun_core/util.rs +++ b/src/bun_core/util.rs @@ -2318,6 +2318,10 @@ unsafe extern "C" { /// directly. #[cfg(unix)] safe fn clock_gettime(clk_id: libc::clockid_t, tp: &mut libc::timespec) -> core::ffi::c_int; + /// Bun C++ shim over `QueryPerformanceCounter` (c-bindings.cpp). Infallible + /// on Windows XP+; out-params are `&mut i64` so pointer validity is typed. + #[cfg(windows)] + safe fn clock_gettime_monotonic(sec: &mut i64, nsec: &mut i64); } impl Default for StackCheck { /// `cached_stack_end` defaults to `0`, so @@ -5265,10 +5269,9 @@ impl Timespec { } } - /// `bun.timespec.now(.allow_mocked_time)` — monotonic-ish "rough tick". - /// Real impl routes through `getRoughTickCount` (jsc); tier-0 reads the - /// monotonic clock directly. Test-runner fake-timers write the mocked - /// nanosecond value via `mock_time::set` / `mock_time::clear`. + /// Monotonic clock (`CLOCK_MONOTONIC` / QPC). Boot-relative on every + /// platform; never compare against wall-clock epoch. Fake-timers override + /// via `mock_time::set` / `mock_time::clear`. #[inline] pub fn now(mode: TimespecMockMode) -> Timespec { if matches!(mode, TimespecMockMode::AllowMockedTime) { @@ -5297,13 +5300,14 @@ impl Timespec { nsec: ts.tv_nsec, } } - #[cfg(not(unix))] + #[cfg(windows)] { - let n = crate::time::nano_timestamp(); - Timespec { - sec: (n / 1_000_000_000) as i64, - nsec: (n % 1_000_000_000) as i64, - } + // QPC via the c-bindings.cpp shim: the same monotonic clock libuv + // (uv_hrtime), uSockets' sweep and WTF::MonotonicTime::now use. + let mut sec: i64 = 0; + let mut nsec: i64 = 0; + clock_gettime_monotonic(&mut sec, &mut nsec); + Timespec { sec, nsec } } } diff --git a/src/io/lib.rs b/src/io/lib.rs index 7076cde561d7..38a0f90724ab 100644 --- a/src/io/lib.rs +++ b/src/io/lib.rs @@ -511,14 +511,11 @@ bun_core::define_scoped_log!(log, io_loop); // hand-declared static above (tagna #[cfg(windows)] mod windows_ffi { // Bun C++ shim over `QueryPerformanceCounter` (src/bun.js/bindings/ - // c-bindings.cpp). + // c-bindings.cpp). Infallible on Windows XP+. unsafe extern "C" { // safe: out-params are `&mut i64` (non-null, valid for write); C++ side - // only writes the slots and returns a status code — no preconditions. - pub(super) safe fn clock_gettime_monotonic( - sec: &mut i64, - nsec: &mut i64, - ) -> core::ffi::c_int; + // only writes the slots — no preconditions. + pub(super) safe fn clock_gettime_monotonic(sec: &mut i64, nsec: &mut i64); } } @@ -1098,8 +1095,7 @@ impl IoRequestLoop { // scope in `windows_ffi` since `extern` blocks can't live in `impl`. let mut sec: i64 = 0; let mut nsec: i64 = 0; - let rc = windows_ffi::clock_gettime_monotonic(&mut sec, &mut nsec); - debug_assert!(rc == 0); + windows_ffi::clock_gettime_monotonic(&mut sec, &mut nsec); timespec.tv_sec = sec.try_into().expect("infallible: size matches"); timespec.tv_nsec = nsec.try_into().expect("infallible: size matches"); } diff --git a/src/jsc/bindings/c-bindings.cpp b/src/jsc/bindings/c-bindings.cpp index 143213d3b368..5dac8137d34f 100644 --- a/src/jsc/bindings/c-bindings.cpp +++ b/src/jsc/bindings/c-bindings.cpp @@ -233,25 +233,21 @@ extern "C" size_t Bun__memoryFootprint() #define NS_PER_HNS (100ULL) // NS = nanoseconds #define NS_PER_SEC (MS_PER_SEC * US_PER_MS * NS_PER_US) -extern "C" int clock_gettime_monotonic(int64_t* tv_sec, int64_t* tv_nsec) +extern "C" void clock_gettime_monotonic(int64_t* tv_sec, int64_t* tv_nsec) { - static LARGE_INTEGER ticksPerSec; - LARGE_INTEGER ticks; - - if (!ticksPerSec.QuadPart) { - QueryPerformanceFrequency(&ticksPerSec); - if (!ticksPerSec.QuadPart) { - errno = ENOTSUP; - return -1; - } - } + // C++11 thread-safe static init: Timespec::now() runs on multiple threads. + // QueryPerformanceFrequency is documented to always succeed on Windows XP+. + static const LARGE_INTEGER ticksPerSec = [] { + LARGE_INTEGER f; + QueryPerformanceFrequency(&f); + return f; + }(); + LARGE_INTEGER ticks; QueryPerformanceCounter(&ticks); *tv_sec = (int64_t)(ticks.QuadPart / ticksPerSec.QuadPart); *tv_nsec = (int64_t)(((ticks.QuadPart % ticksPerSec.QuadPart) * NS_PER_SEC) / ticksPerSec.QuadPart); - - return 0; } extern "C" void windows_enable_stdio_inheritance() diff --git a/src/perf/hw_timer.rs b/src/perf/hw_timer.rs deleted file mode 100644 index 3bec3eae94b7..000000000000 --- a/src/perf/hw_timer.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Unbarriered hardware timestamp counter. -//! -//! Reads the CPU's timestamp counter directly with no instruction barrier, so -//! the read may be reordered relative to surrounding instructions by an OoO -//! core. This trades a tiny amount of fidelity for speed: on Apple Silicon this -//! is ~0.4 ns/call vs ~1.4 ns for `mach_approximate_time` and ~8.7 ns for -//! `mach_absolute_time`, with ~24 ns resolution instead of ~12 µs. On Windows -//! it replaces `GetTickCount64`'s ~15.6 ms granularity. -//! -//! `now_ns()` is calibrated once against the OS monotonic clock so its values -//! share an epoch with `bun.getRoughTickCount()`. For pure A→B deltas where the -//! epoch doesn't matter, `read_counter()` is the cheapest possible read. -//! -//! On x64 Linux/Windows where the TSC frequency isn't exposed by CPUID 0x15, -//! `now_ns()` reads the OS high-res clock per call (vDSO/QPC, ~20 ns) instead — -//! still sub-µs resolution. -//! -//! See WebKit r312153 (UnbarrieredMonotonicTime) for the original design and -//! drift/monotonicity measurements on Darwin/arm64. - -/// Raw counter read. No barriers. -/// - aarch64: `CNTVCT_EL0` (fixed-frequency virtual counter) -/// - x86_64: `rdtsc` -#[inline(always)] -pub fn read_counter() -> u64 { - #[cfg(target_arch = "aarch64")] - { - let ret: u64; - // SAFETY: reading CNTVCT_EL0 is side-effect-free and always valid at EL0. - unsafe { - core::arch::asm!( - "mrs {ret}, CNTVCT_EL0", - ret = out(reg) ret, - options(nomem, nostack, preserves_flags), - ); - } - return ret; - } - #[cfg(target_arch = "x86_64")] - { - let hi: u32; - let lo: u32; - // SAFETY: rdtsc is side-effect-free and always valid in userspace on x86_64. - unsafe { - core::arch::asm!( - "rdtsc", - out("eax") lo, - out("edx") hi, - options(nomem, nostack, preserves_flags), - ); - } - return ((hi as u64) << 32) | (lo as u64); - } - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - compile_error!("hw_timer::read_counter: unsupported architecture"); -} diff --git a/src/perf/lib.rs b/src/perf/lib.rs index bd02a88a50ce..e87b42221ac1 100644 --- a/src/perf/lib.rs +++ b/src/perf/lib.rs @@ -8,7 +8,6 @@ use core::sync::atomic::{AtomicBool, Ordering}; use std::sync::Once; pub mod generated_perf_trace_events; -pub mod hw_timer; pub mod system_timer; pub mod tracy; diff --git a/src/runtime/server/mod.rs b/src/runtime/server/mod.rs index d557ce278f3d..d57b8b95ee98 100644 --- a/src/runtime/server/mod.rs +++ b/src/runtime/server/mod.rs @@ -3961,7 +3961,7 @@ pub mod http_server_agent { this.next_server_id, (*instance.vm()).hot_reload_counter as i32, &url, - bun_core::Timespec::now_allow_mocked_time().ms() as f64, + bun_core::time::milli_timestamp() as f64, instance.ptr.cast(), ); } diff --git a/test/js/web/timers/setTimeout.test.js b/test/js/web/timers/setTimeout.test.js index 243b567818aa..bc22af80402e 100644 --- a/test/js/web/timers/setTimeout.test.js +++ b/test/js/web/timers/setTimeout.test.js @@ -1,4 +1,5 @@ import { spawnSync } from "bun"; +import { timerInternals } from "bun:internal-for-testing"; import { heapStats } from "bun:jsc"; import { expect, it } from "bun:test"; import { bunEnv, bunExe, isWindows } from "harness"; @@ -529,3 +530,15 @@ it("clearTimeout with a numeric id is a no-op after a timeout promoted to an int expect(stdout).toBe("converted: ok\nsurvived\n"); 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 + // stall or mass-fire timers. A wall-clock reading here would be ~= Date.now(). + const t0 = timerInternals.timerClockMs(); + const t1 = timerInternals.timerClockMs(); + const wallNow = Date.now(); + expect(t0).toBeGreaterThan(0); + expect(t1).toBeGreaterThanOrEqual(t0); + expect(t1).toBeLessThan(wallNow / 2); +});