Skip to content

[JSC] Array profiles should not turn ordinary arrays into ArrayStorage (cherry-pick WebKit/WebKit#75376) - #785

Open
robobun wants to merge 1 commit into
mainfrom
robobun/f989c366/cherry-pick-webkit-75376
Open

robobun wants to merge 1 commit into
mainfrom
robobun/f989c366/cherry-pick-webkit-75376

Conversation

@robobun

@robobun robobun commented Oct 9, 2026

Copy link
Copy Markdown
Collaborator

Cherry-pick of WebKit#75376 (67fc02ae76dc, https://bugs.webkit.org/show_bug.cgi?id=325781), open upstream, not reviewed yet. It applies cleanly. Author and message are unchanged.

Problem

  • One frozen array makes optimized code convert ordinary arrays to ArrayStorage: 1,897 to 2,000 of 2,000 arrays that pass through every after it saw frozen arrays.
  • ArrayMode::fromObserved (dfg/DFGArrayMode.cpp:168) picks ArrayStorage with Array::Convert for a site that saw both shapes. ArrayAllocationProfile::updateProfile (bytecode/ArrayAllocationProfile.cpp:63) does it for allocation sites.

Fix

  • fromObserved ignores ArrayStorage next to Undecided, Int32, Double or Contiguous. If optimized code then fails on an ArrayStorage array, the site becomes Array::Generic.
  • updateProfile ignores a last array that has ArrayStorage.
  • Arrayify exits with the new counted kind SparseIndex, so a sparse store site becomes generic and stops exiting.
  • Verified: five new JSTests/stress tests (49 of 50 runs fail on main, 50 pass), and 1,726 runs of related tests.

Background

  • ArrayStorage is the shape of a frozen, sealed or sparse array. It has fewer JIT and builtin fast paths.
  • An ArrayProfile records the shapes one site saw. fromObserved makes the DFG's ArrayMode from it. Array::Convert converts the array in place.
  • No other design was weighed: this is the upstream patch, unchanged.

Downsides

  • A site that often gets both kinds is generic: with half its arrays frozen, a read takes 33 ns, was 22 ns.
  • Each such site must fail once: K of them in one function need up to K + 2 DFG compiles (main: 2), about 1,024 × 2^K calls.
  • A site whose arrays all become sparse converts each: 210 ns per array, was 167 ns.
Notes

The port

  • git apply of the upstream patch on main (7012d42e55): no conflict, offsets of 218 lines (DFGByteCodeParser.cpp) and 34 lines (FTLLowerDFGToB3.cpp). The added and removed lines are the same as upstream's.
  • Five of the eight source files were byte-identical to upstream's base. ArrayProfile.h differs in one #include. In DFGByteCodeParser.cpp, handleIteratorOpen and handleIteratorNext are identical to upstream's. The fork's own changes to that file and to FTLLowerDFGToB3.cpp are elsewhere.
  • Fork code that touches the same data, and what I checked:
    • A builtin function has no UnlinkedArrayProfile here (CodeBlock::updateAllArrayProfilePredictions). So the new SpeculationFailedOnArrayStorage flag of a builtin is lost when its CodeBlock goes away. The site then learns it again with one more compile. Probe: six rounds of $vm.deleteAllCodeWhenIdle() between 10,000 calls each. DFG compiles per round stay at 2 (every caller), 2 to 3 (index loop) and 1 (for...of), and no ordinary array changes shape.
    • The ArrayProfile of a call site lives in CallSiteData here. getArrayMode and the OSR exit stubs reach it through CodeBlock::getArrayProfile as before.
    • BufferAccessorIntrinsic (fork only) reads two flags from its array mode and builds its own mode. It does not emit Arrayify.

Reproduce (jsc --useDollarVM=1 --useConcurrentJIT=0)

function isObject(value) { return typeof value === "object"; }
function every(array) { return array.every(isObject); }
noInline(every);

let frozen = Object.freeze([{}, {}, {}]);
for (let i = 0; i < 10000; ++i) {
    let array = [{}, {}, {}];
    every(array);
    if ($vm.indexingMode(array) !== "ArrayWithContiguous")
        throw new Error("call " + i + ": " + $vm.indexingMode(array));
    if (!(i % 100))
        every(frozen);
}
  • main: Error: call 5198: ArrayWithArrayStorage. With the change: no error.
  • Allocation profile: function create() { return [{}, {}, {}]; }, then Object.freeze(create()); fullGC();. On main the next create() returns ArrayWithArrayStorage. With the change it returns ArrayWithContiguous.

Measurements (x64, Release jsc: the bun-webkit-linux-amd64 build of main and a local build of this branch with the same flags. Five interleaved runs each, median, range in parentheses.)

Case main This change
2,000 ordinary arrays through every after frozen ones: arrays left as ArrayStorage 1,897 to 2,000 0
Same arrays, index loop, ns per read 2.65 (2.47 to 2.98) 2.41 (2.10 to 2.43)
Index loop over 2,000 arrays, half frozen: ns per read 22.5 (20.0 to 23.6) 33.4 (29.8 to 36.7)
Same: ordinary arrays left as ArrayStorage 1,000 of 1,000 0
Object.freeze([i, i + 1, i + 2, i + 3]) at one site: ns per array 1,311 (1,150 to 1,516) 1,155 (1,105 to 1,485)
a = []; a[0] = i; a[100008] = i at one site: ns per array 167 (119 to 205) 210 (190 to 311)
  • The machine was not idle, so the ranges are wide. The freeze row is inside the noise.
  • DFG compiles of a function with K reads a[0] to a[K-1], half of its arrays frozen (--useConcurrentJIT=0, counts are the same on each run):
K 1 2 4 8 12
main 2 2 2 2 2
This change 2 4 6 10 14
Calls until the last compile (this change) under 1,024 4,096 16,384 262,144 4,194,304

The DFG removes the array check of the later reads because the first read already checked. So only the first speculating site fails in each compile, and the reoptimization thresholds double each time. On main the first site converts every ordinary array and nothing exits.

What it adds for code that never sees ArrayStorage

  • JIT code: nothing. No node, check or exit is added on those paths.
  • DFG compile: one more hasExitSite lookup for each array access node (ArrayMode::refine).
  • Profile update: one load and one branch in updateProfile. One branch in computeUpdatedPrediction when a speculation failure was recorded.
  • Data: one ExitKind value and one ArrayProfileFlag bit. sizeof(ArrayProfile) stays 16.

Tests run

  • The five new tests with run-javascriptcore-tests in all modes: main 49 of 50 runs fail (the run that passes is array-allocation-profile-should-not-select-array-storage.js.ftl-no-cjit-small-pool), this branch 0 of 50.
  • run-javascriptcore-tests --filter 'array-storage|arraystorage|arrayify|frozen|freeze|seal|sparse|prevent-extensions|preventExtensions|array-profile|arrayprofile|allocation-profile|indexing': 1,726 runs, 0 failures.

Related

  • Keep the elements of a frozen, sealed or non-extensible array in the ArrayStorage vector #752 (open) gives a non-extensible array the SlowPutArrayStorage shape and edits the lines after the one this change edits in updateProfile. This change leaves SlowPutArrayStorage alone, as upstream does. From a read of that diff: with both merged, fromObserved would pick SlowPutArrayStorage with Array::Convert again for a site that saw a frozen array. That needs a follow-up in whichever lands second.

https://bugs.webkit.org/show_bug.cgi?id=325781

Reviewed by NOBODY (OOPS!).

Freezing an array turns it into ArrayStorage. After that, two profiles
start to turn ordinary arrays into ArrayStorage, and those arrays miss most
of the array fast paths.

1. ArrayMode::fromObserved picks ArrayStorage with Array::Convert for a site
   that has seen both Contiguous and ArrayStorage. Arrayify then converts
   every array that comes to the site.
2. ArrayAllocationProfile picks ArrayStorage for a site when the last array
   allocated there has become ArrayStorage.

The array builtins share their profiles in a realm. Passing one frozen array
to `every`, or freezing one result of `map`, affects every caller.

This patch stops that.

1. ArrayMode::fromObserved ignores ArrayStorage when the site has seen
   Undecided, Int32, Double or Contiguous too. If an ArrayStorage array then
   fails the check in optimized code, ArrayProfile remembers it and the next
   compilation uses Array::Generic.
2. ArrayAllocationProfile ignores the last array when it has become
   ArrayStorage. The site keeps allocating arrays as before, and only the
   arrays that need ArrayStorage are converted later.
3. Arrayify exits with SparseIndex instead of Uncountable when the index is
   sparse, and ArrayMode::refine uses Array::Generic for the site in the
   next compilation. A site that writes to a sparse index did not reach this
   exit when Arrayify converted the array to ArrayStorage first. The other
   sites already kept exiting here.

Both profiles still pick SlowPutArrayStorage as before. Every array has it
when having a bad time.

Some JetStream 3 tests hit this. Counts in one run, before -> after:

                      Arrayify to ArrayStorage  Allocated as ArrayStorage
    proxy-mobx                      23306 -> 0                219027 -> 0
    babel-wtb                       53886 -> 0                 21265 -> 0
    jsdom-d3-startup                 3011 -> 0                 20834 -> 0

These tests were neutral when I ran them locally.

Tests: JSTests/stress/array-allocation-profile-should-not-select-array-storage.js
       JSTests/stress/array-should-not-be-converted-to-array-storage-by-frequent-frozen-arrays.js
       JSTests/stress/array-should-not-be-converted-to-array-storage-by-frozen-array.js
       JSTests/stress/array-without-elements-should-not-be-converted-to-array-storage-by-frozen-array.js
       JSTests/stress/arrayify-should-not-keep-exiting-for-sparse-index.js

* JSTests/stress/array-allocation-profile-should-not-select-array-storage.js: Added.
(shouldBe):
(createWithPush):
(createWithLiteral):
(createWithNewArray):
(createWithMap):
(createDoubleWithPush):
(createInt32WithNewArray):
(createInt32WithConstantLiteral):
(createEmpty):
(test):
* JSTests/stress/array-should-not-be-converted-to-array-storage-by-frequent-frozen-arrays.js: Added.
(shouldBe):
(create):
(createFrozen):
(isObject):
(every):
(some):
(filter):
(forEach):
(at):
(indexed):
(forOf):
(destructure):
(store):
(push):
(test):
* JSTests/stress/array-should-not-be-converted-to-array-storage-by-frozen-array.js: Added.
(shouldBe):
(create):
(createFrozen):
(isObject):
(every):
(some):
(filter):
(forEach):
(at):
(indexed):
(forOf):
(destructure):
(store):
(push):
(test):
* JSTests/stress/array-without-elements-should-not-be-converted-to-array-storage-by-frozen-array.js: Added.
(shouldBe):
(shouldNotBeCompiledRepeatedly):
(first):
(indexed):
(forOf):
(destructure):
(store):
(push):
(test):
* JSTests/stress/arrayify-should-not-keep-exiting-for-sparse-index.js: Added.
(shouldBe):
(shouldNotBeCompiledRepeatedly):
(putAfterOptimized):
(putOncePerArray):
(putToArrayBecomingSparse):
(read):
* Source/JavaScriptCore/bytecode/ArrayAllocationProfile.cpp:
(JSC::ArrayAllocationProfile::updateProfile):
* Source/JavaScriptCore/bytecode/ArrayProfile.cpp:
(JSC::ArrayProfile::computeUpdatedPrediction):
* Source/JavaScriptCore/bytecode/ArrayProfile.h:
(JSC::ArrayProfile::removeObservedArrayModes):
(JSC::ArrayProfile::speculationFailedOnArrayStorage const):
* Source/JavaScriptCore/bytecode/ExitKind.h:
* Source/JavaScriptCore/dfg/DFGArrayMode.cpp:
(JSC::DFG::ArrayMode::fromObserved):
(JSC::DFG::ArrayMode::refine const):
* Source/JavaScriptCore/dfg/DFGArrayifySlowPathGenerator.h:
* Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp:
(JSC::DFG::ByteCodeParser::handleIteratorNext):
* Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp:
(JSC::FTL::DFG::LowerDFGToB3::compileArrayify):
@robobun

robobun commented Oct 9, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: draft. CI is red: Test bun-webkit-linux-amd64-lto failed. Test bun-webkit-linux-arm64-lto and every build lane passed. The failing test is not identified yet, so this stays a draft.

Verified so far

  • The five new JSTests/stress tests with run-javascriptcore-tests: 49 of 50 runs fail on main, 50 of 50 pass with this change (x64 Release). On the debug lane of this pull request (assertions on): 44 of 44 runs pass.
  • 22,398 runs of the array tests on an x64 Release build of this branch (--filter 'array|get-by-val|put-by-val|getbyval|putbyval|for-of|iterator|destructur|spread|species|freeze|frozen|seal|sparse|indexing|prevent|storage|arrayify'): 0 failures.
  • Not done yet: the measurements on the CI builds. The numbers in the description are from local builds.

How this was reproduced (Release jsc of main 7012d42e55, --useDollarVM=1 --useConcurrentJIT=0)

function isObject(value) { return typeof value === "object"; }
function every(array) { return array.every(isObject); }
noInline(every);

let frozen = Object.freeze([{}, {}, {}]);
for (let i = 0; i < 10000; ++i) {
    let array = [{}, {}, {}];
    every(array);
    if ($vm.indexingMode(array) !== "ArrayWithContiguous")
        throw new Error("call " + i + ": " + $vm.indexingMode(array));
    if (!(i % 100))
        every(frozen);
}
  • On main this throws Error: call 5198: ArrayWithArrayStorage. With this change it does not throw.

@github-actions

github-actions Bot commented Oct 9, 2026

Copy link
Copy Markdown

Preview build of 76c588a: autobuild-preview-pr-785-76c588a5

@sosukesuzuki
sosukesuzuki marked this pull request as ready for review October 9, 2026 09:23
@coderabbitai

coderabbitai Bot commented Oct 9, 2026

Copy link
Copy Markdown

Review in 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: 51733215-b734-4258-90e0-2b73f301822b

📥 Commits

Reviewing files that changed from the base of the PR and between 7012d42 and 76c588a.


📒 Files selected for processing (13)
  • JSTests/stress/array-allocation-profile-should-not-select-array-storage.js
  • JSTests/stress/array-should-not-be-converted-to-array-storage-by-frequent-frozen-arrays.js
  • JSTests/stress/array-should-not-be-converted-to-array-storage-by-frozen-array.js
  • JSTests/stress/array-without-elements-should-not-be-converted-to-array-storage-by-frozen-array.js
  • JSTests/stress/arrayify-should-not-keep-exiting-for-sparse-index.js
  • Source/JavaScriptCore/bytecode/ArrayAllocationProfile.cpp
  • Source/JavaScriptCore/bytecode/ArrayProfile.cpp
  • Source/JavaScriptCore/bytecode/ArrayProfile.h
  • Source/JavaScriptCore/bytecode/ExitKind.h
  • Source/JavaScriptCore/dfg/DFGArrayMode.cpp
  • Source/JavaScriptCore/dfg/DFGArrayifySlowPathGenerator.h
  • Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp
  • Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp

Included review availability: This review used your included allowance. 4 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 5 reviews per hour.



Walkthrough

The change updates JavaScriptCore array profiling and DFG handling for ArrayStorage and sparse indices. It adds stress tests for allocation-profile modes, frozen-array operations, empty arrays, and sparse-index reads and writes.

Changes

Array indexing behavior

Layer / File(s) Summary
Array profile mode selection
Source/JavaScriptCore/bytecode/ArrayProfile.*, Source/JavaScriptCore/bytecode/ArrayAllocationProfile.cpp, Source/JavaScriptCore/dfg/DFGArrayMode.cpp
Profiles record failed speculation on ArrayStorage. Mode selection uses that flag or removes ArrayStorage observations before reclassification. Allocation-profile updates skip arrays with ArrayStorage indexing.
Sparse-index exits and DFG paths
Source/JavaScriptCore/bytecode/ExitKind.h, Source/JavaScriptCore/dfg/DFGArrayifySlowPathGenerator.h, Source/JavaScriptCore/dfg/DFGArrayMode.cpp, Source/JavaScriptCore/dfg/DFGByteCodeParser.cpp, Source/JavaScriptCore/ftl/FTLLowerDFGToB3.cpp, JSTests/stress/arrayify-should-not-keep-exiting-for-sparse-index.js
Arrayification and FTL lowering use the SparseIndex exit kind. Array-mode refinement falls back to Generic after that exit. Generic iterator handling reads length with GetById. The stress test checks dense and sparse reads and writes, array values and lengths, and DFG compilation counts.
Allocation-profile mode tests
JSTests/stress/array-allocation-profile-should-not-select-array-storage.js
The test creates object, integer, double, and empty arrays, periodically freezes results, runs full GC, and checks each creator’s expected indexing mode.
Frozen-array indexing-mode tests
JSTests/stress/array-should-not-be-converted-to-array-storage-by-*.js
Tests exercise array operations with mutable and frozen arrays, including empty arrays. They check indexing modes and, for JIT cases, DFG compilation counts.

Priority: ➖ Normal

Merge Risk: ⚪ Minimal · up to 76c58

No confirmed change-related issue blocks merge. The reported Linux LTO failure still needs identification before the draft is ready.

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly identifies the main change: preventing ordinary arrays from being converted to ArrayStorage. The cherry-pick reference is relevant additional context.
Description check Passed The description is complete and relevant. It includes the associated Bugzilla link, problem explanation, implementation details, affected behavior, testing results, performance trade-offs, and follow-…
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.


  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

Beyond the inline findings, I also checked whether the new ExitKind::SparseIndex needs entries elsewhere: this fork's Source/JavaScriptCore/bytecode/ExitKind.cpp has no name/string table and exitKindMayJettison falls through to default: return true, so the new value is counted and jettisons without further table edits. I also checked that the fromObserved recursion in Source/JavaScriptCore/dfg/DFGArrayMode.cpp terminates and does not mutate the CodeBlock's profile: the profile is a by-value copy and the ArrayStorage bits are cleared before the single recursive call, so the new branch cannot be re-entered.

Extended reasoning...

The change is a cherry-pick touching JSC array profiling (ArrayProfile, ArrayAllocationProfile), DFG ArrayMode selection, the DFG and FTL Arrayify slow paths, a new ExitKind, and the for-of iterator-next lowering, plus five new stress tests; it touches no security-sensitive surface. Four verified findings are posted inline about performance-tier regressions (permanent Generic fallback and repeated recompiles), so a human should weigh those; this note only records the two extra checks that were ruled out.

Findings marked 🟡 are optional suggestions and need no follow-up push.

Comment on lines +12830 to +12835
if (arrayMode.type() == Array::Generic) {
auto* data = m_graph.m_getByIdData.add(GetByIdData { CacheableIdentifier::createFromImmortalIdentifier(m_vm->propertyNames->length.impl()), CacheType::GetByIdSelf });
length = addToGraph(GetById, OpInfo(data), OpInfo(SpecInt32Only), iteratedObject);
} else {
Node* butterfly = addToGraph(GetButterfly, iteratedObject);
length = addToGraph(GetArrayLength, OpInfo(arrayMode.asWord()), Edge(iteratedObject), Edge(butterfly, KnownStorageUse));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔴 After a single sparse-index exit inside a for-of loop, every later DFG/FTL compile of that function force-exits at the loop, so the loop never runs optimized again. handleIteratorNext (DFGByteCodeParser.cpp:12830) still emits GetArrayLength unless fromObserved returned Generic, but refine (DFGArrayMode.cpp:233) now makes that node Generic whenever a SparseIndex exit site exists at the same bytecode, and DFGFixupPhase.cpp:2834 turns a Generic GetArrayLength into ForceExit. Fix: take the GetById length path whenever the exit profile has SparseIndex at m_currentIndex (as is done for Generic), or exclude GetArrayLength from the SparseIndex rule in refine, so the recompiled loop runs generically instead of exiting.

Why this was flagged

Trigger: a for-of over a JSArray with at least MIN_SPARSE_ARRAY_INDEX (100000) elements whose op_iterator_next iterable profile saw two shapes (e.g. Int32 and Contiguous, giving Contiguous with Array::Convert), entered mid-loop by OSR entry at an index >= 100000 while the array still has the lesser shape; the Arrayify inserted for the GetByVal (DFGFixupPhase.cpp:4798, index edge at 4790) hits the index check and exits with SparseIndex (DFGArrayifySlowPathGenerator.h:63, FTLLowerDFGToB3.cpp:4437). Any later jettison records every compiled exit stub as a frequent exit site (CodeBlock.cpp:2903 and 3755) into the UnlinkedCodeBlock exit profile (DFGGraph.h:610), which persists. On the recompile the GetArrayLength at DFGByteCodeParser.cpp:12835 has the same origin.semantic as the GetByVal (both emitted at startIndex); its fixup calls refine (DFGFixupPhase.cpp:2832), which returns Array::Generic at DFGArrayMode.cpp:233; line 2834 maps that to ForceExit and blessArrayOperation (DFGFixupPhase.cpp:4910) inserts ForceOSRExit.

Verification: normal — triggered when a for-of site whose iterable profile saw two non-ArrayStorage shapes (e.g. Int32 + Contiguous, giving Contiguous/Array::Convert from DFGArrayMode.cpp:185-203) is entered mid-loop (baseline->DFG OSR entry, or an iterator advanced elsewhere) at an index >= MIN_SPARSE_ARRAY_INDEX while the array still has the lesser shape; after that the loop can never run optimized again in that function.

case Array::Double:
case Array::Contiguous:
m_badPropertyJump = jit->speculationCheck(Uncountable, JSValueSource(), nullptr);
m_badPropertyJump = jit->speculationCheck(SparseIndex, JSValueSource(), nullptr);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) Hot array stores can be permanently downgraded to the generic IC path after one in-bounds store that needed a storage conversion. DFGArrayifySlowPathGenerator.h:63 (and FTLLowerDFGToB3.cpp:4437) now exit with SparseIndex for any index >= MIN_SPARSE_ARRAY_INDEX on the Arrayify slow path, including an in-bounds index into a dense array of 100,000+ elements. One such exit plus any later jettison records the site, and DFGArrayMode.cpp:233 then returns Array::Generic for every later compile, with no recovery. Fix: raise SparseIndex only when the index is actually beyond the array's current length or vector (truly sparse), keeping Uncountable for in-bounds large indices, in both the DFG and FTL Arrayify paths.

Why this was flagged

Trigger: a DFG/FTL-compiled put_by_val (or get_by_val) site whose profile needs Array::Convert, reached with a dense array longer than 100000 whose first store of the new element type lands at an index >= 100000. The index is inside the butterfly vector, so the baseline store just converts Int32 to Double in place; nothing is sparse. In the Arrayify slow path the diff changes the check at DFGArrayifySlowPathGenerator.h:84-86 from an Uncountable exit to a SparseIndex exit without narrowing its condition. CodeBlock::tallyFrequentExitSites (CodeBlock.cpp:3755) adds every stub as a FrequentExitSite at the next jettison for any reason except old age or VM traps (CodeBlock.cpp:2902), into the UnlinkedCodeBlock exit profile, which is never cleared. ArrayMode::refine at DFGArrayMode.cpp:233 then returns Array::Generic in every subsequent compile, so all stores at the site go through the JITPutByValGenerator IC path. On the base branch the same event is an Uncountable exit that nothing consults, so the recompiled code keeps its fast path.

Verification: Source/JavaScriptCore/dfg/DFGArrayifySlowPathGenerator.h:63 and :84-86 now exit with SparseIndex (was Uncountable). The compare is on the raw index, not against the vector length. DFGArrayMode.cpp:233-234 then returns Array::Generic for every later DFG/FTL compile of that function. On the base the identical check exits with Uncountable, which is not consulted by refine(), so the permanent Generic downgrade is new with this change.

Comment on lines +170 to +174
if (profile.speculationFailedOnArrayStorage())
return ArrayMode(Array::Generic, nonArray, Array::AsIs, action).withProfile(profile, makeSafe);
profile.removeObservedArrayModes(arrayStorageModes);
ASSERT(!(profile.observedArrayModes() & arrayStorageModes));
return fromObserved(profile, action, makeSafe);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) Hot functions that read one array at several sites and sometimes see a frozen array now jettison and recompile once per site, up to K+1 DFG compiles with doubling thresholds, not 2 as on the base. DFGArrayMode.cpp:170-174 decides per ArrayProfile; only the exited site's profile gets the flag, and the AI folds later checks on the same base, so one site learns per compile. Fix: once one site fails on ArrayStorage, make every profile in that CodeBlock that observed ArrayStorage with other shapes Generic on the next compile, e.g. propagate SpeculationFailedOnArrayStorage to sibling profiles at jettison. The PR notes this downside.

Why this was flagged

A function reads a[0] .. a[K-1] from the same base and is called with ordinary Contiguous arrays plus an occasional Object.freeze'd one, so each get_by_val's ArrayProfile observes ArrayWithContiguous | ArrayWithArrayStorage. DFGArrayMode.cpp:165-174 strips ArrayStorage from each profile that lacks SpeculationFailedOnArrayStorage and returns Contiguous; DFGAbstractInterpreterInlines.h:4814 (alreadyChecked) lets constant folding remove every check after the first on the same value. When a frozen array arrives only the first site's CheckArray exits; DFGOSRExit.cpp:436-476 stores the failure structure only in that site's profile and ArrayProfile.cpp:132-133 sets the flag only there. The next compile makes that one site Generic, the next site now carries the live check and exits in turn, so K+1 DFG compiles are needed. Each jettison runs countReoptimization (CodeBlock.cpp:3070); the optimize threshold is shifted by 2^counter (CodeBlock.cpp:3197), so the function spends about 1000*2^(K+1) calls in baseline. The base branch returned ArrayStorage+Convert for every site (DFGArrayMode.cpp:181-203) and settled in 2 compiles.

Verification: The PR description's "Downsides" states "K of them in one function need up to K + 2 DFG compiles". Source/JavaScriptCore/dfg/DFGArrayMode.cpp:165-174 decides per ArrayProfile; the OSR exit stub writes the failure structure only to the exiting site's profile (dfg/DFGOSRExit.cpp:446-448,476). On the base, the same profile hits shouldUseFastArrayStorage at line 183, no exits, 2 compiles.

Comment on lines +130 to +133
if (auto structureID = std::exchange(m_speculationFailureStructureID, StructureID())) {
Structure* structure = structureID.decode();
if (hasArrayStorage(structure->indexingType()))
m_arrayProfileFlags.add(ArrayProfileFlag::SpeculationFailedOnArrayStorage);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 (optional) Sites that mostly read frozen arrays and rarely see an ordinary one lose their ArrayStorage fast path for good after merging; every read there goes through the Generic IC (the PR's own numbers: 33 ns vs 22 ns). ArrayProfile.cpp:133 sets SpeculationFailedOnArrayStorage after a single BadIndexingType exit on an ArrayStorage array, and UnlinkedArrayProfile::update (ArrayProfile.h:292-295) copies it into the unlinked profile, so every later CodeBlock of that function starts Generic at DFGArrayMode.cpp:171 and nothing ever clears it. Fix: make the Generic fallback proportional, e.g. only after repeated ArrayStorage failures, and clear or age the flag rather than persisting it in UnlinkedArrayProfile.

Why this was flagged

Trigger: a hot read or write site whose arrays are almost all frozen or sealed (ArrayStorage), plus one ordinary array passing through once. On the base branch observed = ArrayWithContiguous|ArrayWithArrayStorage gives ArrayMode(ArrayStorage, Convert): the one ordinary array is converted and the frozen majority keeps a CheckArray plus direct vector access. With the change DFGArrayMode.cpp:172-174 strips the ArrayStorage bits and compiles Contiguous Convert; the first frozen array hits the Arrayify slow path, fails to convert, exits BadIndexingType and stores its structure in m_speculationFailureStructureID. The next computeUpdatedPrediction (ArrayProfile.cpp:130-135) sets SpeculationFailedOnArrayStorage; DFGArrayMode.cpp:170-171 then returns Array::Generic. UnlinkedArrayProfile::update at ArrayProfile.h:292-295 propagates the flag into the UnlinkedCodeBlock, so every CodeBlock later linked from it, for every closure of the function, is Generic from the first compile; ArrayProfile::clear is never called on that path.

Verification: The PR description lists under "Downsides" that "A site that often gets both kinds is generic: with half its arrays frozen, a read takes 33 ns, was 22 ns"; the bound is understated, because the code needs only ONE ordinary array observation and ONE exit, and the resulting state is permanent and shared across all CodeBlocks of the function. Nothing removes the flag (only ArrayProfile::clear(), ArrayProfile.h:221-227).

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.

2 participants