From e0f878affed7db759ab2a4039e8c373b8c2f6cf2 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 4 Sep 2026 05:06:20 +0000 Subject: [PATCH 1/2] [JSC] CodeBlock aging: drop the per-allocation byte counter; age idle optimized code against the last collection that saw allocation #557 added a running total in Heap::didAllocate and a per-CodeBlock MB snapshot to tell whether the mutator had been allocating between full collections. With #558's idle tag that is more machinery than the decision needs: a collection already knows how much was allocated since the previous one (totalBytesAllocatedThisCycle()). - Heap stamps m_lastActiveCollectionTime at the start of any collection that finds more than optimizedCodeAgingQuietAllocationMB allocated since the previous one. Nothing is added to didAllocate(). - CodeBlock::shouldJettisonDueToOldAge for FTL / counter-less DFG code: only an idle-tagged collection may drop it, and only once neither the block's compilation nor an active collection has happened for optimizedCodeAgingQuietSeconds. No per-block state is written; m_leaseStartAllocatedMB is gone (CodeBlock is 4 bytes smaller again). - An embedder's periodic no-op collections while idle (Bun ticks collectAsync() every 30 s) don't see allocation and so don't hold the code alive; a forced or allocation-paced collection still can never drop it. Same behaviour end to end (Bun, express app, idle +120 s: 125 MB anon before and after; idle probe 89 -> 2 CodeBlocks, forced-GC probe 89 -> 84); the stress test additionally checks that an idle collection right after an active one keeps the code. --- JSTests/stress/codeblock-aging-ftl-idle.js | 19 ++++++++------ Source/JavaScriptCore/bytecode/CodeBlock.cpp | 27 ++++++-------------- Source/JavaScriptCore/bytecode/CodeBlock.h | 4 --- Source/JavaScriptCore/heap/Heap.cpp | 7 ++--- Source/JavaScriptCore/heap/Heap.h | 8 +++--- Source/JavaScriptCore/runtime/OptionsList.h | 4 +-- 6 files changed, 30 insertions(+), 39 deletions(-) diff --git a/JSTests/stress/codeblock-aging-ftl-idle.js b/JSTests/stress/codeblock-aging-ftl-idle.js index db8d331223ecf..52edbaa6d65b2 100644 --- a/JSTests/stress/codeblock-aging-ftl-idle.js +++ b/JSTests/stress/codeblock-aging-ftl-idle.js @@ -1,12 +1,10 @@ //@ skip if !$isFTLPlatform //@ runDefault("--useEagerCodeBlockJettisonTiming=1", "--useConcurrentJIT=0", "--optimizedCodeAgingQuietSeconds=0.02") -// FTL code has no execution counter of its own, so it ages against the mutator as a whole -// (Options::optimizedCodeAgingQuietAllocationMB): it is jettisoned by a full collection that the -// embedder tagged as idle (idleFullGC() here, GCRequest::isIdle) once (almost) nothing was allocated -// since the previous full collection looked at it for optimizedCodeAgingQuietSeconds, and kept - -// together with the baseline code it pins - whenever the mutator has been allocating, and by every -// collection that is not tagged idle. +// FTL code has no execution counter of its own, so it ages against the mutator as a whole: it is +// jettisoned only by a full collection the embedder tagged idle (idleFullGC() here, GCRequest::isIdle), and +// only once no collection has seen the mutator allocating for optimizedCodeAgingQuietSeconds. Ordinary +// collections never drop it; allocation keeps it (and the baseline code it pins) alive. function shouldBe(actual, expected, msg) { if (actual !== expected) @@ -47,13 +45,18 @@ var o = { a: 1, b: 2 }; var f = makeHot(); if (!warmToFTL(f)) throw new Error("test needs f to reach FTL"); -fullGC(); // records the allocation total for f's FTL block +fullGC(); idleFor(quiet); fullGC(); // quiet and past the window, but not an idle collection: kept idleFor(quiet); fullGC(); shouldBe(f(o)[1], true, "an FTL block should survive full collections that are not tagged idle;"); -idleFullGC(); // the embedder says the application is idle: aged out +allocateMB(4); +fullGC(); // this collection saw the mutator allocating: the clock restarts here +idleFullGC(); // idle, but the mutator was active a moment ago: kept +shouldBe(f(o)[1], true, "an FTL block should survive an idle collection right after an active one;"); +idleFor(quiet); +idleFullGC(); // idle and nothing has happened for the quiet window: aged out shouldBe(f(o)[1], false, "an FTL block should age out in an idle collection after a quiet stretch;"); shouldBe(f(o)[0], 64, "and the function should still work;"); diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index 228689fbb997d..af009d450e52b 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -1375,31 +1375,20 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker // returns and loop back-edges as the sign of life. FTL code, and DFG code compiled without tier-up checks, has no // counter of its own. It is expensive to rebuild, but it also pins its baseline alternative and every baseline // CodeBlock it inlined along with their metadata, so rather than never ageing it ages against the mutator as a - // whole: a full collection lets it go once it is past its TTL and (almost) nothing has been allocated since the - // previous full collection looked at it and the embedder says the application is idle, and renews it whenever the - // mutator has been allocating, keeping the code exactly as before. + // whole: only a collection the embedder tagged idle (GCRequest::isIdle) may let it go, and only once neither its + // own compilation nor a collection that saw the mutator allocating (Heap::lastActiveCollectionTime) has happened + // for optimizedCodeAgingQuietSeconds. Collections paced by allocation, forced by the program or triggered by + // memory pressure never drop it, and a busy mutator keeps the active-collection time fresh. if (agesByMutatorQuietness()) { - unsigned quietMB = Options::optimizedCodeAgingQuietAllocationMB(); - if (!quietMB) + if (!Options::optimizedCodeAgingQuietAllocationMB()) return false; - // Whole megabytes, wrapping: only the distance from the lease's start matters. The first check of a block (field - // still 0) and a block that just moved into this regime both see a large distance and renew. - uint32_t allocatedMB = static_cast(vm().heap.totalBytesAllocated() >> 20); - if (static_cast(allocatedMB - m_leaseStartAllocatedMB) > quietMB) { - // The mutator has been allocating since this block's lease started: renew it from here. - m_leaseStartAllocatedMB = allocatedMB; - m_creationTime = ApproximateTime::now(); - return false; - } - // Any full collection may renew the lease, but only one the embedder requested because the application went idle - // (GCRequest::isIdle) lets the code go: never one paced by allocation, forced by the program (which may do so on - // a timer while hot code runs between the calls) or triggered by memory pressure. - if (!vm().heap.isIdleCollection()) + Heap& heap = vm().heap; + if (!heap.isIdleCollection()) return false; Seconds quietFor = Seconds(Options::optimizedCodeAgingQuietSeconds()); if (Options::useEagerCodeBlockJettisonTiming()) [[unlikely]] quietFor = std::min(quietFor, ttl); - return timeSinceCreation() >= quietFor; + return ApproximateTime::now() - std::max(m_creationTime, heap.lastActiveCollectionTime()) >= quietFor; } if (timeSinceCreation() < ttl) diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.h b/Source/JavaScriptCore/bytecode/CodeBlock.h index 63326b1ce4740..897b5eac5a4bb 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.h +++ b/Source/JavaScriptCore/bytecode/CodeBlock.h @@ -1002,10 +1002,6 @@ class CodeBlock : public JSCell { uint16_t m_optimizationDelayCounter { 0 }; uint16_t m_reoptimizationRetryCounter { 0 }; float m_previousCounter { 0 }; -#if USE(BUN_JSC_ADDITIONS) - // For agesByMutatorQuietness() code: the heap's allocation total, in whole MB modulo 2^32, when the current lease started. - uint32_t m_leaseStartAllocatedMB { 0 }; -#endif StructureWatchpointMap m_llintGetByIdWatchpointMap; RefPtr m_jitCode; #if ENABLE(JIT) diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index 38587d1289764..57d5396eb2d46 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -1574,6 +1574,10 @@ NEVER_INLINE bool Heap::runBeginPhase(GCConductor conn) RELEASE_ASSERT(!m_requests.isEmpty()); m_currentRequest = m_requests.first(); } +#if USE(BUN_JSC_ADDITIONS) + if (totalBytesAllocatedThisCycle() > Options::optimizedCodeAgingQuietAllocationMB() * MB) + m_lastActiveCollectionTime = ApproximateTime::now(); +#endif dataLogIf(Options::logGC(), "[GC<", RawPointer(this), ">: START ", gcConductorShortName(conn), " ", capacity() / 1024, "kb "); @@ -2754,9 +2758,6 @@ void Heap::setGarbageCollectionTimerEnabled(bool enable) constexpr size_t oversizedAllocationThreshold = 64 * KB; void Heap::didAllocate(size_t bytes) { -#if USE(BUN_JSC_ADDITIONS) - m_totalBytesAllocated += bytes; -#endif if (bytes >= oversizedAllocationThreshold) { m_oversizedBytesAllocatedThisCycle += bytes; m_lastOversidedAllocationThisCycle = bytes; diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index b7173fd6ae858..248cc3a4b72f9 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -47,6 +47,7 @@ #include "Synchronousness.h" #include "WeakHandleOwner.h" #include +#include #include #include #include @@ -726,8 +727,9 @@ class Heap { size_t totalBytesAllocatedThisCycle() { return m_nonOversizedBytesAllocatedThisCycle + m_oversizedBytesAllocatedThisCycle; } #if USE(BUN_JSC_ADDITIONS) - // Monotonic; lets a full collection tell whether the mutator did anything since an earlier one (CodeBlock::shouldJettisonDueToOldAge). - uint64_t totalBytesAllocated() const { return m_totalBytesAllocated; } + // When a collection last began that found the mutator had allocated more than a trickle since the one before: the + // mutator was at work then. Idle optimized code ages against this (CodeBlock::shouldJettisonDueToOldAge). + ApproximateTime lastActiveCollectionTime() const { return m_lastActiveCollectionTime; } // The collection in progress was requested by the embedder because the application went idle (GCRequest::isIdle). bool isIdleCollection() const { return m_currentRequest.isIdle; } #endif @@ -884,7 +886,7 @@ class Heap { const size_t m_minBytesPerCycle; size_t m_bytesAllocatedBeforeLastEdenCollect { 0 }; #if USE(BUN_JSC_ADDITIONS) - uint64_t m_totalBytesAllocated { 0 }; + ApproximateTime m_lastActiveCollectionTime; #endif size_t m_sizeAfterLastCollect { 0 }; size_t m_sizeAfterLastFullCollect { 0 }; diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h index cf2b7f246f703..5ffd506303da3 100644 --- a/Source/JavaScriptCore/runtime/OptionsList.h +++ b/Source/JavaScriptCore/runtime/OptionsList.h @@ -86,8 +86,8 @@ bool hasCapacityToUseLargeGigacage(); v(Bool, verboseFFI, false, Normal, "dataLog on FFI thunk/stub/signature creation"_s) #define FOR_EACH_JSC_CODEBLOCK_AGING_OPTION(v) \ v(Bool, useExecutionCountForCodeBlockAging, true, Normal, "If true, an LLInt/Baseline CodeBlock whose execution counter has advanced since the last old-age check is treated as still in use and its TTL is renewed instead of being jettisoned."_s) \ - v(Unsigned, optimizedCodeAgingQuietAllocationMB, 1, Normal, "FTL code (and DFG code without a tier-up counter) past its TTL is jettisoned for old age by a full collection that finds at most this much was allocated since the previous one looked at it, i.e. the mutator has been idle. 0 = such code never ages out."_s) \ - v(Double, optimizedCodeAgingQuietSeconds, 30, Normal, "How long the mutator must have been that quiet, measured from the last collection that saw it allocating, before such code is let go (capped at the tier's TTL under useEagerCodeBlockJettisonTiming)."_s) \ + v(Unsigned, optimizedCodeAgingQuietAllocationMB, 1, Normal, "A collection that finds more than this much allocated since the previous one marks the mutator as active for the aging of FTL code (and DFG code without a tier-up counter); an embedder-tagged idle collection lets such code go once nothing has been active for optimizedCodeAgingQuietSeconds. 0 = such code never ages out."_s) \ + v(Double, optimizedCodeAgingQuietSeconds, 30, Normal, "How long since the last active collection (and since the code was installed) before an idle collection lets such code go (capped at the tier's TTL under useEagerCodeBlockJettisonTiming)."_s) \ v(Double, codeBlockAgingLeaseMultiplier, 3.0, Normal, "When useExecutionCountForCodeBlockAging proves a CodeBlock is still active, renew its old-age TTL to this many multiples of timeToLive for its tier."_s) #define FOR_EACH_JSC_BYTECODE_CACHE_DECODER_OPTION(v) \ v(Bool, useLeanBytecodeCacheDecoder, true, Normal, "If true, the bytecode cache Decoder skips bookkeeping that is only needed for decoded objects shared by multiple references."_s) \ From d265b477e7336132852043ea7ca9810bf85ecc34 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 4 Sep 2026 08:21:36 +0000 Subject: [PATCH 2/2] Review: accumulate per-cycle allocation across collections for the active stamp (a steadily working mutator collected in small cycles still reads as active); guard the test's back-to-back-collections assertion on the measured gap --- JSTests/stress/codeblock-aging-ftl-idle.js | 5 ++++- Source/JavaScriptCore/heap/Heap.cpp | 7 ++++++- Source/JavaScriptCore/heap/Heap.h | 1 + Source/JavaScriptCore/runtime/OptionsList.h | 2 +- 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/JSTests/stress/codeblock-aging-ftl-idle.js b/JSTests/stress/codeblock-aging-ftl-idle.js index 52edbaa6d65b2..9eb0d13889152 100644 --- a/JSTests/stress/codeblock-aging-ftl-idle.js +++ b/JSTests/stress/codeblock-aging-ftl-idle.js @@ -52,9 +52,12 @@ idleFor(quiet); fullGC(); shouldBe(f(o)[1], true, "an FTL block should survive full collections that are not tagged idle;"); allocateMB(4); +var activeAt = preciseTime(); fullGC(); // this collection saw the mutator allocating: the clock restarts here idleFullGC(); // idle, but the mutator was active a moment ago: kept -shouldBe(f(o)[1], true, "an FTL block should survive an idle collection right after an active one;"); +// (unless the two collections themselves took longer than the quiet window, e.g. on a slow debug build) +if (preciseTime() - activeAt < 0.02) + shouldBe(f(o)[1], true, "an FTL block should survive an idle collection right after an active one;"); idleFor(quiet); idleFullGC(); // idle and nothing has happened for the quiet window: aged out shouldBe(f(o)[1], false, "an FTL block should age out in an idle collection after a quiet stretch;"); diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index 57d5396eb2d46..7c2407ce6013b 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -1575,8 +1575,13 @@ NEVER_INLINE bool Heap::runBeginPhase(GCConductor conn) m_currentRequest = m_requests.first(); } #if USE(BUN_JSC_ADDITIONS) - if (totalBytesAllocatedThisCycle() > Options::optimizedCodeAgingQuietAllocationMB() * MB) + // Accumulated across collections, so a mutator that works steadily but is collected often (each cycle small) still + // reads as active; only a genuinely quiet stretch leaves the stamp to age. + m_bytesAllocatedSinceLastActiveCollection += totalBytesAllocatedThisCycle(); + if (m_bytesAllocatedSinceLastActiveCollection > Options::optimizedCodeAgingQuietAllocationMB() * MB) { + m_bytesAllocatedSinceLastActiveCollection = 0; m_lastActiveCollectionTime = ApproximateTime::now(); + } #endif dataLogIf(Options::logGC(), "[GC<", RawPointer(this), ">: START ", gcConductorShortName(conn), " ", capacity() / 1024, "kb "); diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index 248cc3a4b72f9..cc24b589e8be9 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -887,6 +887,7 @@ class Heap { size_t m_bytesAllocatedBeforeLastEdenCollect { 0 }; #if USE(BUN_JSC_ADDITIONS) ApproximateTime m_lastActiveCollectionTime; + size_t m_bytesAllocatedSinceLastActiveCollection { 0 }; #endif size_t m_sizeAfterLastCollect { 0 }; size_t m_sizeAfterLastFullCollect { 0 }; diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h index 5ffd506303da3..4c852bdd2e8fd 100644 --- a/Source/JavaScriptCore/runtime/OptionsList.h +++ b/Source/JavaScriptCore/runtime/OptionsList.h @@ -86,7 +86,7 @@ bool hasCapacityToUseLargeGigacage(); v(Bool, verboseFFI, false, Normal, "dataLog on FFI thunk/stub/signature creation"_s) #define FOR_EACH_JSC_CODEBLOCK_AGING_OPTION(v) \ v(Bool, useExecutionCountForCodeBlockAging, true, Normal, "If true, an LLInt/Baseline CodeBlock whose execution counter has advanced since the last old-age check is treated as still in use and its TTL is renewed instead of being jettisoned."_s) \ - v(Unsigned, optimizedCodeAgingQuietAllocationMB, 1, Normal, "A collection that finds more than this much allocated since the previous one marks the mutator as active for the aging of FTL code (and DFG code without a tier-up counter); an embedder-tagged idle collection lets such code go once nothing has been active for optimizedCodeAgingQuietSeconds. 0 = such code never ages out."_s) \ + v(Unsigned, optimizedCodeAgingQuietAllocationMB, 1, Normal, "A collection that finds more than this much allocated since the last one that did marks the mutator as active for the aging of FTL code (and DFG code without a tier-up counter); an embedder-tagged idle collection lets such code go once nothing has been active for optimizedCodeAgingQuietSeconds. 0 = such code never ages out."_s) \ v(Double, optimizedCodeAgingQuietSeconds, 30, Normal, "How long since the last active collection (and since the code was installed) before an idle collection lets such code go (capped at the tier's TTL under useEagerCodeBlockJettisonTiming)."_s) \ v(Double, codeBlockAgingLeaseMultiplier, 3.0, Normal, "When useExecutionCountForCodeBlockAging proves a CodeBlock is still active, renew its old-age TTL to this many multiples of timeToLive for its tier."_s) #define FOR_EACH_JSC_BYTECODE_CACHE_DECODER_OPTION(v) \