Skip to content

[JSC] Freezing built-in prototypes should not permanently disable fast paths - #622

Open
Jarred-Sumner wants to merge 1 commit into
mainfrom
jarred/cheaper-freeze
Open

[JSC] Freezing built-in prototypes should not permanently disable fast paths#622
Jarred-Sumner wants to merge 1 commit into
mainfrom
jarred/cheaper-freeze

Conversation

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Freezing built-in prototypes (Object.freeze on Array.prototype, RegExp.prototype, String.prototype, Promise.prototype, Map/Set.prototype, Array, Object.prototype) permanently fired JSGlobalObject fast-path watchpoints even though no values changed, leaving common operations up to 200x slower for the rest of the process. This keeps those fast paths alive after freezing.

Changes

  • putDirectInternal: fire a property's replacement watchpoint only when the structure is watching and the stored value actually changes, so attribute-only redefinitions no longer invalidate value-based adaptive watchpoints.
  • validateAndApplyPropertyDescriptor: reuse the existing GetterSetter when getter and setter are unchanged.
  • Object.assign: when only a prototype (not the target) has read-only or accessor properties, check the source keys against the chain instead of always taking the generic path.
  • Freezing an array with no indexed storage (Array.prototype) keeps blank indexing; a new Structure bit, didFreeze, makes length read-only. This keeps the array prototype chain "sane" watchpoint valid.

Verification

  • New stress tests: define-property-same-value-keeps-adaptive-watchpoints.js, object-assign-frozen-object-prototype.js, freeze-array-prototype.js.
  • All 5,836 JSTests/stress files, clean HEAD vs an earlier revision of this branch: no unexpected differences (two watchdog/timeout tests flipped between timeout and failure under load; not re-run in isolation). Final revision: ~450-test targeted subset, no unexpected differences.
  • After freezing: str.replace 244→0 ms, hole reads 110→12 ms, slice 131→16 ms, species map 30→4 ms, promise then chains 28→12 ms, Object.assign 72→41 ms (unfrozen: 29 ms).
  • Unfrozen: 45 put/define/assign/freeze microbenchmarks stay within the measured HEAD-vs-HEAD noise floor (±4%).

…t paths

Object.freeze on intrinsics (Array.prototype, RegExp.prototype, String.prototype,
Promise.prototype, Map/Set.prototype, the Array constructor, Object.prototype)
permanently fired JSGlobalObject fast-path watchpoints even though no property
value changed. This made str.replace, spread, species creation, hole reads,
slice/includes, promise resolution and Object.assign up to 200x slower for the
rest of the process.

- putDirectInternal: only fire a property's replacement watchpoint when the
  stored value actually changes, and only when the structure is watching.
  A defineProperty that only changes attributes no longer invalidates adaptive
  watchpoints that check the value.
- validateAndApplyPropertyDescriptor: reuse the existing GetterSetter when the
  getter and setter are unchanged, so attribute-only redefinitions keep the
  same cell and Equivalence conditions on it stay valid.
- Object.assign: when only a prototype (not the target) has read-only or
  accessor properties, check the source keys against the prototype chain
  instead of always taking the generic path.
- Freezing a JSArray with no indexed storage (Array.prototype) keeps blank
  indexing instead of converting to sparse ArrayStorage. A new Structure bit,
  didFreeze, makes length read-only. Rejected indexed writes on such arrays
  throw without converting storage. This keeps the array prototype chain sane.
@github-actions

Copy link
Copy Markdown

Preview Builds

Commit Release Date
fc961567 autobuild-preview-pr-622-fc961567 2026-09-11 00:04:12 UTC

@coderabbitai

coderabbitai Bot commented Sep 11, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Essentials

Run ID: b8551311-5dcf-49bc-a0d6-ffcac5680d2f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b9ff99 and fc96156.

📒 Files selected for processing (14)
  • JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js
  • JSTests/stress/freeze-array-prototype.js
  • JSTests/stress/object-assign-frozen-object-prototype.js
  • Source/JavaScriptCore/dfg/DFGOperations.cpp
  • Source/JavaScriptCore/runtime/JSArray.cpp
  • Source/JavaScriptCore/runtime/JSArray.h
  • Source/JavaScriptCore/runtime/JSArrayInlines.h
  • Source/JavaScriptCore/runtime/JSObject.cpp
  • Source/JavaScriptCore/runtime/JSObject.h
  • Source/JavaScriptCore/runtime/JSObjectInlines.h
  • Source/JavaScriptCore/runtime/ObjectConstructor.cpp
  • Source/JavaScriptCore/runtime/ObjectConstructorInlines.h
  • Source/JavaScriptCore/runtime/Structure.cpp
  • Source/JavaScriptCore/runtime/Structure.h

Included review availability: 2 reviews are currently available. Your included PR review attempts over the past 7 days set your current allowance at 3 reviews per hour.


Walkthrough

The changes add JavaScriptCore support for frozen dense arrays, prototype-aware Object.assign fast paths, structure freeze tracking, and conditional replacement watchpoints. New stress tests cover array freezing, frozen-prototype assignment, descriptor changes, and built-in operations.

Changes

Freeze and assignment behavior

Layer / File(s) Summary
Array freeze state and mutation paths
Source/JavaScriptCore/runtime/Structure.*, Source/JavaScriptCore/runtime/JSArray.*, Source/JavaScriptCore/runtime/JSArrayInlines.h, Source/JavaScriptCore/runtime/JSObject.cpp, Source/JavaScriptCore/runtime/ObjectConstructor.cpp
Structures record freeze state. Dense arrays preserve indexing during freezing. Length, push, pop, and indexed writes reject invalid mutations.
Prototype-aware Object.assign fast paths
Source/JavaScriptCore/runtime/JSObject.*, Source/JavaScriptCore/runtime/ObjectConstructor.*, Source/JavaScriptCore/runtime/ObjectConstructorInlines.h, Source/JavaScriptCore/dfg/DFGOperations.cpp
Fast-put analysis reports prototype constraints. Object.assign checks read-only and accessor properties when required.
Accessor descriptors and replacement watchpoints
Source/JavaScriptCore/runtime/JSObject.cpp, Source/JavaScriptCore/runtime/JSObjectInlines.h
Matching accessor objects are reused. Replacement watchpoints fire only for enabled watches and changed values.
Freeze, assignment, and watchpoint stress coverage
JSTests/stress/define-property-same-value-keeps-adaptive-watchpoints.js, JSTests/stress/freeze-array-prototype.js, JSTests/stress/object-assign-frozen-object-prototype.js
Stress tests cover frozen arrays, frozen prototypes, Object.assign, accessor descriptors, adaptive watchpoints, and built-in operations.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to fc961

The reviewed freeze, assignment, and watchpoint changes have no identified actionable merge risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description clearly explains the bug, implementation changes, tests, and performance results. It omits the required Bugzilla bug title and link, review-status line, and path/function change list f… Add the associated Bugzilla bug title and URL, include the required review-status line, and list the changed paths with relevant functions or classes.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the primary change: preserving fast paths when built-in prototypes are frozen.
Full details: Description check

Explanation

The description clearly explains the bug, implementation changes, tests, and performance results. It omits the required Bugzilla bug title and link, review-status line, and path/function change list from the repository template.

  • Fix all pre-merge checks with AI

Warning

Git: CodeRabbit could not clone the repository, so clone-backed analysis was skipped and this review may be incomplete. Verify repository clone access, such as SSH credentials, before requesting another full review. If clone access is intentionally unavailable, use path_filters to narrow the review scope.


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

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

throw new Error("FAIL: " + msg);
}

function warm(f, n = 1e4) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit (optional): New stress tests hard-code iteration counts (1e4/1e3) instead of using testLoopCount, which JSTests/README.md (imported by JSTests/CLAUDE.md) lists as a required rule so tests tier up only in configurations where it matters and stay under 200ms elsewhere. sweep:\b1e[34]\b in the three added JSTests/stress files. Fix: drive warm-up loops with testLoopCount (e.g. function warm(f, n = testLoopCount) here; for (let i = 0; i < testLoopCount; i++) in freeze-array-prototype.js:61 and object-assign-frozen-object-prototype.js:21/26/32/42/51). [also at: JSTests/stress/object-assign-frozen-object-prototype.js:20 - nit: new stress tests hard-code 1e4/1e3 iteration counts instead of testLoopCount; JSTests/README.md (imported by…]

Extended reasoning...

JSTests/CLAUDE.md imports JSTests/README.md, whose rule 2 states new tests are required to use testLoopCount/wasmTestLoopCount so the harness can scale iterations per configuration (eager tier-up vs. no-JIT vs. GC-heavy). All three new tests instead hard-code 1e4 (and 1e3) loops: define-property-same-value-keeps-adaptive-watchpoints.js:6/75, freeze-array-prototype.js:27/61/67, object-assign-frozen-object-prototype.js:21/26/32/42/51. In no-JIT or GC-stress configurations these loops run at fixed cost with no tier-up benefit, risking the 200ms budget, and in eager configurations they may over-iterate. Base branch has no such files; the diff introduces the violation.

Verification: nit — JSTests/README.md:20 (imported by JSTests/CLAUDE.md:1 via @ README.md) states as a hard rule for new tests: "Use testLoopCount or wasmTestLoopCount to control how many iterations a test runs. The jsc CLI sets these based on the configuration of the test, so tests iterate enough to tier up where that matters and exit early where it doesn't." All three new tests hard-code counts…

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant