Skip to content

Event: implement timeStamp instead of always returning 0#34894

Open
robobun wants to merge 3 commits into
mainfrom
farm/d9be4262/event-timestamp
Open

Event: implement timeStamp instead of always returning 0#34894
robobun wants to merge 3 commits into
mainfrom
farm/d9be4262/event-timestamp

Conversation

@robobun

@robobun robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

Event.prototype.timeStamp returned 0 for every event (new Event, CustomEvent, MessageEvent, ErrorEvent, dispatched listener events, AbortSignal abort events), regardless of how long the process had been running. Node and browsers return a DOMHighResTimeStamp on the performance.now() scale; WPT dom/events/Event-constructors.any.js asserts ev.timeStamp > 0.

await new Promise(r => setTimeout(r, 100));
console.log(new Event("x").timeStamp);          // bun: 0 | node: ~100.x
const ac = new AbortController();
ac.signal.addEventListener("abort", e => console.log(e.timeStamp)); // bun: 0
ac.abort();
console.log(performance.now());                 // bun: ~100 (clock itself is fine)

Cause

Event::timeStampForBindings in src/jsc/bindings/webcore/Event.cpp was a literal return 0.0; // TODO:. The WebCore path it is meant to use (m_createTime - Performance::m_timeOrigin) was also unusable because GlobalObject::performance() stored wall-clock epoch milliseconds in m_timeOrigin via MonotonicTime::fromRawSeconds, purely so Performance::timeOrigin() could hand them back for performance.toJSON(). Subtracting that from a real MonotonicTime produces garbage.

Fix

  • GlobalObject::performance() now derives a real MonotonicTime origin: MonotonicTime::now() - Seconds(Bun__readOriginTimer(bunVM) / 1e9), i.e. the WTF clock value at the instant origin_timer was captured.
  • Performance::timeOrigin() reads Bun__readOriginTimerStart directly for the wall-clock value (matching the performance.timeOrigin own-property path in JSPerformance::finishCreation).
  • Event::timeStampForBindings returns (m_createTime - performance->monotonicTimeOrigin()).milliseconds(), clamped to 0. Both operands are fixed once set, so repeated reads return the same value.

Verification

New test in test/js/web/web-globals.test.js spawns a subprocess, captures performance.now() before and after constructing six event variants, and asserts each timeStamp falls within [before, after], plus a stability check that two reads of the same event's timeStamp are identical. Fails on main (Received: 0), passes with this change.


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

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().
@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 4:48 AM PT - Jul 21st, 2026

@robobun, your commit 99ab564 has 1 failures in Build #76789 (All Failures):

  • 📦 Binary size — 8 over 0.50 MB
  • targetthis build canary: main #76791
    sizeΔ
    bun-darwin-aarch6458.31 MB57.56 MB+759.6 KB
    bun-darwin-x6464.72 MB62.95 MB+1.77 MB
    bun-linux-aarch6470.30 MB70.74 MB-449.0 KB
    bun-linux-x6473.73 MB72.28 MB+1.45 MB
    bun-linux-x64-baseline72.67 MB
    bun-linux-aarch64-musl63.73 MB64.26 MB-537.4 KB
    bun-linux-x64-musl67.72 MB66.38 MB+1.33 MB
    bun-linux-x64-musl-baseline66.97 MB
    bun-linux-aarch64-android78.57 MB78.09 MB+491.6 KB
    bun-linux-x64-android81.55 MB80.28 MB+1.28 MB
    bun-freebsd-x6484.32 MB82.54 MB+1.78 MB
    bun-freebsd-aarch6485.01 MB84.28 MB+750.9 KB
    bun-windows-x6476.39 MB79.67 MB-3.28 MB
    bun-windows-x64-baseline75.39 MB
    bun-windows-aarch6470.85 MB70.29 MB+577.5 KB

    Add [skip size check] to the commit message if this increase is intentional.


🧪   To try this PR locally:

bunx bun-pr 34894

That installs a local version of the PR into your bun-34894 executable, so you can run:

bun-34894 --bun

@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 86c20e6d-d288-4bf0-a3a8-908e04501e94

📥 Commits

Reviewing files that changed from the base of the PR and between 5b98630 and 99ab564.

📒 Files selected for processing (7)
  • src/jsc/bindings/ZigGlobalObject.cpp
  • src/jsc/bindings/webcore/Event.cpp
  • src/jsc/bindings/webcore/Performance.cpp
  • src/jsc/bindings/webcore/Performance.h
  • src/jsc/virtual_machine_exports.rs
  • test/js/bun/util/inspect.test.js
  • test/js/web/web-globals.test.js

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

@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 test-flake nits, I checked the other consumers of Performance::m_timeOrigin since its meaning changed from wall-clock-in-a-MonotonicTime-wrapper to a real MonotonicTime: Performance::timeOrigin() (used by performance.toJSON() at JSPerformance.cpp:408) was updated in the same commit and returns the identical value as before; addResourceTiming, relativeTimeFromTimeOriginInReducedResolution, and monotonicTimeFromRelativeTime have no live callers in Bun (or feed only commented-out inspector code), so no observable behavior regresses there.

Extended reasoning...

Verified the m_timeOrigin semantic change against every consumer in src/jsc/bindings: the only user-reachable path was Performance::timeOrigin()performance.toJSON(), which now reads Bun__readOriginTimerStart directly and is numerically identical to the old m_timeOrigin.secondsSinceEpoch().milliseconds() round-trip. The PerformanceResourceTiming/UserTiming paths that also read m_timeOrigin are dead (no caller of addResourceTiming; monotonicTimeFromRelativeTime only feeds commented-out InspectorInstrumentation). Not approving — native JSC-bindings change with two flagged test-quality nits and no local test proof; worth a human look.

Comment thread test/js/web/web-globals.test.js Outdated
Comment thread test/js/web/web-globals.test.js Outdated
…pect 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.
Comment thread src/jsc/bindings/ZigGlobalObject.cpp Outdated
…ake-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.

@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.

No new issues after 99ab564 — the three earlier findings are all addressed. This is a real behavioral change to Event.timeStamp plus a semantic change to Performance::m_timeOrigin, so leaving it for a human to sign off.

What was reviewed:

  • Bun__readOriginTimerRaw correctly bypasses vm.overridden_performance_now; Performance::now() still uses the fake-timer-aware variant.
  • Performance::timeOrigin()'s only caller is toJSON(), which now matches the own-property timeOrigin set in JSPerformance::finishCreation — previously they could diverge.
  • Other m_timeOrigin consumers (monotonicTimeFromRelativeTime in performance.mark, PerformanceResourceTiming::create) were already receiving a bogus wall-clock-as-monotonic value; this change makes them coherent rather than regressing them.
  • defaultGlobalObject(context.jsGlobalObject()) in timeStampForBindings resolves per-worker, so worker events use the worker VM's origin_timer.
Extended reasoning...

Overview

Implements Event.prototype.timeStamp (was hard-coded 0.0). To do so it changes what Performance::m_timeOrigin stores — from a wall-clock-epoch value smuggled through MonotonicTime::fromRawSeconds to an actual WTF MonotonicTime at VM-origin — and moves the wall-clock read for performance.timeOrigin/toJSON() to Bun__readOriginTimerStart directly. Adds Bun__readOriginTimerRaw so the one-time origin derivation cannot be poisoned by jest.useFakeTimers(). Test coverage in web-globals.test.js plus two inspect-snapshot normalizations.

Security risks

None. No untrusted input parsing, no auth/crypto, no allocation sizing. The only new surface is a timestamp read.

Level of scrutiny

Medium. The diff is small, but it redefines the meaning of a field (m_timeOrigin) that other WebCore code reads, and it interacts with fake timers, workers, and two independent monotonic clock sources. I checked each existing consumer of m_timeOrigin: monotonicTimeFromRelativeTime (only feeds a commented-out InspectorInstrumentation call), relativeTimeFromTimeOriginInReducedResolution (no live callers), and PerformanceResourceTiming::create (was already receiving garbage) — none regress. Performance::timeOrigin() uses the same scriptExecutionContext()->vm() pattern as the existing Performance::now() right above it.

Other factors

All three concerns I raised on earlier revisions (clock-derivation δ skew vs zero-tolerance assertions, expect(stderr).toBe(""), and fake-timer poisoning of the lazy m_timeOrigin init) have been addressed in 6c528ca and 99ab564. The 1 ms epsilon in the test is generous relative to the expected sub-µs gap between two adjacent clock reads but could in principle still be exceeded under extreme preemption on a debug+ASAN runner — worth a human glance but not something I'd block on. This is a user-visible Web API behavior change with WPT implications, which per the approval guidelines is not the kind of change to auto-approve.

@robobun

robobun commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator Author

CI status on 99ab564 (build #76789):

  • web-globals.test.js and inspect.test.js pass on all lanes; the Windows timing failure and the snapshot failures from the first run are gone.
  • The binary-size job is the only hard failure. It is comparing against canary main #76791 (e550f2c, ci: single arm64 debian-13 build host; ThinLTO everywhere; baseline-only x64; rust+link merge; sysroots; WebKit a36c188; rust 2026-07-20 #34782), which landed a ThinLTO/sysroot/WebKit/Rust-toolchain change after this branch was cut from 5b98630. The deltas range from -3.28 MB (windows-x64) to +1.78 MB (freebsd-x64); a 15-line C++/Rust change does not move the binary by megabytes in both directions. The comparison is across two different toolchains.
  • Remaining reds are single-lane [flaky] retries on net-mongodb-pattern-leak, webview-chrome, napi, all unrelated to this diff.

Happy to rebase onto e550f2c if you'd rather the size check run apples-to-apples.

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.

1 participant