From 2eaf64efa0f7ca6ad1dac4ceab7cbe4a822c5a06 Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 4 Sep 2026 23:22:24 +0000 Subject: [PATCH 1/2] CodeBlock aging: refresh the execution-counter snapshot on every look, not only once the block is past its TTL shouldJettisonDueToOldAge() returned early while a block was inside its TTL/lease without recording the block's current execution counter. The next collection then compared against a snapshot from before the last burst of work, saw the counter had "moved", and renewed the lease of code that had not run since - and with an embedder whose idle collections come as a pair (Bun: ~10 s and ~75 s after the heap goes quiet) that renewal was never expired: the first idle collection finds every recently-run DFG/Baseline block too young, the second finds its counter moved. Check the counter first and refresh the snapshot on every look; only then apply the TTL. "Moved" now always means "since the previous collection". Actively running code is unaffected (codeblock-aging-execution-count.js). Also: an optimizing block that the old-age check let go this cycle is now jettisoned with JettisonDueToOldAge rather than JettisonDueToWeakReference (an unmarked optimizing block always looked like the latter), so jettison() takes its old-age path. Claude Code (compiled, 20-turn session, then idle), CodeBlocks alive after Bun's second idle collection: before 2,482 (Baseline 1,183 / DFG 1,087, ~7.3 MB + their metadata/JITData), after 432 (225 / 107, ~1.1 MB); anonymous RSS at idle 190-196 MB -> 179 MB. (cherry picked from commit 794bcc3ee56c78e5e3217bdf2139d34f754411c5) (cherry picked from commit 8aa8f4e18e5f12e713254a7eb5f7aecf767bbed9) --- Source/JavaScriptCore/bytecode/CodeBlock.cpp | 29 +++++++++---------- .../runtime/ScriptExecutableInlines.h | 7 +++++ 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index 88b39f3d0eac4..c852d5208f825 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -1393,21 +1393,17 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker return ApproximateTime::now() - std::max(m_creationTime, heap.lastActiveCollectionTime()) >= quietFor; } - if (timeSinceCreation() < ttl) - return false; - if (Options::useExecutionCountForCodeBlockAging()) { - // LLInt and Baseline CodeBlocks already tick an execution counter on - // function entry and loop back-edges. If that counter has moved since we - // last sampled it, the block is demonstrably still running regardless of - // wall-clock age, so renew its lease instead of throwing away a warm block - // that the next iteration will immediately relink, re-profile and re-JIT. + // LLInt and Baseline CodeBlocks already tick an execution counter on function entry and loop back-edges (a DFG + // block, its tier-up counter). If it has moved since the last collection that looked, the block ran in between: + // renew its lease. The snapshot is refreshed on every look - including while the block is still inside its + // lease - so "moved" always means "since the previous collection", never "since some collection before the + // last burst of work" (which kept idle code alive for one extra lease every time, and forever when the + // embedder's idle collections come in pairs). // - // The snapshot lives in m_previousCounter, which updateActivity() in - // reconcileWeakReferencesAtGCEnd 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. + // The snapshot lives in m_previousCounter, which updateActivity() in reconcileWeakReferencesAtGCEnd 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. float currentCount = 0; bool hasCounter = false; switch (type) { @@ -1434,13 +1430,14 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker } if (hasCounter && currentCount != m_previousCounter) { m_previousCounter = currentCount; - // Push the effective creation time forward so the block is not - // considered for old-age jettison again until leaseMultiplier * ttl - // has elapsed with no observed execution. + // Ran since the last look: the lease runs leaseMultiplier * ttl from now with no observed execution. m_creationTime = ApproximateTime::now() + ttl * (Options::codeBlockAgingLeaseMultiplier() - 1.0); return false; } } + + if (timeSinceCreation() < ttl) + return false; #else if (timeSinceCreation() < timeToLive(jitType())) return false; diff --git a/Source/JavaScriptCore/runtime/ScriptExecutableInlines.h b/Source/JavaScriptCore/runtime/ScriptExecutableInlines.h index 381295b5427d4..8d510c83657d0 100644 --- a/Source/JavaScriptCore/runtime/ScriptExecutableInlines.h +++ b/Source/JavaScriptCore/runtime/ScriptExecutableInlines.h @@ -40,6 +40,13 @@ inline void ScriptExecutable::jettisonCodeBlockEdgeIfDead(VM& vm, WriteBarrieragedOut()) + codeBlock->jettison(Profiler::JettisonDueToOldAge); + else +#endif if (codeBlock->shouldJettisonDueToWeakReference(vm)) codeBlock->jettison(Profiler::JettisonDueToWeakReference); else From d8d957b25dca4664041cb6f80aa3f4ad8f22448b Mon Sep 17 00:00:00 2001 From: Jarred Sumner Date: Fri, 4 Sep 2026 23:58:26 +0000 Subject: [PATCH 2/2] CodeBlock aging: one clock read per collection, not one per block visited Heap reads ApproximateTime once when a collection begins (m_currentGCStartApproximateTime, next to the MonotonicTime it already takes) and shouldJettisonDueToOldAge() measures the TTL, renews leases and compares against lastActiveCollectionTime() with that value instead of calling ApproximateTime::now() for every CodeBlock the collection visits. Same results at collection granularity. --- Source/JavaScriptCore/bytecode/CodeBlock.cpp | 8 +++++--- Source/JavaScriptCore/heap/Heap.cpp | 3 ++- Source/JavaScriptCore/heap/Heap.h | 4 ++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Source/JavaScriptCore/bytecode/CodeBlock.cpp b/Source/JavaScriptCore/bytecode/CodeBlock.cpp index c852d5208f825..c7fee3ffff9ee 100644 --- a/Source/JavaScriptCore/bytecode/CodeBlock.cpp +++ b/Source/JavaScriptCore/bytecode/CodeBlock.cpp @@ -1372,6 +1372,8 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker #if USE(BUN_JSC_ADDITIONS) JITType type = jitType(); Seconds ttl = timeToLive(type); + // One clock read per collection (Heap), not one per block visited. + ApproximateTime now = vm().heap.currentGCStartApproximateTime(); // Optimizing tiers: a DFG block ages like a baseline one, using the tier-up counter its code already decrements at // returns and loop back-edges as the sign of life. FTL code, and DFG code compiled without tier-up checks, has no @@ -1390,7 +1392,7 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker Seconds quietFor = Seconds(Options::optimizedCodeAgingQuietSeconds()); if (Options::useEagerCodeBlockJettisonTiming()) [[unlikely]] quietFor = std::min(quietFor, ttl); - return ApproximateTime::now() - std::max(m_creationTime, heap.lastActiveCollectionTime()) >= quietFor; + return now - std::max(m_creationTime, heap.lastActiveCollectionTime()) >= quietFor; } if (Options::useExecutionCountForCodeBlockAging()) { @@ -1431,12 +1433,12 @@ ALWAYS_INLINE bool CodeBlock::shouldJettisonDueToOldAge(const ConcurrentJSLocker if (hasCounter && currentCount != m_previousCounter) { m_previousCounter = currentCount; // Ran since the last look: the lease runs leaseMultiplier * ttl from now with no observed execution. - m_creationTime = ApproximateTime::now() + ttl * (Options::codeBlockAgingLeaseMultiplier() - 1.0); + m_creationTime = now + ttl * (Options::codeBlockAgingLeaseMultiplier() - 1.0); return false; } } - if (timeSinceCreation() < ttl) + if (now - m_creationTime < ttl) return false; #else if (timeSinceCreation() < timeToLive(jitType())) diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index ebe7cbd121c37..bd86665b27ca1 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -1574,12 +1574,13 @@ NEVER_INLINE bool Heap::runBeginPhase(GCConductor conn) m_currentRequest = m_requests.first(); } #if USE(BUN_JSC_ADDITIONS) + m_currentGCStartApproximateTime = ApproximateTime::now(); // 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(); + m_lastActiveCollectionTime = m_currentGCStartApproximateTime; } #endif diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index 0920267d0968a..9d54fc028a173 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -730,6 +730,9 @@ class Heap { // 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; } + // Read once when the current (or last) collection began; CodeBlock aging measures against it instead of reading the + // clock for every block it visits. + ApproximateTime currentGCStartApproximateTime() const { return m_currentGCStartApproximateTime; } // The collection in progress was requested by the embedder because the application went idle (GCRequest::isIdle). bool isIdleCollection() const { return m_currentRequest.isIdle; } #endif @@ -887,6 +890,7 @@ class Heap { size_t m_bytesAllocatedBeforeLastEdenCollect { 0 }; #if USE(BUN_JSC_ADDITIONS) ApproximateTime m_lastActiveCollectionTime; + ApproximateTime m_currentGCStartApproximateTime; size_t m_bytesAllocatedSinceLastActiveCollection { 0 }; #endif size_t m_sizeAfterLastCollect { 0 };