From 1e21280d7c0bf4355ec8a016db2cfae14f4ea140 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:36:51 +0000 Subject: [PATCH 1/8] WebKit: use GetSystemTimePreciseAsFileTime for WallTime and QPC for MonotonicTime on Windows Bumps WEBKIT_VERSION to the oven-sh/WebKit#304 preview build so bun's Windows CI exercises the new clock sources. With that change, JSC's Date.now() and bun_core::time::milli_timestamp() read the same kernel clock on every platform, so the two remaining JS-visible defaults that compared against Date.now() can read it directly: - Bun.randomUUIDv7() default timestamp now reads global.js_date_now() instead of milli_timestamp(), which is the same clock source crypto.randomUUIDv7() already uses. Both paths are now the same js_date_now() + UUID7::init; only the option validation and disableEntropyCache differ. - new File([], name).lastModified default now reads js_date_now(), which is what the File API spec says the default is ("the equivalent of Date.now()"). Both defaults also now respect setSystemTime(), matching Date.now(). Tests: a Date.now() bracketing check over Bun.randomUUIDv7 / crypto.randomUUIDv7 / File.lastModified (50k iters on Windows where the ~1ms skew used to bite ~80% of samples), and a setSystemTime() pinning check for all three. --- scripts/build/deps/webkit.ts | 2 +- src/runtime/node/node_crypto_binding.rs | 4 +- src/runtime/webcore/Blob.rs | 5 +- src/runtime/webcore/Crypto.rs | 7 ++- test/js/bun/util/randomUUIDv7.test.ts | 76 ++++++++++++++++++++++++- 5 files changed, 86 insertions(+), 8 deletions(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index b7ca822369da..975e4354fb13 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -7,7 +7,7 @@ // -lto variants built with ThinLTO (per-module summaries for cross-language // importing), and the Windows ICU data table filtered + per-item zstd // compressed (lazily decompressed via bun_icu_decompress.cpp). -export const WEBKIT_VERSION = "4895f45dfbd0d1226c4d41799887bc0ecb9f341b"; +export const WEBKIT_VERSION = "autobuild-preview-pr-304-36986ac9"; /** * WebKit (JavaScriptCore) — the JS engine. diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 83c9824dc388..e901029690af 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -517,8 +517,8 @@ pub mod random { } } - // jsDateNow() is exactly what JS Date.now() returns, so the embedded - // timestamp is never behind a Date.now() sample taken by the caller. + // Same clock source and UUID7 path as Bun.randomUUIDv7(); only the + // option validation and disableEntropyCache differ. let now_ms = global.js_date_now().max(0.0) as u64; let mut entropy = [0u8; 10]; if disable_entropy_cache { diff --git a/src/runtime/webcore/Blob.rs b/src/runtime/webcore/Blob.rs index 3e921301c2e9..7e89fb346352 100644 --- a/src/runtime/webcore/Blob.rs +++ b/src/runtime/webcore/Blob.rs @@ -5710,9 +5710,8 @@ pub fn jsdom_file_construct_( } if !set_last_modified { - // `lastModified` should be the current date in milliseconds if unspecified. - blob.last_modified - .set(bun_core::time::milli_timestamp() as f64); + // File API spec: default is "the equivalent of Date.now()". + blob.last_modified.set(global_this.js_date_now()); } if blob.content_type_slice().is_empty() { diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 87e98928e2d6..eb7adea62a3e 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -247,7 +247,12 @@ pub(crate) fn bun_random_uuid_v7( .unwrap(); } - break 'brk u64::try_from(bun_core::time::milli_timestamp().max(0)).expect("int cast"); + // jsDateNow() is the exact value JS Date.now() would return (same + // precise system clock as milli_timestamp() on every platform since + // oven-sh/WebKit#304, plus any setSystemTime() override), so a caller + // bracketing this call with Date.now() always observes + // before <= embedded-timestamp <= after. + break 'brk global.js_date_now().max(0.0) as u64; }; // SAFETY: `bun_vm()` never returns null for a Bun-owned global. diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index 50167d8daf2d..4dec43aaa917 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "bun:test"; -import { bunEnv, bunExe } from "harness"; +import { bunEnv, bunExe, isWindows } from "harness"; describe("randomUUIDv7", () => { test("basic", () => { @@ -210,4 +210,78 @@ describe("randomUUIDv7", () => { expect(Number(stdout.trim())).toBeGreaterThan(1); expect(exitCode).toBe(0); }); + + // https://github.com/oven-sh/WebKit/pull/304 + test("default timestamp is bracketed by Date.now()", async () => { + // Before oven-sh/WebKit#304, Windows Date.now() (WTF QPC-interpolated) ran + // up to ~1ms ahead of the native precise clock the runtime read for default + // timestamps, so before > embedded-timestamp in ~80% of samples. With that + // fixed, Bun.randomUUIDv7 / crypto.randomUUIDv7 / File.lastModified all + // default to global.js_date_now(), which is Date.now() exactly. + // Subprocess keeps the process-global UUIDv7 last-timestamp untouched. + const N = isWindows ? 50_000 : 5_000; + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const crypto = require("node:crypto"); + const tsOf = buf => buf.readUIntBE(0, 6); + const tsOfHex = s => parseInt(s.replaceAll("-", "").slice(0, 12), 16); + let bad = { bun: null, node: null, file: null }; + for (let i = 0; i < ${N}; i++) { + const before = Date.now(); + const b = tsOf(Bun.randomUUIDv7("buffer")); + const c = tsOfHex(crypto.randomUUIDv7()); + const f = new File([], "x").lastModified; + const after = Date.now(); + if (bad.bun === null && !(before <= b && b <= after)) bad.bun = { i, before, b, after }; + if (bad.node === null && !(before <= c && c <= after)) bad.node = { i, before, c, after }; + if (bad.file === null && !(before <= f && f <= after)) bad.file = { i, before, f, after }; + if (bad.bun && bad.node && bad.file) break; + } + console.log(JSON.stringify(bad)); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ bun: null, node: null, file: null }); + expect(exitCode).toBe(0); + }); + + test("default timestamp respects setSystemTime()", async () => { + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const { setSystemTime } = require("bun:test"); + const crypto = require("node:crypto"); + const tsOf = s => parseInt(s.replaceAll("-", "").slice(0, 12), 16); + const pin = 1_700_000_000_000; + setSystemTime(pin); + console.log(JSON.stringify({ + dateNow: Date.now(), + bun: tsOf(Bun.randomUUIDv7()), + node: tsOf(crypto.randomUUIDv7()), + file: new File([], "x").lastModified, + })); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toEqual({ + dateNow: 1_700_000_000_000, + bun: 1_700_000_000_000, + node: 1_700_000_000_000, + file: 1_700_000_000_000, + }); + expect(exitCode).toBe(0); + }); }); From 1324097349b40977a6d9972e0a2d9610b90553eb Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:43:00 +0000 Subject: [PATCH 2/8] Extract shared uuid_v7_at / uuid_v7_to_hex_js so crypto.randomUUIDv7 calls the same core as Bun.randomUUIDv7 Both now route entropy+UUID7::init+hex-print through webcore::crypto::{uuid_v7_at, uuid_v7_to_hex_js}. Only the argument validation (Bun's encoding/timestamp vs Node's options object) stays separate. --- src/runtime/node/node_crypto_binding.rs | 26 ++++----------- src/runtime/webcore/Crypto.rs | 42 ++++++++++++++++++------- 2 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index e901029690af..f5ca6fc5892d 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -312,7 +312,7 @@ pub mod random { use super::*; use crate::node::util::validators; use bun_core::String as BunString; - use bun_jsc::{JSType, StringJsc as _, UUID, UUID7}; + use bun_jsc::{JSType, StringJsc as _, UUID}; #[bun_jsc::host_fn] pub(crate) fn random_int( @@ -517,25 +517,11 @@ pub mod random { } } - // Same clock source and UUID7 path as Bun.randomUUIDv7(); only the - // option validation and disableEntropyCache differ. - let now_ms = global.js_date_now().max(0.0) as u64; - let mut entropy = [0u8; 10]; - if disable_entropy_cache { - boringssl::rand_bytes(&mut entropy); - } else { - entropy - .copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]); - } - let uuid = UUID7::init(now_ms, entropy); - - let (mut str, bytes) = BunString::create_uninitialized_latin1(36); - uuid.print( - (&mut bytes[..36]) - .try_into() - .expect("infallible: size matches"), - ); - str.transfer_to_js(global) + // Same implementation as Bun.randomUUIDv7()'s default path; only + // the option validation above differs. + let timestamp = global.js_date_now().max(0.0) as u64; + let uuid = crate::webcore::crypto::uuid_v7_at(global, timestamp, disable_entropy_cache); + crate::webcore::crypto::uuid_v7_to_hex_js(global, &uuid) } pub(crate) fn assert_offset( diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index eb7adea62a3e..595eabdad30b 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -255,24 +255,44 @@ pub(crate) fn bun_random_uuid_v7( break 'brk global.js_date_now().max(0.0) as u64; }; - // SAFETY: `bun_vm()` never returns null for a Bun-owned global. - let entropy = global.bun_vm().as_mut().rare_data().entropy_slice(10); - - let uuid = UUID7::init(timestamp, <[u8; 10]>::try_from(&entropy[0..10]).unwrap()); + let uuid = uuid_v7_at(global, timestamp, false); if encoding == Encoding::Hex { - let (mut str, bytes) = BunString::create_uninitialized_latin1(36); - uuid.print( - (&mut bytes[0..36]) - .try_into() - .expect("infallible: size matches"), - ); - return str.transfer_to_js(global); + return uuid_v7_to_hex_js(global, &uuid); } encoding.encode_with_max_size(global, 32, &uuid.bytes) } +/// Shared core of `Bun.randomUUIDv7()` and `crypto.randomUUIDv7()`: 10 bytes +/// of entropy from the VM cache (or fresh BoringSSL bytes when bypassed), +/// fed to `UUID7::init` at `timestamp`. Only argument validation differs +/// between the two public functions. +pub(crate) fn uuid_v7_at( + global: &JSGlobalObject, + timestamp: u64, + disable_entropy_cache: bool, +) -> UUID7 { + let mut entropy = [0u8; 10]; + if disable_entropy_cache { + bun_boringssl_sys::rand_bytes(&mut entropy); + } else { + entropy.copy_from_slice(&global.bun_vm().as_mut().rare_data().entropy_slice(10)[..10]); + } + UUID7::init(timestamp, entropy) +} + +/// Renders `uuid` as the canonical 36-character string. +pub(crate) fn uuid_v7_to_hex_js(global: &JSGlobalObject, uuid: &UUID7) -> JsResult { + let (mut str, bytes) = BunString::create_uninitialized_latin1(36); + uuid.print( + (&mut bytes[..36]) + .try_into() + .expect("infallible: size matches"), + ); + str.transfer_to_js(global) +} + #[bun_jsc::host_fn(export = "Bun__randomUUIDv5")] pub(crate) fn bun_random_uuid_v5( global: &JSGlobalObject, From 07080291d6bfe87a2d8d44074dbed6fdbd8ee155 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 00:55:39 +0000 Subject: [PATCH 3/8] Address review: drop UUID upper-bound check (rollover bump); shorten Crypto.rs comment UUID7::init may bump the embedded timestamp by +1ms on 12-bit counter rollover, so the test now only asserts the lower bound for the UUID paths (which is what the WebKit clock change actually fixes) and keeps full bracketing for File.lastModified, which has no counter. The Crypto.rs comment is down to 3 lines and no longer claims the embedded ts is bracketed or references the PR number. --- src/runtime/webcore/Crypto.rs | 8 +++----- test/js/bun/util/randomUUIDv7.test.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 595eabdad30b..708befcf3e8e 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -247,11 +247,9 @@ pub(crate) fn bun_random_uuid_v7( .unwrap(); } - // jsDateNow() is the exact value JS Date.now() would return (same - // precise system clock as milli_timestamp() on every platform since - // oven-sh/WebKit#304, plus any setSystemTime() override), so a caller - // bracketing this call with Date.now() always observes - // before <= embedded-timestamp <= after. + // js_date_now() is exactly Date.now() (same precise clock + any + // setSystemTime() override), so the input timestamp is never behind a + // caller's Date.now() sample. UUID7::init may bump it on rollover. break 'brk global.js_date_now().max(0.0) as u64; }; diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index 4dec43aaa917..03c3f0057dfa 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -212,13 +212,14 @@ describe("randomUUIDv7", () => { }); // https://github.com/oven-sh/WebKit/pull/304 - test("default timestamp is bracketed by Date.now()", async () => { + test("default timestamp is never behind Date.now()", async () => { // Before oven-sh/WebKit#304, Windows Date.now() (WTF QPC-interpolated) ran // up to ~1ms ahead of the native precise clock the runtime read for default - // timestamps, so before > embedded-timestamp in ~80% of samples. With that - // fixed, Bun.randomUUIDv7 / crypto.randomUUIDv7 / File.lastModified all - // default to global.js_date_now(), which is Date.now() exactly. - // Subprocess keeps the process-global UUIDv7 last-timestamp untouched. + // timestamps, so before > embedded-timestamp in ~80% of samples. All three + // now default to js_date_now() which is Date.now() exactly. UUID7::init may + // bump the embedded timestamp on 12-bit counter rollover (RFC 9562 §6.2), + // so only the lower bound is asserted for the UUID paths; File.lastModified + // has no counter and is fully bracketed. const N = isWindows ? 50_000 : 5_000; await using proc = Bun.spawn({ cmd: [ @@ -235,8 +236,8 @@ describe("randomUUIDv7", () => { const c = tsOfHex(crypto.randomUUIDv7()); const f = new File([], "x").lastModified; const after = Date.now(); - if (bad.bun === null && !(before <= b && b <= after)) bad.bun = { i, before, b, after }; - if (bad.node === null && !(before <= c && c <= after)) bad.node = { i, before, c, after }; + if (bad.bun === null && !(before <= b)) bad.bun = { i, before, b }; + if (bad.node === null && !(before <= c)) bad.node = { i, before, c }; if (bad.file === null && !(before <= f && f <= after)) bad.file = { i, before, f, after }; if (bad.bun && bad.node && bad.file) break; } From 11736aca99c12d214034d1386a8a9de0d2bb78f8 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 01:39:05 +0000 Subject: [PATCH 4/8] Address review: condense test comment to 3 lines; tighten blob.test.ts lastModified-default to a bracket The blob.test.ts comment described the pre-change native-clock skew that this PR removes; drop it and assert the Date.now() bracket the default now satisfies. --- test/js/bun/util/randomUUIDv7.test.ts | 10 +++------- test/js/web/fetch/blob.test.ts | 11 ++++++----- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index 03c3f0057dfa..55400404d09f 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -213,13 +213,9 @@ describe("randomUUIDv7", () => { // https://github.com/oven-sh/WebKit/pull/304 test("default timestamp is never behind Date.now()", async () => { - // Before oven-sh/WebKit#304, Windows Date.now() (WTF QPC-interpolated) ran - // up to ~1ms ahead of the native precise clock the runtime read for default - // timestamps, so before > embedded-timestamp in ~80% of samples. All three - // now default to js_date_now() which is Date.now() exactly. UUID7::init may - // bump the embedded timestamp on 12-bit counter rollover (RFC 9562 §6.2), - // so only the lower bound is asserted for the UUID paths; File.lastModified - // has no counter and is fully bracketed. + // All three default to js_date_now() (== Date.now()). UUID7::init may bump + // the embedded ts on 12-bit counter rollover (RFC 9562 §6.2), so only the + // lower bound is asserted for UUIDs; File.lastModified has no counter. const N = isWindows ? 50_000 : 5_000; await using proc = Bun.spawn({ cmd: [ diff --git a/test/js/web/fetch/blob.test.ts b/test/js/web/fetch/blob.test.ts index 8aa907c12b92..17a3bacd85e3 100644 --- a/test/js/web/fetch/blob.test.ts +++ b/test/js/web/fetch/blob.test.ts @@ -222,13 +222,14 @@ describe("new File() lastModified option", () => { expect(lm({ lastModified: input })).toBe(expected); }); - // The default comes from a native wall-clock read that may differ from JS - // Date.now() by a few ms on Windows; assert "current time" within a wide - // tolerance rather than an exact bracket. test.each([[{ lastModified: undefined }], [{}]])("%p defaults to the current time", opts => { + const before = Date.now(); const value = lm(opts); - expect(Number.isFinite(value)).toBe(true); - expect(Math.abs(value - Date.now())).toBeLessThan(60_000); + const after = Date.now(); + expect({ finite: Number.isFinite(value), bracketed: before <= value && value <= after }).toEqual({ + finite: true, + bracketed: true, + }); }); test("valueOf throwing propagates", () => { From 5285240de4314999df94d37e818dd1562a3c1846 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:31:17 +0000 Subject: [PATCH 5/8] test: Date.now() is never ahead of performance.timeOrigin + performance.now() on Windows Pure-JS observable of the WebKit-side clock change: performance.timeOrigin is set from the precise kernel clock at startup and performance.now() is QPC elapsed, so their sum tracks the precise clock. With Date.now() now reading the same clock, sampling Date.now() then the sum gives floor(t1) - t2 <= 0. On the previous WTF QPC-interpolated path, Date.now() ran ~0.4ms ahead and ~72% of 200k samples had Date.now() > the sum. --- test/js/bun/util/randomUUIDv7.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index 55400404d09f..b42dc549d31c 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -212,6 +212,20 @@ describe("randomUUIDv7", () => { }); // https://github.com/oven-sh/WebKit/pull/304 + test.skipIf(!isWindows)("Date.now() is never ahead of performance.timeOrigin + performance.now()", () => { + // performance.timeOrigin + performance.now() is precise-clock-at-start + + // QPC elapsed. With Date.now() on the same precise clock, floor(t1) <= t2 + // for t1 <= t2; before, Date.now() ran ~0.4ms ahead in ~72% of samples. + const origin = performance.timeOrigin; + let firstAhead = null; + for (let i = 0; i < 50_000; i++) { + const d = Date.now(); + const p = origin + performance.now(); + if (d > p && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(3) }; + } + expect(firstAhead).toBe(null); + }); + test("default timestamp is never behind Date.now()", async () => { // All three default to js_date_now() (== Date.now()). UUID7::init may bump // the embedded ts on 12-bit counter rollover (RFC 9562 §6.2), so only the From 52820f783951bd35557b4bddfcb599c22235ea76 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 05:50:10 +0000 Subject: [PATCH 6/8] Address review: run the Date.now()/performance clock test in a subprocess; trim uuid_v7_at doc to 3 lines In a subprocess, performance.timeOrigin is captured milliseconds before the loop, so w32tm slew between VM init and the test cannot drift the live wall clock ahead of timeOrigin + QPC elapsed. --- src/runtime/webcore/Crypto.rs | 3 +-- test/js/bun/util/randomUUIDv7.test.ts | 38 ++++++++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/runtime/webcore/Crypto.rs b/src/runtime/webcore/Crypto.rs index 708befcf3e8e..435ffa9839ba 100644 --- a/src/runtime/webcore/Crypto.rs +++ b/src/runtime/webcore/Crypto.rs @@ -264,8 +264,7 @@ pub(crate) fn bun_random_uuid_v7( /// Shared core of `Bun.randomUUIDv7()` and `crypto.randomUUIDv7()`: 10 bytes /// of entropy from the VM cache (or fresh BoringSSL bytes when bypassed), -/// fed to `UUID7::init` at `timestamp`. Only argument validation differs -/// between the two public functions. +/// fed to `UUID7::init` at `timestamp`. pub(crate) fn uuid_v7_at( global: &JSGlobalObject, timestamp: u64, diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index b42dc549d31c..f619926fe434 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -212,18 +212,32 @@ describe("randomUUIDv7", () => { }); // https://github.com/oven-sh/WebKit/pull/304 - test.skipIf(!isWindows)("Date.now() is never ahead of performance.timeOrigin + performance.now()", () => { - // performance.timeOrigin + performance.now() is precise-clock-at-start + - // QPC elapsed. With Date.now() on the same precise clock, floor(t1) <= t2 - // for t1 <= t2; before, Date.now() ran ~0.4ms ahead in ~72% of samples. - const origin = performance.timeOrigin; - let firstAhead = null; - for (let i = 0; i < 50_000; i++) { - const d = Date.now(); - const p = origin + performance.now(); - if (d > p && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(3) }; - } - expect(firstAhead).toBe(null); + test.skipIf(!isWindows)("Date.now() is never ahead of performance.timeOrigin + performance.now()", async () => { + // Subprocess so timeOrigin is captured milliseconds before the loop and + // w32tm slew between VM init and test cannot drift the two clocks apart. + // Before, Date.now() ran ~0.4ms ahead in ~72% of samples. + await using proc = Bun.spawn({ + cmd: [ + bunExe(), + "-e", + ` + const origin = performance.timeOrigin; + let firstAhead = null; + for (let i = 0; i < 50_000; i++) { + const d = Date.now(); + const p = origin + performance.now(); + if (d > p && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(3) }; + } + console.log(JSON.stringify(firstAhead)); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + expect(JSON.parse(stdout)).toBe(null); + expect(exitCode).toBe(0); }); test("default timestamp is never behind Date.now()", async () => { From 5104e8fe07469c87cf73bb8e026e87b65bb715e3 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:12:33 +0000 Subject: [PATCH 7/8] test: allow 0.01ms double-precision slack in the Date.now()/performance clock test origin + performance.now() at ~1.8e12 has ~0.0004ms double ULP, so the sum can round below the true value and make d > p by a fraction of a microsecond. CI hit d - p = 0.0002 on one sample. The old-path skew was ~0.4ms, so a 0.01ms threshold still separates the two cleanly. --- test/js/bun/util/randomUUIDv7.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/js/bun/util/randomUUIDv7.test.ts b/test/js/bun/util/randomUUIDv7.test.ts index f619926fe434..d8f1c8ab332b 100644 --- a/test/js/bun/util/randomUUIDv7.test.ts +++ b/test/js/bun/util/randomUUIDv7.test.ts @@ -222,11 +222,13 @@ describe("randomUUIDv7", () => { "-e", ` const origin = performance.timeOrigin; + // origin + perf.now() at ~1.8e12 has ~0.0004ms double ULP; the old + // skew was ~0.4ms, so a 0.01ms threshold separates the two cleanly. let firstAhead = null; for (let i = 0; i < 50_000; i++) { const d = Date.now(); const p = origin + performance.now(); - if (d > p && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(3) }; + if (d - p > 0.01 && firstAhead === null) firstAhead = { i, d, p, diff: +(d - p).toFixed(4) }; } console.log(JSON.stringify(firstAhead)); `, From 48490a4ad8076a73c945367872eef8a4b80b2d20 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:32:35 +0000 Subject: [PATCH 8/8] Swap WEBKIT_VERSION to the merged oven-sh/WebKit#304 main sha autobuild-722f2a8a1a3da159a89b35730c5460a6ef58f0af is now published (43 assets). Also pulls in the three WebKit main commits that landed between the preview base and the merge: 365cb024 (Dockerfile.windows build flags), ae5110d3 (defer termination at reifyStaticProperty call sites), 8be99556 (AsyncContextSwapScope). --- scripts/build/deps/webkit.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index 975e4354fb13..df5a2fb01e3b 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -7,7 +7,7 @@ // -lto variants built with ThinLTO (per-module summaries for cross-language // importing), and the Windows ICU data table filtered + per-item zstd // compressed (lazily decompressed via bun_icu_decompress.cpp). -export const WEBKIT_VERSION = "autobuild-preview-pr-304-36986ac9"; +export const WEBKIT_VERSION = "722f2a8a1a3da159a89b35730c5460a6ef58f0af"; /** * WebKit (JavaScriptCore) — the JS engine.