WebKit: skip eager timezone prewarm in VM::VM (1.3.14 startup regression) - #35258
WebKit: skip eager timezone prewarm in VM::VM (1.3.14 startup regression)#35258robobun wants to merge 3 commits into
Conversation
Picks up oven-sh/WebKit#322, which gates the eager IANA-timezone-table and display-name prewarm at the end of VM::VM behind USE(BUN_JSC_ADDITIONS). Upstream added the prewarm in 311982@main so the cost lands at process start; for a short-lived CLI process that tradeoff is backwards, and on Linux hosts where /etc/localtime is a regular file (Amazon Linux, many container images) the display-name warm drags in ICU's searchForTZFile zoneinfo walk. hyperfine -N, 400 runs, pinned core, release build with local WebKit: /etc/localtime symlink: 14.6ms -> 8.6ms (1.71x) /etc/localtime regular: 16.3ms -> 8.4ms (1.95x) openat syscalls on a regular-file /etc/localtime host go 447 -> 31 for bun -e 'console.log(1)'. Adds a Worker-race test to intl.test.ts covering the now-lazy init: in a fresh process, 8 Workers plus the main thread all hit Date / Intl first and must observe the same resolved zone, supportedValuesOf count, and Date.prototype.toString output.
WalkthroughChangesWebKit and Intl timezone behavior
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@test/js/web/intl/intl.test.ts`:
- Around line 326-331: Synchronize the initial timezone access in the worker
setup so every Worker signals readiness before any call to probe() occurs. Wait
for all worker-ready messages, release the workers through a shared barrier,
then run the main-thread probe concurrently with the workers’ probe calls;
preserve result collection and worker cleanup.
🪄 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: 6e134019-3750-4067-9477-4a6ea8eaec27
📒 Files selected for processing (2)
scripts/build/deps/webkit.tstest/js/web/intl/intl.test.ts
|
Updated 7:37 AM PT - Jul 23rd, 2026
❌ @robobun, your commit 3a21ff1 has 1 failures in
🧪 To try this PR locally: bunx bun-pr 35258That installs a local version of the PR into your bun-35258 --bun |
|
This PR may be a duplicate of:
🤖 Generated with Claude Code |
|
Not a duplicate; the two PRs target adjacent bugs and #32765's core fix is already obsolete at the current #32765 (June) worked around This PR fixes the newer regression from upstream 311982@main: Applying #32765's |
… lazy-init test spawn.test.ts: with the eager timezone prewarm removed from VM::VM the child bun -e process starts ~6ms sooner, so the first pipe-buffer flush of the 16MB write can observe the closed read end and reject the write() promise as well as end(). Attach a .catch() to the unawaited write() so that path cannot surface as an unhandled rejection; end() remains the assertion point. intl.test.ts (lazy-init Worker race): drop the unused threads[] array, race the barrier poll against the Worker result promises so a startup error surfaces instead of spinning, and pin LANG/LC_ALL so the Date.prototype.toString long-name assertion is hermetic on non-English hosts.
There was a problem hiding this comment.
All three nits from the previous pass are addressed in 3a21ff1 (LANG/LC_ALL pinned, dead threads array removed, barrier poll now races against Promise.all(results)), and the spawn.test.ts EPIPE tweak is a sensible consequence of faster child startup. Nothing further from my side — deferring to a maintainer for the WebKit bump itself, since the actual behavior change lives in oven-sh/WebKit#322 and there's a stated WEBKIT_VERSION conflict with #35193 to coordinate.
What was reviewed:
- Worker barrier: verified main thread's
probe()now runs strictly afterawait Promise.all(results), so it cannot warm the process-globalcall_onceahead of the 8 Workers. Atomics.wait(gate, 1, 0)in a Workeronmessage— Workers permit blockingAtomics.wait, so noTypeErrorhere.- spawn.test.ts:
write()return is a thenable in Bun's FileSink, so the.catchguard is live; the unawaited write still leaves buffered data forend()to reject on.
Extended reasoning...
Overview
Three files: scripts/build/deps/webkit.ts bumps WEBKIT_VERSION to pick up oven-sh/WebKit#322 (gating the eager IANA-timezone / display-name prewarm in VM::VM behind USE(BUN_JSC_ADDITIONS)); test/js/web/intl/intl.test.ts adds a fresh-process Worker-race test that barriers 8 Workers on a SharedArrayBuffer and asserts they all agree on resolvedOptions().timeZone / supportedValuesOf("timeZone").length / new Date(0).toString(); test/js/bun/spawn/spawn.test.ts swallows a possible EPIPE rejection from the unawaited write() in the existing EPIPE test, since faster child startup (no more ~6 ms tz prewarm) can now let the child exit before the first flush.
Security risks
None. No auth, crypto, permissions, or untrusted-input parsing touched. The WebKit-side change removes work at startup rather than adding a new code path; the caches it defers are already std::call_once / per-VM-under-JSLock and were already lazy under isInMiniMode().
Level of scrutiny
High — this is a WebKit prebuilt bump, and WebKit is the JS engine. The Bun-side diff is small and mechanical (a sha + comment, one new test, one test robustness tweak), but the behavioral change it pulls in is not reviewable from this repo. The PR description is thorough (strace/hyperfine A/B, orderfile trace, safety argument), and the new test does exercise the concurrent lazy-init path it claims to after the barrier rework, but a maintainer should still sign off on engine bumps.
Other factors
- All prior review feedback (CodeRabbit's barrier concern, my three nits on locale hermeticity / dead code / error-path spinning) is addressed in 00d5cf6 and 3a21ff1; I re-read the final diff against each and they're all applied.
- The author explicitly notes a
WEBKIT_VERSIONconflict with #35193 (preview build of oven-sh/WebKit#316) — merge order needs a human decision. - CI build #78617 was still running at last timeline update; the PR body's "Verified" section reports local passes on intl/temporal/v8-date/TZ/cron plus zero zoneinfo opens under strace, but CI green across all platforms is the real gate for a WebKit bump.
|
CI on 3a21ff1 (build #78617) finished: 195/196 jobs passed. The one red lane is All other annotation entries are
|
The WebKit bump pulls in oven-sh/WebKit#322 (skip the VM timezone prewarm), which makes the spawned child start faster and widens the window where it has already exited before the first stdin.write() flush. When that happens the unawaited write() rejects with EPIPE as an unhandled rejection that surfaces in the next test. Same fix as #35258, which also carries this WebKit commit.
## Problem Every Bun process runs libstdc++'s iostream and locale static initializers before `main`. From the release `bun-profile`: ``` _GLOBAL__sub_I.00090_globals_io.cc _GLOBAL__sub_I_cxx11_locale_inst.cc _GLOBAL__sub_I_cxx11_wlocale_inst.cc _GLOBAL__sub_I_locale_inst.cc _GLOBAL__sub_I_wlocale_inst.cc ``` `std::ios_base::Init`, `std::locale::_S_initialize`, `std::locale::_Impl`, and construction of ctype/numpunct/moneypunct/timepunct/messages for both `char` and `wchar_t`: roughly fifty functions out of libstdc++ that sit ahead of `main` in the `bun run orderfile` trace. Bun never uses C++ iostreams. ## Cause On libstdc++, `<iostream>` (and only `<iostream>`; not `<ostream>`/`<istream>`/`<sstream>`) emits an undefined reference to `_ZSt21ios_base_library_initv` in every TU that includes it. One such reference anywhere in the link pulls `globals_io.o` from libstdc++.a, whose initializer constructs `cin`/`cout`/`cerr`/`clog` and their `wchar_t` siblings, which in turn reference the full locale facet set. There are two sources: 1. **WebKit's vendored simdutf** (`Source/WTF/wtf/simdutf/simdutf_impl.h:9949`, an unused include in the `scalar/base64.h` section). Because `SIMDUTF.h` is included from `src/jsc/bindings/helpers.h`, ~70 Bun TUs carry the reference. Fixed in oven-sh/WebKit#320; upstream simdutf already dropped it in simdutf/simdutf#962. 2. **bun-uws headers**: `AsyncSocket.h`, `HttpRouter.h`, `Loop.h` include it without using it; `App.h`, `HttpContext.h`, `TopicTree.h` write fixed error strings via `std::cerr`. 7 Bun TUs carry the reference via `<bun-uws/src/App.h>`. ## Changes - `scripts/build/deps/webkit.ts`: bump `WEBKIT_VERSION` to `af2e8dc639` (oven-sh/WebKit#320: drop the simdutf `<iostream>` include and ban it at compile time for non-Debug `USE_BUN_JSC_ADDITIONS` builds; also picks up oven-sh/WebKit#321 lazy WebAssembly namespace and oven-sh/WebKit#322 skip eager timezone prewarm). Subsumes the `WEBKIT_VERSION` change in #35258. - `packages/bun-uws`: drop the `<iostream>` include from `AsyncSocket.h`, `HttpContext.h`, `HttpRouter.h`, `Loop.h`, `TopicTree.h`; delete the five `std::cerr << ...` validation messages (Bun validates these inputs before calling into uWS, so the paths are unreachable programmer errors and `std::terminate()` alone is enough). - `src/banned-includes/iostream`: a `#error` shim prepended to the `-I` search path for release builds. Any `#include <iostream>` in a Bun TU (including transitively from a WebKit header) fails the release compile with an explanation pointing here. Debug builds keep the real header for ad-hoc printf debugging; `-DBUN_ALLOW_IOSTREAM` is the opt-out. - `test/internal/source-lints/no-iostream-include.test.ts`: scans `src/`, `packages/bun-uws`, `packages/bun-usockets` for the include so debug CI also catches it. ## Verification With both oven-sh/WebKit#320 and this change applied to a release build: ``` nm build/release/bun-profile | grep ios_base4Init -> empty nm build/release/bun-profile | grep _S_initialize -> empty _GLOBAL__sub_I.00090_globals_io.cc gone bun-profile 547 KB smaller ``` Four `_GLOBAL__sub_I_*locale_inst.cc` stubs remain: these are the libstdc++ facet-id guard-byte initializers (a few dozen `movb $1,(%rax)` each) pulled by `Int128.cpp`'s `std::ostringstream`. `<sstream>` does not emit the static `Init` object and those stubs are near-free. ## Int128.cpp `Source/WTF/wtf/Int128.cpp` includes `<sstream>` and `<ostream>`. Both are load-bearing (`UInt128ToFormattedString`'s `std::ostringstream` and the `operator<<(std::ostream&, ...)` overloads) and neither emits the static `ios_base::Init` object, so they are left alone. <!-- robobun:evidence:begin --> --- **[decide:webkit]** gate passed · iteration 6 · 10 files touched <details><summary>fails on main (without fix)</summary> ```console ASAN without fix: 1 FAILED $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/no-iostream-include.test.ts bun test v1.4.0 (eba02a1) test/internal/source-lints/no-iostream-include.test.ts: 45 | // root going away, which would make the ban below pass vacuously. 46 | expect(scanned).toBeGreaterThan(0); 47 | } 48 | 49 | violations.sort(); 50 | expect(violations).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "packages/bun-uws/src/AsyncSocket.h", + "packages/bun-uws/src/HttpContext.h", + "packages/bun-uws/src/HttpRouter.h", + "packages/bun-uws/src/Loop.h", + "packages/bun-uws/src/TopicTree.h", + ] - Expected - 1 + Received + 7 at <anonymous> (/workspace/bun/test/internal/source-lints/no-iostream-include.test.ts:50:22) (fail) C++ sources compiled into Bun do not include <iostream> [748.19ms] 0 pass 1 fail 4 expect() calls Ran 1 test across 1 file. [3.00s] error: script "bd" exited with code 1 __F:1:S:0 release without fix: 1 FAILED bun test v1.4.0-canary.1 (507ab81) test/internal/source-lints/no-iostream-include.test.ts: 45 | // root going away, which would make the ban below pass vacuously. 46 | expect(scanned).toBeGreaterThan(0); 47 | } 48 | 49 | violations.sort(); 50 | expect(violations).toEqual([]); ^ error: expect(received).toEqual(expected) - [] + [ + "packages/bun-uws/src/AsyncSocket.h", + "packages/bun-uws/src/HttpContext.h", + "packages/bun-uws/src/HttpRouter.h", + "packages/bun-uws/src/Loop.h", + "packages/bun-uws/src/TopicTree.h", + ] - Expected - 1 + Received + 7 at <anonymous> (/workspace/bun/test/internal/source-lints/no-iostream-include.test.ts:50:22) (fail) C++ sources compiled into Bun do not include <iostream> [59.52ms] 0 pass 1 fail 4 expect() calls Ran 1 test across 1 file. [213.00ms] __F:1:S:0 ``` </details> <details><summary>passes on PR (with fix)</summary> ```console ASAN with fix: all passed $ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/mechgate.xml" test/internal/source-lints/no-iostream-include.test.ts bun test v1.4.0 (eba02a1) test/internal/source-lints/no-iostream-include.test.ts: (pass) C++ sources compiled into Bun do not include <iostream> [885.41ms] 1 pass 0 fail 4 expect() calls Ran 1 test across 1 file. [3.41s] __F:0:S:0 release with fix: all passed $ bun scripts/build.ts --profile=release [configured] bun-profile → bun (stripped) in 1174ms (unchanged) ninja: Entering directory `/workspace/bun/build/release' [0/1] reconfigure [0/13] cargo bun_bin → libbun_rust.a (--target x86_64-unknown-linux-gnu) nightly-2026-07-20-x86_64-unknown-linux-gnu unchanged - rustc 1.99.0-nightly (9f36de775 2026-07-19) �[1m�[92m Compiling�[0m bun_core v0.0.0 (/workspace/bun/src/bun_core) �[1m�[92m Compiling�[0m bun_errno v0.0.0 (/workspace/bun/src/errno) �[1m�[92m Compiling�[0m bun_ptr v0.0.0 (/workspace/bun/src/ptr) �[1m�[92m Compiling�[0m bun_boringssl_sys v0.0.0 (/workspace/bun/src/boringssl_sys) �[1m�[92m Compiling�[0m bun_safety v0.0.0 (/workspace/bun/src/safety) �[1m�[92m Compiling�[0m bun_zlib_sys v0.0.0 (/workspace/bun/src/zlib_sys) �[1m�[92m Compiling�[0m bun_cares_sys v0.0.0 (/workspace/bun/src/cares_sys) �[1m�[92m Compiling�[0m bun_zstd v0.0.0 (/workspace/bun/src/zstd) �[1m�[92m Compiling�[0m bun_picohttp v0.0.0 (/workspace/bun/src/picohttp) �[1m�[92m Compiling�[0m bun_output v0.0.0 (/workspace/bun/src/output) �[1m�[92m Compiling�[0m bun_clap v0.0.0 (/workspace/bun/src/clap) �[1m�[92m Compili ... (truncated) ``` </details> <details><summary>diff hotspot</summary> ``` packages/bun-uws/src/App.h | 3 -- packages/bun-uws/src/AsyncSocket.h | 1 - packages/bun-uws/src/HttpContext.h | 3 -- packages/bun-uws/src/HttpRouter.h | 1 - packages/bun-uws/src/Loop.h | 1 - packages/bun-uws/src/TopicTree.h | 5 --- scripts/build/deps/webkit.ts | 7 --- scripts/build/flags.ts | 6 +++ src/banned-includes/iostream | 27 ++++++++++++ .../source-lints/no-iostream-include.test.ts | 51 ++++++++++++++++++++++ 10 files changed, 84 insertions(+), 21 deletions(-) ``` </details> **gate history** · 8 passed · 1 rejected · iteration 6 <details><summary>evidence per changed file</summary> ``` file reads edits tests packages/bun-uws/src/App.h 4 3 0 packages/bun-uws/src/AsyncSocket.h 1 1 0 packages/bun-uws/src/HttpContext.h 2 2 0 packages/bun-uws/src/HttpRouter.h 1 1 0 packages/bun-uws/src/Loop.h 1 1 0 packages/bun-uws/src/TopicTree.h 4 4 0 scripts/build/deps/webkit.ts 2 3 0 scripts/build/flags.ts 1 1 0 src/banned-includes/iostream 0 1 0 test/internal/source-lints/no-iostream-include.test.ts 2 3 0 ``` </details> <!-- robobun:evidence:end --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
Picks up oven-sh/WebKit#322, which gates the eager IANA-timezone-table and display-name prewarm at the end of
VM::VMbehindUSE(BUN_JSC_ADDITIONS).Regression
Between 1.3.13 and 1.3.14, upstream WebKit 311982@main added this prewarm so the cost lands at process start rather than on a later critical path. That is the right tradeoff for a long-lived browser; for a short-lived CLI process it is backwards, and on Linux the
timeZoneDisplayNamewarm pulls inucal_getHostTimeZone→detectHostTimeZone→uprv_tzname, which walks/usr/share/zoneinfo/**byte-comparing every file against/etc/localtimewhen/etc/localtimeis a regular file rather than a symlink (Amazon Linux, many container base images).strace on a full-tzdata Linux host with a regular-file
/etc/localtime:hyperfine on the same host, pinned core:
TZunsetTZ=UTCWhat the block costs
Direct instrumentation around the block (release build with local WebKit, Linux x64,
/etc/localtimesymlink):hyperfine A/B, 400 runs, pinned core, same binary:
/etc/localtimebun -e 'console.log(1)'bun -e 'new Date().toString()'bun -e 'console.log(1)'openatsyscalls forbun -e 'console.log(1)'on a regular-file host: 447 → 31 (zoneinfo opens 414 → 0). The block accounts for ~12% of unique function entries in the orderfile trace ofbun -e 'console.log(1)'(entries 1253–1861 of ~4983).Safety
Both caches are
std::call_once/ per-VM-under-JSLock, so first access fromDate/Intlfills them on demand. This is the same pathisInMiniMode()already takes. Adds a Worker-race test tointl.test.ts: in a fresh process, 8 Workers plus the main thread concurrently hitDate.toString()/Intl.supportedValuesOf("timeZone")/Intl.DateTimeFormat().resolvedOptions()and must all agree.Verified
bun bd teston the new prebuilt:intl.test.ts(32/32),temporal-global.test.ts,v8-date-parser.test.js,process.env.TZ,cron.test.tsall pass. strace on the debug build confirms zero zoneinfo opens forbun -e 'console.log(1)'.Note
Conflicts on
WEBKIT_VERSIONwith #35193, which bumps to a preview build of oven-sh/WebKit#316 (not yet on main). Whichever merges second can rebase; once oven-sh/WebKit#316 lands the combined bump is one sha.no test proof · iteration 1 · Platform-specific test-only change; deferring to CI.