Repository navigation
GC: Give back the YG room that a young external memory debit releases - #2263
YevheniiKotyrlo wants to merge 1 commit into
Conversation
creditExternalMemory lowers the YG's effective end by the room a young cell's credit takes, and debitExternalMemory left it there until the next YG collection. Memory released from a young cell while it lives - a lowered setExternalMemoryPressure, a detached ArrayBuffer, a shrunk OrderedHashMap - therefore brought the next YG collection forward as if it were still held. The YG segment now records the room external memory has taken since its effective end was last set, its external memory charge, and a young debit caps that charge at the young external memory still credited, giving back the rest. Setting the effective end resets the charge, so the OG's request for a YG collection stands after a debit. clearExternalMemoryCharge becomes resetEffectiveEnd, since it lifts every limit on the end, not only the charge.
|
The measurements behind this PR, from runs of its head Base against head. Linux x86-64 (WSL Ubuntu 22.04), GCC 13.3, Debug with asserts.
The JSI case is the reproduction. It starts from an empty YG and runs this, each object's pressure released before the next object is made: for (int i = 0; i < 16; ++i) {
jsi::Object o{rt};
o.setExternalMemoryPressure(rt, 16 * 1024 * 1024);
o.setExternalMemoryPressure(rt, 0);
}On What each case rules out. Each was run against a copy of the change broken one way, and fails there:
|
Summary
HadesGC::debitExternalMemorylowersygExternalBytes_for a young cell and leaves the YG's effective end wherecreditExternalMemoryput it (HadesGC.cpp):So the room a credit took comes back only at the next YG collection, and memory a young cell releases while it is alive — a lowered
jsi::Object::setExternalMemoryPressure, a detachedArrayBuffer, a shrunkOrderedHashMap— still brings that collection forward as if it were held. JSI hosts that release native memory eagerly meet this on every release: react-native-skia'sdispose()lowers an object's pressure to 256 bytes (JsiSkNativeObjects.h), so an app that bakes each view's drop shadow into an offscreen surface and disposes the surface once it has the image keeps every texture's room taken until the next YG collection. React Native 0.86 ships this code as Hermes V1 (250829098.0.17, from250829098.0.0-stable), andmainhas the same debit. Sixteen credits each released before the next allocation run 15 YG collections onstatic_h, and none with this PR (measurements).The YG segment now records the room external memory has taken since its effective end was last set, its external memory charge, and a young debit caps that charge at the young external memory still credited, giving back the rest. A credit that found the segment full took less, so its debit gives back only what it took. Setting the effective end resets the charge, so the OG's request for a YG collection (
creditExternalMemorysetting the end to the level) still stands after a debit, and a finalizer's debit, which runs during a YG collection orfinalizeAll, is overwritten by the end each of them sets afterwards, as before. With a charge of its own on the segment,clearExternalMemoryChargebecomesresetEffectiveEnd: it sets the end to the real end, lifting the size factor and the OG's request with the charge, as the comment inyoungGenCollectionalready describes it. A young debit costs amin, an add and a store more; the allocation path and the JIT's offsets are untouched.An old cell's debit is unchanged: it never moved the YG's end, and the OG's request, once made, stays until the YG collection it asks for. The checked-in sandbox picks this up at its next regeneration.
Test Plan
Linux x86-64 (WSL Ubuntu 22.04), GCC 13.3, Debug and ASan+Debug at -O1. The reproduction is the new JSI case, which starts from an empty YG and runs:
On
static_hbuilt with this PR's tests, the YG collects on every iteration after the first:With this PR it does not collect, and the case passes on the plain and the thread-safe runtime; the sandbox reports no heap information, so its case returns early.
HermesVMRuntimeTests --gtest_filter='GCBasicsTestExternalMemory.*:AlignedHeapSegmentTest.*ExternalMemory*'—YoungGenDebitGivesBackRoomruns 15 YG collections onstatic_hand none with the fix; the other cases pin the charge and its cap in exact bytes, and each case fails against a copy of the fix broken the way it rules out (which).HermesVMRuntimeTests,APITestsandcheck-hermespass, with no AddressSanitizer report; test262 at the revisionbuild.ymlpins passes on Linux (38,702) and on Windows with Visual Studio 2026, Debug and Release (37,030), none failing.clang-format20.1.8 leaves the changed files as they are.debitExternalMemoryandexternal memory young generation collection, and PRs fordebitExternalMemory effective end; OrderedHashMapBase::clear() strands external-memory credit; per-frame Set/Map clear()+refill OOMs long sessions #2111 is a different imbalance in the same accounting.