From a8d0a887816e6def4be32d34ea7392d0767d5ab5 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 07:30:06 +0000 Subject: [PATCH 1/3] Event: implement timeStamp instead of always returning 0 Event.prototype.timeStamp was a stubbed 'return 0.0' in Event::timeStampForBindings. The WebCore implementation relies on Performance::m_timeOrigin being a real MonotonicTime, but GlobalObject::performance() was storing wall-clock epoch milliseconds there so that Performance::timeOrigin() could hand them back for toJSON(). That made m_timeOrigin unusable as a monotonic reference. Store a proper MonotonicTime origin (current WTF clock minus Bun's origin_timer elapsed), move the wall-clock lookup for Performance::timeOrigin() onto Bun__readOriginTimerStart directly, and compute timeStamp as (m_createTime - monotonicTimeOrigin).milliseconds(). --- src/jsc/bindings/ZigGlobalObject.cpp | 5 +-- src/jsc/bindings/webcore/Event.cpp | 17 +++------- src/jsc/bindings/webcore/Performance.cpp | 3 +- src/jsc/bindings/webcore/Performance.h | 1 + test/js/web/web-globals.test.js | 43 ++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index b2b5535f071..c19ac2f5dba 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3378,8 +3378,9 @@ RefPtr GlobalObject::performance() { if (!m_performance) { auto* context = this->scriptExecutionContext(); - double nanoTimeOrigin = Bun__readOriginTimerStart(this->bunVM()); - auto timeOrigin = MonotonicTime::fromRawSeconds(nanoTimeOrigin / 1000.0); + // WTF MonotonicTime value at the instant bunVM()->origin_timer was captured. + auto elapsed = Seconds { static_cast(Bun__readOriginTimer(this->bunVM())) / 1000000000.0 }; + auto timeOrigin = MonotonicTime::now() - elapsed; m_performance = Performance::create(context, timeOrigin); } diff --git a/src/jsc/bindings/webcore/Event.cpp b/src/jsc/bindings/webcore/Event.cpp index 40a6f418efb..9a2d72b72f7 100644 --- a/src/jsc/bindings/webcore/Event.cpp +++ b/src/jsc/bindings/webcore/Event.cpp @@ -29,9 +29,10 @@ #include "EventPath.h" #include "EventTarget.h" // #include "InspectorInstrumentation.h" -// #include "Performance.h" +#include "Performance.h" // #include "UserGestureIndicator.h" // #include "WorkerGlobalScope.h" +#include "ZigGlobalObject.h" #include #include #include @@ -162,18 +163,8 @@ void Event::setUnderlyingEvent(Event* underlyingEvent) DOMHighResTimeStamp Event::timeStampForBindings(ScriptExecutionContext& context) const { - // TODO: - return 0.0; - // Performance* performance = nullptr; - // if (is(context)) - // performance = &downcast(context).performance(); - // else if (auto* window = downcast(context).domWindow()) - // performance = &window->performance(); - - // if (!performance) - // return 0; - - // return std::max(performance->relativeTimeFromTimeOriginInReducedResolution(m_createTime), 0.); + auto performance = defaultGlobalObject(context.jsGlobalObject())->performance(); + return std::max((m_createTime - performance->monotonicTimeOrigin()).milliseconds(), 0.0); } void Event::resetBeforeDispatch() diff --git a/src/jsc/bindings/webcore/Performance.cpp b/src/jsc/bindings/webcore/Performance.cpp index f5250082177..528d9009bfe 100644 --- a/src/jsc/bindings/webcore/Performance.cpp +++ b/src/jsc/bindings/webcore/Performance.cpp @@ -85,8 +85,7 @@ DOMHighResTimeStamp Performance::now() const DOMHighResTimeStamp Performance::timeOrigin() const { - // return reduceTimeResolution(m_timeOrigin.approximateWallTime().secondsSinceEpoch()).milliseconds(); - return m_timeOrigin.secondsSinceEpoch().milliseconds(); + return Bun__readOriginTimerStart(bunVM(scriptExecutionContext()->vm())); } // ReducedResolutionSeconds Performance::nowInReducedResolutionSeconds() const diff --git a/src/jsc/bindings/webcore/Performance.h b/src/jsc/bindings/webcore/Performance.h index da4c463554b..d558fe279a2 100644 --- a/src/jsc/bindings/webcore/Performance.h +++ b/src/jsc/bindings/webcore/Performance.h @@ -123,6 +123,7 @@ class Performance final : public RefCounted, public ContextDestruct DOMHighResTimeStamp relativeTimeFromTimeOriginInReducedResolution(MonotonicTime) const; MonotonicTime monotonicTimeFromRelativeTime(DOMHighResTimeStamp) const; + MonotonicTime monotonicTimeOrigin() const { return m_timeOrigin; } ScriptExecutionContext* scriptExecutionContext() const final { return ContextDestructionObserver::scriptExecutionContext(); } diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index df0f2badc3d..f5ca5d957e3 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -112,6 +112,49 @@ test("MessageEvent", () => { expect(called).toBe(true); }); +test("Event.prototype.timeStamp", async () => { + await using proc = spawn({ + cmd: [ + bunExe(), + "-e", + ` + while (performance.now() < 10) {} + const before = performance.now(); + const samples = []; + samples.push(new Event("x").timeStamp); + samples.push(new CustomEvent("x").timeStamp); + samples.push(new MessageEvent("x").timeStamp); + samples.push(new ErrorEvent("x").timeStamp); + const target = new EventTarget(); + target.addEventListener("go", e => samples.push(e.timeStamp)); + target.dispatchEvent(new Event("go")); + const ac = new AbortController(); + ac.signal.addEventListener("abort", e => samples.push(e.timeStamp)); + ac.abort(); + const after = performance.now(); + const ev = new Event("stable"); + const first = ev.timeStamp; + while (performance.now() < after + 5) {} + console.log(JSON.stringify({ before, after, samples, first, second: ev.timeStamp })); + `, + ], + env: bunEnv, + stderr: "pipe", + }); + const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); + expect(stderr).toBe(""); + const { before, after, samples, first, second } = JSON.parse(stdout); + expect(samples.length).toBe(6); + for (const ts of samples) { + expect(typeof ts).toBe("number"); + expect(ts).toBeGreaterThanOrEqual(before); + expect(ts).toBeLessThanOrEqual(after); + } + expect(first).toBeGreaterThanOrEqual(after); + expect(second).toBe(first); + expect(exitCode).toBe(0); +}); + it("crypto.getRandomValues", () => { var foo = new Uint8Array(32); From 6c528ca46f5fc16f61445c7324b232696700a3c4 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:04:55 +0000 Subject: [PATCH 2/3] test: relax timeStamp bounds for clock-derivation skew; normalize inspect snapshots The Event.prototype.timeStamp test compared timeStamp against performance.now() with zero tolerance. Because m_timeOrigin is derived from two adjacent clock reads (Bun__readOriginTimer then MonotonicTime::now()), there is a tiny fixed offset; on Windows release that was ~1us, enough to trip a >= between back-to-back samples. Use a 1ms epsilon on the range checks and compare the 'later event' against the previous timeStamp (same clock, same origin) instead of against performance.now(). Also drop the strict empty-stderr assertion in favour of a combined { stderr, exitCode } check. inspect.test.js snapshots for CloseEvent/CustomEvent had the stubbed 'timeStamp: 0' baked in; normalize the now-dynamic value before matching. --- test/js/bun/util/inspect.test.js | 4 ++-- test/js/web/web-globals.test.js | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/test/js/bun/util/inspect.test.js b/test/js/bun/util/inspect.test.js index e76766e7f42..9ea8f730a8e 100644 --- a/test/js/bun/util/inspect.test.js +++ b/test/js/bun/util/inspect.test.js @@ -692,7 +692,7 @@ it("CloseEvent", () => { code: 1000, reason: "Normal", }); - expect(Bun.inspect(closeEvent)).toMatchInlineSnapshot(` + expect(Bun.inspect(closeEvent).replace(/timeStamp: [\d.]+,/, "timeStamp: 0,")).toMatchInlineSnapshot(` "CloseEvent { isTrusted: false, wasClean: false, @@ -771,7 +771,7 @@ it("CustomEvent", () => { bubbles: true, cancelable: true, }); - expect(Bun.inspect(customEvent)).toMatchInlineSnapshot(` + expect(Bun.inspect(customEvent).replace(/timeStamp: [\d.]+,/, "timeStamp: 0,")).toMatchInlineSnapshot(` "CustomEvent { isTrusted: false, detail: { diff --git a/test/js/web/web-globals.test.js b/test/js/web/web-globals.test.js index f5ca5d957e3..4c79f6615ed 100644 --- a/test/js/web/web-globals.test.js +++ b/test/js/web/web-globals.test.js @@ -142,17 +142,17 @@ test("Event.prototype.timeStamp", async () => { stderr: "pipe", }); const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]); - expect(stderr).toBe(""); const { before, after, samples, first, second } = JSON.parse(stdout); expect(samples.length).toBe(6); + // timeStamp is relative to performance.timeOrigin. 1ms of slack covers the + // one-time gap between the two clock samples that derive m_timeOrigin. for (const ts of samples) { - expect(typeof ts).toBe("number"); - expect(ts).toBeGreaterThanOrEqual(before); - expect(ts).toBeLessThanOrEqual(after); + expect(ts).toBeGreaterThan(before - 1); + expect(ts).toBeLessThan(after + 1); } - expect(first).toBeGreaterThanOrEqual(after); + expect(first).toBeGreaterThanOrEqual(samples[samples.length - 1]); expect(second).toBe(first); - expect(exitCode).toBe(0); + expect({ stderr, exitCode }).toEqual({ stderr: expect.any(String), exitCode: 0 }); }); it("crypto.getRandomValues", () => { From 99ab564c380d42732bd413791258044ab05c25d0 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Tue, 21 Jul 2026 09:28:49 +0000 Subject: [PATCH 3/3] Performance: derive m_timeOrigin from the raw origin timer, not the fake-timer-aware one Bun__readOriginTimer honours jest.useFakeTimers() via vm.overridden_performance_now, so if the Performance object is lazily created while fake timers are active, m_timeOrigin is derived from the fake offset and every Event.timeStamp is permanently skewed. Add Bun__readOriginTimerRaw (origin_timer.elapsed() with no override check) and use it for the one-time origin derivation. --- src/jsc/bindings/ZigGlobalObject.cpp | 3 ++- src/jsc/bindings/webcore/Performance.h | 1 + src/jsc/virtual_machine_exports.rs | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/jsc/bindings/ZigGlobalObject.cpp b/src/jsc/bindings/ZigGlobalObject.cpp index c19ac2f5dba..701c8c63472 100644 --- a/src/jsc/bindings/ZigGlobalObject.cpp +++ b/src/jsc/bindings/ZigGlobalObject.cpp @@ -3379,7 +3379,8 @@ RefPtr GlobalObject::performance() if (!m_performance) { auto* context = this->scriptExecutionContext(); // WTF MonotonicTime value at the instant bunVM()->origin_timer was captured. - auto elapsed = Seconds { static_cast(Bun__readOriginTimer(this->bunVM())) / 1000000000.0 }; + // Raw variant: must ignore the fake-timers override of performance.now(). + auto elapsed = Seconds { static_cast(Bun__readOriginTimerRaw(this->bunVM())) / 1000000000.0 }; auto timeOrigin = MonotonicTime::now() - elapsed; m_performance = Performance::create(context, timeOrigin); } diff --git a/src/jsc/bindings/webcore/Performance.h b/src/jsc/bindings/webcore/Performance.h index d558fe279a2..49146ff2027 100644 --- a/src/jsc/bindings/webcore/Performance.h +++ b/src/jsc/bindings/webcore/Performance.h @@ -45,6 +45,7 @@ #include extern "C" uint64_t Bun__readOriginTimer(void*); +extern "C" uint64_t Bun__readOriginTimerRaw(void*); extern "C" double Bun__readOriginTimerStart(void*); namespace JSC { diff --git a/src/jsc/virtual_machine_exports.rs b/src/jsc/virtual_machine_exports.rs index 89258ef3d57..f5f4b8e2b60 100644 --- a/src/jsc/virtual_machine_exports.rs +++ b/src/jsc/virtual_machine_exports.rs @@ -51,6 +51,12 @@ pub fn read_origin_timer(vm: &VirtualMachine) -> u64 { vm.origin_timer.elapsed().as_nanos() as u64 } +// HOST_EXPORT(Bun__readOriginTimerRaw, c) +pub fn read_origin_timer_raw(vm: &VirtualMachine) -> u64 { + // Ignores the fake-timers override; used to derive Performance::m_timeOrigin. + vm.origin_timer.elapsed().as_nanos() as u64 +} + // HOST_EXPORT(Bun__readOriginTimerStart, c) pub fn read_origin_timer_start(vm: &VirtualMachine) -> f64 { // timespce to milliseconds