From 6473271a6b413bcf1fc3f933544147c517575e80 Mon Sep 17 00:00:00 2001 From: Dylan Conway Date: Fri, 28 Aug 2026 00:30:39 +0000 Subject: [PATCH] [JSC] Honor a GC request deferred by DeferGCForAWhile at the next VM entry collectIfNecessaryOrDefer() under a DeferGCForAWhile scope only records m_didDeferGCWork; unlike ~DeferGC, ~DeferGCForAWhile does not act on it, so the request waits for the next allocation slow path or ~DeferGC. The large extra-memory reports made while linking a CodeBlock (ScriptExecutable::prepareForExecutionImpl: metadata table, baseline JIT code) are always in that situation. An embedder that repeatedly enters the VM to run freshly linked large functions can go tens of iterations without either trigger, allocating hundreds of MB of CodeBlocks with no collection, which then land in MarkedBlocks the allocator never revisits and stay unswept until a synchronous full collection. Check the pending flag when a VMEntryScope is constructed and run collectIfNecessaryOrDefer() there. This is a point where the callee is about to allocate anyway; it is skipped while any DeferGC scope is active. Gated by Options::usePollingDeferredGCWorkAtVMEntry (default on). --- Source/JavaScriptCore/heap/Heap.cpp | 7 +++++++ Source/JavaScriptCore/heap/Heap.h | 7 +++++++ Source/JavaScriptCore/runtime/OptionsList.h | 1 + Source/JavaScriptCore/runtime/VMEntryScopeInlines.h | 2 ++ 4 files changed, 17 insertions(+) diff --git a/Source/JavaScriptCore/heap/Heap.cpp b/Source/JavaScriptCore/heap/Heap.cpp index 2b2046fd4f3d..0eb3801665b6 100644 --- a/Source/JavaScriptCore/heap/Heap.cpp +++ b/Source/JavaScriptCore/heap/Heap.cpp @@ -3005,6 +3005,13 @@ void Heap::collectIfNecessaryOrDefer(GCDeferralContext* deferralContext) } } +void Heap::performPendingDeferredGCWork() +{ + ASSERT(hasPendingDeferredGCWork()); + m_didDeferGCWork = false; + collectIfNecessaryOrDefer(); +} + void Heap::decrementDeferralDepthAndGCIfNeededSlow() { // Can't do anything if we're still deferred. diff --git a/Source/JavaScriptCore/heap/Heap.h b/Source/JavaScriptCore/heap/Heap.h index 61ee1f3769ea..f3c245314b53 100644 --- a/Source/JavaScriptCore/heap/Heap.h +++ b/Source/JavaScriptCore/heap/Heap.h @@ -438,6 +438,13 @@ class Heap { void collectIfNecessaryOrDefer(GCDeferralContext* = nullptr); + // A GC request raised while collection was deferred by DeferGCForAWhile (e.g. the extra-memory + // reports in ScriptExecutable::prepareForExecutionImpl) only sets m_didDeferGCWork; nothing acts on it + // until the next allocation slow path or ~DeferGC, which an embedder-driven loop may not reach for a + // long time. VM entry is a safe point to honor it. + bool hasPendingDeferredGCWork() const { return m_didDeferGCWork && !m_deferralDepth; } + JS_EXPORT_PRIVATE void performPendingDeferredGCWork(); + void completeAllJITPlans(); // Note that: diff --git a/Source/JavaScriptCore/runtime/OptionsList.h b/Source/JavaScriptCore/runtime/OptionsList.h index bb22bd7a8b5c..3f1353b807c1 100644 --- a/Source/JavaScriptCore/runtime/OptionsList.h +++ b/Source/JavaScriptCore/runtime/OptionsList.h @@ -238,6 +238,7 @@ bool hasCapacityToUseLargeGigacage(); v(Bool, testTheFTL, false, Normal, nullptr) \ v(Bool, verboseSanitizeStack, false, Normal, nullptr) \ v(Bool, useGenerationalGC, true, Normal, nullptr) \ + v(Bool, usePollingDeferredGCWorkAtVMEntry, true, Normal, "Honor a GC request left pending by a DeferGCForAWhile scope when the VM is next entered, instead of waiting for the next allocation slow path or ~DeferGC."_s) \ v(Bool, useConcurrentGC, true, Normal, nullptr) \ v(Bool, collectContinuously, false, Normal, nullptr) \ v(Double, collectContinuouslyPeriodMS, 1, Normal, nullptr) \ diff --git a/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h b/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h index e8570400283f..3be83b0783fe 100644 --- a/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h +++ b/Source/JavaScriptCore/runtime/VMEntryScopeInlines.h @@ -37,6 +37,8 @@ ALWAYS_INLINE VMEntryScope::VMEntryScope(VM& vm, JSGlobalObject* globalObject) if (!vm.entryScope) setUpSlow(); vm.clearLastException(); + if (vm.heap.hasPendingDeferredGCWork() && Options::usePollingDeferredGCWorkAtVMEntry()) [[unlikely]] + vm.heap.performPendingDeferredGCWork(); } ALWAYS_INLINE VMEntryScope::~VMEntryScope()