Skip to content

Object.seal/freeze: take the JSObject fast path for arrays and indexed objects - #35478

Open
robobun wants to merge 1 commit into
mainfrom
farm/fbe5fb68/fast-seal-freeze-arrays
Open

Object.seal/freeze: take the JSObject fast path for arrays and indexed objects#35478
robobun wants to merge 1 commit into
mainfrom
farm/fbe5fb68/fast-seal-freeze-arrays

Conversation

@robobun

@robobun robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Problem

Object.seal / Object.freeze on a JSArray (or a plain object with indexed properties) always fall through to JSC's generic SetIntegrityLevel loop, which per element allocates an Identifier, dispatches through the method table, and runs the full defineOwnProperty descriptor validation. For the #6360 benchmark (2000 arrays, avg 1000 elements) that costs ~460ms for seal and ~530ms for freeze, vs ~3ms in Node/V8:

$ bun repro.js
[22.96ms] test-nonoptimized
[476.77ms] test-optimized   # the Object.seal path

Fix

oven-sh/WebKit#337 adds bulk seal() / freeze() to SparseArrayValueMap (mirroring PropertyTable::seal/freeze), teaches JSObject::seal/freeze to apply it to the sparse map after enterDictionaryIndexingMode, freezes JSArray "length" from the same place, and widens the objectConstructorSeal / objectConstructorFreeze fast-path gate from is<JSFinalObject> && !hasIndexedProperties to is<JSFinalObject> || isJSArray.

isJSArray matches only exact ArrayType, so DerivedArrayType subclasses and every receiver with overridden [[DefineOwnProperty]] / [[PreventExtensions]] (e.g. Proxy) still take the generic observable loop.

Results (x86_64, release, local WebKit build)

2000 iterations, new Array(i).fill(i):

before after node 26
Object.seal(arr) 462 ms 89 ms 3.6 ms
Object.freeze(arr) 530 ms 98 ms 3.2 ms
preventExtensions(arr) 102 ms 102 ms 4.3 ms
baseline (fill only) 5 ms 4 ms 3.9 ms

The remaining ~90ms is enterDictionaryIndexingMode itself (contiguous → ArrayStorage vector → SparseArrayValueMap); V8 keeps a contiguous sealed/frozen elements kind and so never pays for a sparse-map conversion. That is a larger change.

Verification

The WebKit PR carries JSTests/stress/object-seal-freeze-array-fast-path.js covering descriptor attributes on dense / holey / accessor-bearing / named-prop-bearing arrays and plain objects, length writability, seal-after-preventExtensions, freeze-after-seal, idempotence, strict-mode throw semantics, and that a Proxy target still observes the preventExtensions / ownKeys / defineProperty traps.

All 279 test262 tests under built-ins/Object/{seal,freeze,isSealed,isFrozen,preventExtensions} and the pre-existing JSTests/stress/*{seal,freeze,frozen}* pass.

There is no Bun-side test proof for this change: the diff is WEBKIT_VERSION only (the fix lives in JavaScriptCore) and the new fast path is semantically identical to the generic loop, so a correctness test passes with and without the fix. Before/after numbers above are from a local --webkit=local release build.

Also in this range (oven-sh/WebKit 54917009..e4f1d0d0)

Note

CI will fail until the autobuild-preview-pr-337-e4f1d0d0 artifacts finish building.

Fixes #6360.


no test proof · iteration 4 · build/CI scripts only; test-proof not applicable

@github-actions

Copy link
Copy Markdown
Contributor

Found 3 issues this PR may fix:

  1. Using bun:ffi with --inspect on record #22783 - Inspector throw scope fix (WebKit#328) addresses crash in InspectorBackendDispatchers dispatch with CatchScope context when using --inspect with bun:ffi
  2. String and JSON size limits #2570 - JSON LiteralParser OOM fix (WebKit#317) addresses JSON.parse silently truncating/crashing at ~2GB instead of throwing a proper RangeError
  3. Bun.file(path).json() is missing a max length check for UTF-16 strings #19276 - JSON LiteralParser OOM fix (WebKit#317) addresses Bun.file().json() crashing with IOT instruction on large (4GB) JSON files instead of throwing RangeError

If this is helpful, copy the block below into the PR description to auto-close these issues on merge.

Fixes #22783
Fixes #2570
Fixes #19276

🤖 Generated with Claude Code

@robobun

robobun commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 3:30 AM PT - Jul 25th, 2026

@robobun, your commit 119953e is still building in Build #80407, but has 1 failures so far (All Failures):

@robobun
robobun force-pushed the farm/fbe5fb68/fast-seal-freeze-arrays branch from ba6af41 to 5d1300e Compare July 25, 2026 00:11
@robobun
robobun marked this pull request as ready for review July 25, 2026 03:14
@coderabbitai

coderabbitai Bot commented Jul 25, 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: 24 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: bbd06646-6499-4261-a353-b0e2b354b0b2

📥 Commits

Reviewing files that changed from the base of the PR and between df6c7ee and 119953e.

📒 Files selected for processing (1)
  • scripts/build/deps/webkit.ts

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

@robobun

robobun commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Review on oven-sh/WebKit#337 caught a real regression: the new JSObject::freeze fast path bulk-marked sparse-map entries ReadOnly without calling notifyPresenceOfIndexedAccessors(vm), so a child of a frozen array/object prototype wrote through to its own indexed property instead of throwing. Fixed in WebKit e4f1d0d0 (fail-before / pass-after verified against jsc), with regression cases added to the stress test. Perf unchanged: seal ~90ms, freeze ~100ms on the #6360 benchmark.

autobuild-preview-pr-337-e4f1d0d0 is published (all 36 WebKit CI lanes green). Earlier bun CI runs (79756, 79861, 80223) 404'd on the WebKit fetch because they raced that upload.

Build 80407 on the rebased 119953e974: linux-x64 build + debian/ubuntu test lanes pass; the red lanes are build-cpp jobs that expired in the runner queue (darwin-x64, freebsd-aarch64) and their sibling build-bun jobs that then fail waiting on them. None of it touches this diff, which is the one WEBKIT_VERSION line.

Merge order: oven-sh/WebKit#337 first, then this PR's WEBKIT_VERSION moves from the autobuild-preview-pr-337-* tag to the merged main sha.

@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 issues with the one-line diff itself — prebuiltUrl() and prebuiltDestDir() both already branch on the autobuild- prefix, so the preview tag resolves correctly. Deferring because this is a JSC engine behavior change (Object.seal/freeze fast path + two other WebKit PRs in range) riding on an autobuild-preview-* tag that, per the merge-order note above, still needs to be swapped for the merged oven-sh/WebKit main SHA before this lands. That coordination step and the upstream engine change itself want a human sign-off.

Checked: the version-string handling in webkit.ts for non-40-hex tags; that no other file in the repo hardcodes the old WebKit SHA.

Extended reasoning...

Overview

The Bun-side diff is a single line: WEBKIT_VERSION in scripts/build/deps/webkit.ts moves from commit 54917009… to the preview tag autobuild-preview-pr-337-c00301cf. The substantive change lives in oven-sh/WebKit#337 (bulk seal/freeze on SparseArrayValueMap, widened fast-path gate in objectConstructorSeal/Freeze), plus two other WebKit PRs (#328 inspector throw-scope, #317 LiteralParser OOM) that fall in the bumped range.

Security risks

None introduced by the Bun diff. The upstream change touches JS-observable engine semantics for Object.seal/Object.freeze on arrays and indexed objects; the PR description reports test262 (built-ins/Object/{seal,freeze,isSealed,isFrozen,preventExtensions}) and JSTests stress coverage pass, and that Proxy/DerivedArray still take the observable generic path.

Level of scrutiny

Mechanically this is a version bump, but effectively it is a JavaScriptCore engine change to core language builtins — the class of change the repo's own guidance ("Dependencies & vendoring") flags for human attention. It also carries an explicit process dependency: robobun's own comment states oven-sh/WebKit#337 must merge first and then WEBKIT_VERSION here must be updated from the preview tag to the merged main SHA. As written, the PR is intentionally not in its final merge state.

Other factors

I confirmed the build script already handles non-SHA version strings: prebuiltUrl() skips re-prefixing when the value already starts with autobuild-, and prebuiltDestDir() strips the autobuild- prefix rather than truncating to 16 chars, so the cache key stays distinct. No other files reference the old SHA. CI on build 79977 is effectively green (one unrelated aarch64-musl runner timeout, four retry-green flakes). Nothing blocks beyond the upstream-merge → SHA-swap step, which a human should coordinate.

@robobun
robobun force-pushed the farm/fbe5fb68/fast-seal-freeze-arrays branch from 2eaaec7 to cb814a4 Compare July 25, 2026 04:45
Comment thread scripts/build/deps/webkit.ts
@robobun
robobun force-pushed the farm/fbe5fb68/fast-seal-freeze-arrays branch from cb814a4 to 119953e Compare July 25, 2026 08:47
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.

Object.seal is extremely slow when applying it in big array or object

1 participant