CodeBlock: renew old-age TTL for LLInt/Baseline blocks that are still executing - #333
CodeBlock: renew old-age TTL for LLInt/Baseline blocks that are still executing#333robobun wants to merge 4 commits into
Conversation
… executing CodeBlock::shouldJettisonDueToOldAge discards a CodeBlock purely by wall-clock age since creation (LLInt 5s, Baseline 15s, DFG 20s, FTL 60s). In a long-running JS-heavy process that re-executes the same working set every iteration, this throws away the entire warm set at every full GC and then pays to relink, re-profile and re-JIT it. LLInt and Baseline CodeBlocks already carry an ExecutionCounter that the interpreter / baseline JIT ticks on function entry and loop back-edges. Snapshot that counter in a new CodeBlock::m_lastExecutionCountForAging (8 bytes; sizeof stays at the 224-byte static_assert limit) and, at the point where shouldJettisonDueToOldAge would otherwise return true, compare the live counter with the snapshot: if it moved, update the snapshot, push m_creationTime forward by codeBlockAgingLeaseMultiplier * timeToLive for the tier (default 3x), and return false. Optimizing-tier CodeBlocks have no cheap per-entry counter and keep the existing pure-TTL policy. Two new options in OptionsList.h: useExecutionCountForCodeBlockAging (Bool, default true) codeBlockAgingLeaseMultiplier (Double, default 3.0) Add JSTests/stress/codeblock-aging-execution-count.js which, under useEagerCodeBlockJettisonTiming, verifies that actively-executed functions keep their CodeBlock across repeated full GCs while a never-called function is still discarded.
WalkthroughChangesThe change adds configurable execution-count-based code block aging, renews aging leases when counters advance, and introduces a stress test covering continuously executed and idle functions across repeated garbage-collection cycles. Code block aging
Suggested reviewers: 🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
Comment |
There was a problem hiding this comment.
No issues found, but this changes an on-by-default GC/JIT retention heuristic and grows sizeof(CodeBlock) to the static_assert limit — worth a human sign-off on the policy and memory tradeoff.
What was reviewed:
- Counter-read logic mirrors the existing
updateActivity()lambda;#if ENABLE(JIT)guard matches. m_creationTimeis only read viatimeSinceCreation()in this one path, so pushing it into the future is safe.- Shared
llintExecuteCounteracross sibling CodeBlocks was flagged and ruled out — worst case is a benign extra lease for an idle sibling. - Snapshot initialized in both constructors so a never-entered block does not get a spurious first renewal.
Extended reasoning...
Overview
This PR modifies CodeBlock::shouldJettisonDueToOldAge to renew the old-age TTL for LLInt/Baseline CodeBlocks whose execution counter has advanced since the last check, instead of jettisoning purely on wall-clock age. It adds an 8-byte m_lastExecutionCountForAging field (bringing sizeof(CodeBlock) from 216 to 224, exactly at the existing static_assert limit), initializes it in both constructors, and adds two new runtime options (useExecutionCountForCodeBlockAging, default true; codeBlockAgingLeaseMultiplier, default 3.0). A stress test is included.
Security risks
None identified. This is a GC-time retention heuristic; it does not touch parsing, JIT codegen, auth, or any untrusted-input path. The worst-case failure mode is over-retention of CodeBlocks (higher memory) or under-retention (the old behavior).
Level of scrutiny
This warrants human review. It is a well-reasoned, well-tested, self-contained change, and the implementation mirrors the existing updateActivity() pattern in finalizeUnconditionally. However, it is an on-by-default policy change to a core JIT/GC lifecycle heuristic that trades memory retention against recompilation cost. The 8-byte per-CodeBlock growth lands exactly at the size assertion limit, and the m_creationTime mutation (to a future timestamp) is a novel use of that field. Someone familiar with JSC's memory/performance envelope should confirm the default multiplier and the size budget spend are acceptable.
Other factors
m_creationTime/timeSinceCreation()is only referenced byshouldJettisonDueToOldAgeitself, so setting it to a future time has no side effects elsewhere.- The BaselineJIT counter access is correctly guarded by
#if ENABLE(JIT). - A finder raised the shared
llintExecuteCounter(per-UnlinkedCodeBlock, not per-CodeBlock) causing spurious renewals for idle siblings; verifiers concluded this is benign since the effect is bounded to one extra lease and idle siblings are rare. - The PR description reports stress-test and end-to-end verification with clear before/after numbers, and the option can be disabled at runtime if a regression surfaces.
… to cover Baseline TTL Drop the new m_lastExecutionCountForAging field and store the aging snapshot in the existing float m_previousCounter instead. updateActivity() in finalizeUnconditionally already uses that field for the same signal behind VM::useUnlinkedCodeBlockJettisoning(); outside that mode the field was unused, and in that mode both writers store the same current count so they agree. This keeps sizeof(CodeBlock) at 216 rather than spending the remaining headroom under the 224-byte static_assert. Bump the stress test's busy-wait from 25ms to 60ms so it exceeds the eager Baseline TTL (30ms) as well as the LLInt TTL (10ms), making the active-function assertion a real regression test even if the active set reaches Baseline.
|
Pushed 91ca606 addressing both points:
Re-ran the probe (200 active fns × 50 GCs, 60ms wait): OFF ≈ 10k jettisons, ON = 0; stress test passes in default / |
There was a problem hiding this comment.
Beyond the inline nits, two other concerns were checked and ruled out: (1) a CodeBlock that starts life at Baseline via a cached unlinked-code path still snapshots the LLInt counter in the constructor, but the first old-age check reads the Baseline executeCounter() instead, so the != compare against the LLInt-sourced snapshot does not grant a free renewal for a never-entered block; (2) the renewal path writing m_previousCounter before finalizeUnconditionally runs does not starve updateActivity()'s UnlinkedCodeBlock-age reset, since that path compares with < and both writers store the same current count.
Extended reasoning...
This is a default-on change to a core JSC GC/jettison heuristic (shouldJettisonDueToOldAge), so it should get a human sign-off regardless of the automated findings; I'm not approving. The three inline comments are all nit-level (float-ULP edge, eden-vs-full snapshot timing under a non-default option, shared LLInt counter aliasing) and each degrades gracefully to the pre-PR policy — none block. This note just records the two additional interactions the finder pass raised that verifiers refuted, so a human reviewer doesn't re-derive them.
| float currentCount = 0; | ||
| bool hasCounter = false; | ||
| switch (type) { | ||
| case JITType::InterpreterThunk: | ||
| currentCount = m_unlinkedCode->llintExecuteCounter().count(); | ||
| hasCounter = true; | ||
| break; | ||
| #if ENABLE(JIT) | ||
| case JITType::BaselineJIT: | ||
| if (auto* jitData = baselineJITData()) { | ||
| currentCount = jitData->executeCounter().count(); | ||
| hasCounter = true; | ||
| } | ||
| break; | ||
| #endif | ||
| default: | ||
| break; | ||
| } | ||
| if (hasCounter && currentCount != m_previousCounter) { | ||
| m_previousCounter = currentCount; |
There was a problem hiding this comment.
🟡 Minor: after deferIndefinitely() (e.g. dontJITAnytimeSoon() when !shouldJIT() / !ENABLE(JIT), or dontOptimizeAnytimeSoon() on DFG CompilationFailed), count() sits at ~INT32_MIN where the float ULP is 128, so up to ~4 function entries (+15) or ~64 loop back-edges (+1) round to the same float and the != check misses the movement. Not a regression — it just falls back to the pre-PR TTL policy for those blocks — and updateActivity() has the same limitation via the same field, but a short comment noting the ULP cliff (or comparing the raw int32_t m_counter if you ever revisit this) might save a future reader some confusion.
Extended reasoning...
What happens
ExecutionCounter::count() returns static_cast<double>(m_totalCount) + m_counter (ExecutionCounter.h:66), but the renewal path stores it into float currentCount and compares against float m_previousCounter. When the counter has been through deferIndefinitely() — which sets m_totalCount = 0, m_counter = INT32_MIN (ExecutionCounter.cpp:71-73) — count() returns -2147483648.0. In the range [2^30, 2^31) a 32-bit float has ULP 2^7 = 128, so any subsequent m_counter value in [INT32_MIN, INT32_MIN+64] rounds back to -2147483648.0f. The currentCount != m_previousCounter check at line 1352 then reports "no movement" and the block is jettisoned even though it executed.
When the counter is deferred
This state is reached routinely for exactly the blocks that are pinned to LLInt/Baseline forever:
- LLInt-pinned:
LLIntSlowPathsentryOSR/loop_osr/retcallcodeBlock->dontJITAnytimeSoon()(which callsm_unlinkedCode->llintExecuteCounter().deferIndefinitely(), CodeBlock.h:617) whenever!shouldJIT()— i.e.--useJIT=0,bytecodeRangeToJITCompilefilters, or!ENABLE(JIT)builds (LLIntSlowPaths.cpp:419/436/490/512/531/534). LLInt keeps incrementingm_counterviabaddisafter the defer (LowLevelInterpreter.asm:1581), so the counter climbs fromINT32_MINby +15/entry or +1/back-edge. - Baseline-pinned:
setOptimizationThresholdBasedOnCompilationResult()callsdontOptimizeAnytimeSoon()(→baselineJITData()->executeCounter().deferIndefinitely(), CodeBlock.cpp:2782-2783) after DFGCompilationFailed(CodeBlock.cpp:2860).
These are the two "stuck-in-tier-forever" populations the PR's policy is arguably most useful for.
Concrete walk-through
- Block is LLInt with
--useJIT=0. After ~34 calls the LLInt counter crossesthresholdForJITAfterWarmUp=500,entryOSRfires,!shouldJIT()→dontJITAnytimeSoon()→m_totalCount=0, m_counter=-2147483648. - Full GC maybe upgrade #1, TTL exceeded.
currentCount = (float)(0 + -2147483648) = -2147483648.0f. Saym_previousCounterwas ~-500.0ffrom before the defer, so!=holds → renewed, snapshot set to-2147483648.0f. - Block is called once in the next lease window:
m_counter = -2147483633. - Full GC Move stdint.h import out of __cplusplus guard in ForkExtras.h #2, TTL exceeded.
count() = -2147483633.0;(float)(-2147483633.0)rounds to nearest representable float. Nearest floats are-2147483648.0fand-2147483520.0f; midpoint is-2147483584;-2147483633is on the-2147483648side →currentCount = -2147483648.0f == m_previousCounter. Block is jettisoned despite having run.
Any block called ≤4 times (4×15=60 < 64) or with ≤64 loop back-edges per lease window falls through this hole.
Why this is a nit, not a bug
- Not a regression. The failure mode is exactly the pre-PR TTL jettison. Nothing gets worse than before this change.
- Pre-existing pattern.
updateActivity()infinalizeUnconditionally(CodeBlock.cpp:1854/1885) already storescount()into afloatand compares viam_previousCounter < countwith the identical precision cliff. The PR deliberately reuses that field to keepsizeof(CodeBlock)==216(per the review round in the timeline), so this is aligning with an established trade-off, not introducing a new one. - Narrow population × low rate.
!useJIT/bytecodeRangeToJITCompileare debug/testing knobs,!ENABLE(JIT)is not a Bun production config, and DFGCompilationFailedis rare. Within that population it only bites blocks called <~5 times per3×TTL— marginal "warm" candidates. - Test gap is real but low-value. The stress test's 80 active functions get ~32 calls × 15 ≈ 480 < 500 before the first GC, so under
--useJIT=0entryOSRnever fires anddeferIndefinitely()is never reached — the test passes without exercising this path. That's worth knowing but doesn't change the severity.
Possible fix (if the author cares)
Compare the raw int32_t m_counter directly (it's what actually moves post-defer, and int32 equality has no ULP), or store the snapshot as double. Both would either diverge from updateActivity() or grow sizeof(CodeBlock), which the PR explicitly avoided — so simply adding a comment next to the float currentCount declaration noting that deferred counters near INT32_MIN may lose small deltas to float rounding is probably the right call.
| // The snapshot lives in m_previousCounter, which updateActivity() in | ||
| // finalizeUnconditionally also writes for UnlinkedCodeBlock aging when | ||
| // VM::useUnlinkedCodeBlockJettisoning() is enabled. Both sites store the | ||
| // same current count for the same tier, so they agree; outside that mode | ||
| // updateActivity() never touches the field. |
There was a problem hiding this comment.
🟡 Minor: under --useUnlinkedCodeBlockJettisoning=1, updateActivity() in finalizeUnconditionally can run on an eden GC (for a write-barriered CodeBlock) and overwrite m_previousCounter with the current count, while this old-age check only reaches the counter comparison on full GCs — so an eden GC after the last execution can absorb the delta and the next full GC sees currentCount == m_previousCounter and jettisons anyway. It's mini-mode only, needs the block to have been barriered (IC fill / call link), and gracefully degrades to the pre-PR policy, so not blocking — but the "both sites … agree" comment slightly overstates the harmony; might be worth noting the eden-vs-full timing caveat.
Extended reasoning...
What the interaction is. shouldJettisonDueToOldAge() now reuses m_previousCounter as its "did the block execute since I last looked" snapshot, and the comment at CodeBlock.cpp:1329-1333 says the other writer — updateActivity() inside finalizeUnconditionally() — "stores the same current count for the same tier, so they agree". That is true when both writers observe the counter within the same GC cycle, but they can also fire in different cycles. shouldJettisonDueToOldAge() runs during marking and early-returns at visitor.isMarked(this) (line 1309), so on an eden GC an old-generation CodeBlock (sticky-marked) never reaches the counter comparison. updateActivity(), on the other hand, runs in finalizeUnconditionally() and unconditionally does m_previousCounter = count; (line 1885) whenever VM::useUnlinkedCodeBlockJettisoning() is on — with no CollectionScope gate.
Concrete walk-through (mini-mode, --useUnlinkedCodeBlockJettisoning=1):
- Full GC N: block is past its TTL;
shouldJettisonDueToOldAgeseescurrentCount = 100 ≠ m_previousCounter, renews the lease, writesm_previousCounter = 100. Later in the same cycleupdateActivity()writesm_previousCounter = 100again — harmless. - The block executes; an LLInt slow path (IC fill,
CallLinkInfolink, etc.) callsvm.writeBarrier(codeBlock), putting it in the remembered set. Counter is now 150. - Eden GC: the barriered CodeBlock's
visitChildrenfires from the mutator mark stack, which re-adds it to the finalizerIsoCellSet(line 1234).shouldJettisonDueToOldAgeearly-returns atisMarkedand does not update the snapshot. ThenfinalizeUnconditionally→updateActivity()writesm_previousCounter = 150. - The block happens to be idle in the (short) window between that eden GC and the next full GC.
- Full GC N+1 after the renewed lease expires:
currentCount = 150 == m_previousCounter→hasCounter && currentCount != m_previousCounteris false → the block is jettisoned even though it ran between the two full GCs.
Addressing the counter-argument. One reading is that updateActivity() "doesn't run on every eden GC" because finalizeUnconditionally iterates codeBlockSpaceAndSet.set (Heap.cpp:807), which is emptied each cycle at line 1889 and only repopulated by visitChildren — so on an eden GC only write-barriered CodeBlocks are in the set. That's correct, and it does narrow the window: bumping the LLInt/Baseline execution counter alone does not barrier the CodeBlock. But actively-executing LLInt/Baseline blocks are frequently barriered — LLIntSlowPaths.cpp calls vm.writeBarrier(codeBlock) at 10+ sites for IC transitions and call linking — so the scenario is reachable for exactly the warm-but-not-yet-IC-stable blocks this PR is trying to protect. The stress test only calls fullGC() explicitly with no interleaved eden collections, so it wouldn't observe this.
Why this is a nit, not a bug. It only applies under the non-default useUnlinkedCodeBlockJettisoning (mini-mode); it requires the block to have been barriered before the eden GC and idle in the eden→full window; and the failure mode is a graceful fallback to exactly the pre-PR TTL policy — no crash, no correctness issue, just a missed lease renewal. The PR author's timeline comment ("kept the refresh in the age check rather than finalizeUnconditionally, so the eden-GC gating caveat doesn't apply") shows this was considered, but that remark covers where the new refresh lives — it doesn't stop the pre-existing updateActivity() write from consuming the delta first.
Suggested tweak. Tighten the comment to something like: "…so within a full GC they agree; note that under useUnlinkedCodeBlockJettisoning, updateActivity() may also run on an eden GC for a write-barriered block and advance the snapshot before this check next runs, in which case that block falls back to the plain-TTL policy for one lease." If you wanted to close the gap entirely you could gate the m_previousCounter = count; store in updateActivity() on collectionScope == CollectionScope::Full, but given how narrow and benign this is, the comment clarification alone seems sufficient.
| case JITType::InterpreterThunk: | ||
| currentCount = m_unlinkedCode->llintExecuteCounter().count(); | ||
| hasCounter = true; | ||
| break; |
There was a problem hiding this comment.
🟡 Minor caveat worth a note in the comment block: llintExecuteCounter() lives on the UnlinkedCodeBlock, so when one UnlinkedCodeBlock is shared by multiple linked CodeBlocks (multi-realm, code-cache reuse, call/construct pairs), an idle sibling will observe the shared counter advance and get a lease renewal it never earned. This is over-retention only and mirrors the pre-existing updateActivity() aliasing, so not a blocker — but since the PR text frames the counter as a per-CodeBlock activity signal, a one-line acknowledgment in the comment would save the next reader the head-scratch.
Extended reasoning...
What the issue is
The InterpreterThunk branch at CodeBlock.cpp:1337-1338 reads m_unlinkedCode->llintExecuteCounter().count() and compares it against the per-CodeBlock snapshot m_previousCounter. But m_llintExecuteCounter is a field on UnlinkedCodeBlock (UnlinkedCodeBlock.h:525), and a single UnlinkedCodeBlock can back several linked CodeBlocks — the same function linked into multiple JSGlobalObjects, code-cache hits across script re-evaluations, or the CopyParsedBlock constructor for call/construct pairs. The LLInt prologue/loop-hint code increments the counter through the UnlinkedCodeBlock pointer (see LowLevelInterpreter.asm), so every linked sibling sees the same monotonically advancing value.
Concrete walk-through
- Function
fis linked into global A →CodeBlock_A, and also into global B →CodeBlock_B. Both shareUnlinkedCodeBlock_Uand both constructors snapshotm_previousCounter = U.llintExecuteCounter().count()— say0. - Global A calls
fin a hot loop; the LLInt ticksU.m_llintExecuteCounterup to, say,420. Global B never callsfagain. - A full GC runs after
CodeBlock_B's LLInt TTL (5 s / 10 ms eager) has elapsed.shouldJettisonDueToOldAgeonCodeBlock_BreadscurrentCount = 420, compares to its snapshot0, sees420 != 0, updates the snapshot to420, pushesm_creationTimeforward by~3×TTL, and returnsfalse. - As long as A keeps calling
f,CodeBlock_B— which never executed after warm-up — keeps getting renewed indefinitely.
The stress test doesn't exercise this because each active/idle function is a distinct new Function(...) body with its own UnlinkedCodeBlock.
Why nothing prevents it
There is no per-linked-CodeBlock LLInt entry counter; the only LLInt-tier signal available is the shared one on UnlinkedCodeBlock. The constructor snapshot added in this PR correctly suppresses the first spurious renewal for a freshly-linked block, but once the active sibling ticks the counter past that snapshot the idle sibling will renew.
Impact
Over-retention only — no crash, no correctness regression. And it's bounded:
- The heavy state (bytecode, metadata, profiles) lives in
UnlinkedCodeBlock_U, which is already kept alive by the activeCodeBlock_A. The extra retained memory is justCodeBlock_B's ~216-byte cell plus its linked metadata. - With JIT enabled, the active sibling typically tiers to Baseline within ~500 ticks and switches to a per-
CodeBlockBaselineJITData::executeCounter(), so the shared-counter window is short in practice. - The identical aliasing already exists in
updateActivity()atCodeBlock.cpp:1861/1881forUnlinkedCodeBlockaging, so this PR follows an accepted precedent rather than introducing a new one.
Suggested change
No code change needed. A one-line note in the existing comment block — e.g. "The LLInt counter is per-UnlinkedCodeBlock, so an idle sibling of an active CodeBlock may also be renewed; this matches updateActivity() and is over-retention only." — would document the trade-off. Actually fixing it would require a per-CodeBlock LLInt counter and grow sizeof(CodeBlock), which is exactly what the m_previousCounter-reuse commit just avoided.
…ault it off Default useExecutionCountForCodeBlockAging to false so CodeBlock old-age jettisoning behaves exactly as before unless the option is opted into. Move the two new options into a FOR_EACH_JSC_CODEBLOCK_AGING_OPTION macro under USE(BUN_JSC_ADDITIONS), following the existing FOR_EACH_JSC_FFI_OPTION pattern, and wrap the CodeBlock.cpp changes in the same directive so builds without BUN_JSC_ADDITIONS keep the upstream code path untouched. No-Verification-Needed: JSC-internal change in the WebKit fork; validated by the preview build CI rather than the pinned prebuilt used by the Bun binary
Preview Builds
|
Problem
CodeBlock::shouldJettisonDueToOldAgediscards a CodeBlock purely by wall-clock age since creation (LLInt 5s, Baseline 15s, DFG 20s, FTL 60s): at each full GC an unmarked CodeBlock older than its TTL is jettisoned even if it ran a millisecond ago, because "still in use" is only sampled by "was on the stack at GC time". In a long-running JS-heavy Bun process that re-executes the same few thousand functions every iteration (for example a large interactive TUI-style app), the whole warm working set is thrown away at every full GC and then relinked, re-profiled and re-JIT'd, over and over.On such a workload (~5.5k linked functions, 100 iterations of the same activity) we measured ~12.5k old-age jettisons, ~2.6k of which were repeated jettisons of the same functions, and hundreds of redundant DFG recompilations.
Change
LLInt and Baseline CodeBlocks already carry an
ExecutionCounterthat the interpreter / baseline JIT ticks on function entry and loop back-edges (UnlinkedCodeBlock::llintExecuteCounter()for LLInt,BaselineJITData::executeCounter()for Baseline). InshouldJettisonDueToOldAge, after the TTL check, compare the live counter with a per-CodeBlocksnapshot: if it moved, update the snapshot, pushm_creationTimeforward so the block is not considered old again forcodeBlockAgingLeaseMultiplier * timeToLive(default 3x) of its tier, and return false. Optimizing-tier CodeBlocks have no cheap per-entry counter and keep the existing pure-TTL policy.The snapshot reuses the existing
float m_previousCounterfield.updateActivity()infinalizeUnconditionallyalready uses it for the same signal behindVM::useUnlinkedCodeBlockJettisoning(); outside that mode the field was unused, and in that mode both writers store the same current count for the same tier so they agree.sizeof(CodeBlock)stays at 216. The snapshot is initialised in bothCodeBlockconstructors to the LLInt counter's value at creation time, so a CodeBlock that is linked and never entered does not get a spurious renewal, and the!=comparison (rather than<) handles the LLInt→Baseline counter-source transition correctly.Two new options in
runtime/OptionsList.h, both tunable at runtime:Verification
Policy
jsc --useEagerCodeBlockJettisonTiming=1with 200 functions each called once per iteration, 60ms busy-wait thenfullGC(), 50 iterations; a jettison is counted when$vm.codeBlockFor(fn) === undefinedimmediately after the GC.--useExecutionCountForCodeBlockAging=0--useExecutionCountForCodeBlockAging=1A companion probe with a separate set of never-called functions under the new policy recorded 0 jettisons of the active set while the idle set was still discarded, so genuinely one-shot CodeBlocks are still collected and this is not equivalent to
forceCodeBlockLiveness.End-to-end (Bun, macOS arm64)
Same binary, policy on vs off, large TUI workload described above:
m_previousCountervariant)phys_footprintThis lands within noise of the
forceCodeBlockLiveness=1upper bound on CPU/wall while still discarding ~7k genuinely one-shot CodeBlocks per session; repeated jettisons of the same function drop from ~2.6k to ~130.(I looked for
oven-sh/bun-perf-testerto A/B the express / fastify / elysia server benches but could not locate that repo; happy to run them if someone points me at it.)Tests
JSTests/stress/codeblock-aging-execution-count.jsruns underuseEagerCodeBlockJettisonTimingwith a 60ms wait between GCs (exceeds both the 10ms LLInt and 30ms Baseline eager TTLs) and asserts that 80 actively-executed functions keep theirCodeBlockacross 30 full GCs while a never-called function is still discarded. The test passes in the default config, with--thresholdForJITAfterWarmUp=5(functions at Baseline), with--useJIT=0(LLInt only), with--useUnlinkedCodeBlockJettisoning=1(mini-mode, sharedm_previousCounter), and with--codeBlockAgingLeaseMultiplier=1.0, and fails as expected with--useExecutionCountForCodeBlockAging=0.Ran
Tools/Scripts/run-jsc-stress-tests JSTests/stress --quick: no regressions attributable to this change (every pre-existing failure on this build behaves identically with the option on vs off). Release and Debug JSCOnly configurations both compile cleanly, and theBaselineJITcounter access is guarded by#if ENABLE(JIT)so non-JIT / 32-bit configs are unaffected.