← All reports

[JSC] Unify JS Allocators

Component: JavaScriptCore Heap | aa19dc6

Source/JavaScriptCore/heap/BlockDirectory.cpp

+void BlockDirectory::noteBlockMayBeStealable(unsigned index)
+{
+ if (!isStealable(index))
+ return;
+ m_emptyCursor = std::min<unsigned>(m_emptyCursor, index);
+ subspace()->alignedMemoryAllocator()->addDirectoryWithEmptyBlocks(this);
+}
 
MarkedBlock::Handle* BlockDirectory::findEmptyBlockToSteal()
{
Locker locker(bitvectorLock());
- m_emptyCursor = (emptyBits() & ~inUseBits()).findBit(m_emptyCursor, true);
- if (m_emptyCursor >= m_blocks.size())
- return nullptr;
+ auto stealable = stealableBits();
+ for (;;) {
+ m_emptyCursor = stealable.findBit(m_emptyCursor, true);
+ if (m_emptyCursor >= m_blocks.size())
+ return nullptr;
+ if (!m_blocks[m_emptyCursor]->weakSet().head())
+ break;
+ m_emptyCursor++;
+ }
setIsInUse(m_emptyCursor, true);
return m_blocks[m_emptyCursor];

Source/JavaScriptCore/heap/AlignedMemoryAllocator.cpp

+void AlignedMemoryAllocator::addDirectoryWithEmptyBlocks(BlockDirectory* directory)
+{
+ if (directory->m_isOnEmptyBlocksList.loadRelaxed())
+ return;
+ Locker locker { m_directoriesWithEmptyBlocksLock };
+ if (directory->m_isOnEmptyBlocksList.loadRelaxed())
+ return;
+ directory->m_isOnEmptyBlocksList.storeRelaxed(true);
+ directory->m_nextDirectoryWithEmptyBlocks = m_firstDirectoryWithEmptyBlocks;
+ m_firstDirectoryWithEmptyBlocks = directory;
+}

JSC's heap partitions cells by type into subspaces (JSObject, Structure, JSString, ...), each with a BlockDirectory managing 16KB MarkedBlocks. Whether two directories can steal empty blocks from each other is governed by whether they share an AlignedMemoryAllocator; stealing between directories on one allocator already existed behind Options::stealEmptyBlocksFromOtherAllocators, but each IsoSubspace previously owned its own private allocator, so no IsoSubspace could steal from another. This commit removes IsoSubspace::m_allocator and groups subspaces by memory-region/alignment class instead (fastMalloc-backed, primitive gigacage, and a new shared Structure allocator), replacing the old subspace-walk bookkeeping (Subspace::findEmptyBlockToSteal, registerDirectory/registerSubspace) with a lock-protected linked list of directories currently holding empty blocks. To make the broader stealing safe it adds two per-block exclusions the old path lacked — it previously masked only ~inUseBits — namely stealableBits() (emptyBits & ~destructibleBits & ~inUseBits), which skips blocks still owing a destructor pass, and a check inside the new search loop skipping any block whose weakSet().head() is non-null.

Before (private allocator per IsoSubspace):
  IsoSubspace A                     IsoSubspace B
    private AlignedMemoryAllocator    private AlignedMemoryAllocator
      BlockDirectory A                   BlockDirectory B
  (no shared allocator => A and B can never steal from each other)

After (allocators shared by memory region):
         Shared AlignedMemoryAllocator (e.g. structureAllocator)
     BlockDirectory A   <──steal──>   BlockDirectory B
  still excluded per-block: destructor-owing blocks, blocks with a live WeakSet

Unifying allocators lets empty MarkedBlocks migrate between far more cell-type subspaces than before, widening a heap block-reuse mechanism that JS heap-grooming techniques care about. Without the two new exclusions, handing off either kind of block would also charge one cell type's destructor or WeakBlock-sweep cost to another type's allocation.

This rewires a core heap block-reuse invariant in JSC's collector, so it is a high-value target. Narrow: audit findEmptyBlockToSteal and noteBlockMayBeStealable for races between the relaxed-atomic fast-path check on m_isOnEmptyBlocksList and the locked list mutation, and check whether a block's stealability can change between the check and the actual steal — a WeakSet becoming empty, or a destructor flag going stale — since a missed edge case lets a block be stolen while still owing destructor work or holding live WeakImpls, producing a use-after-free or type confusion between unrelated cell types. Wider: every other place that reasons about which directories share an allocator now reasons over a much larger equivalence class, so re-examine the remaining consumers of alignedMemoryAllocator() and the Options::stealEmptyBlocksFromOtherAllocators gate for assumptions that a shared allocator implies related cell types. Widest: the pattern is "a pooling optimization that merges previously-disjoint equivalence classes" — every invariant that was accidentally upheld by the partition becomes an explicit exclusion check that must be complete. Code-review tell: a new *Bits() accessor composed of several bitvector masks, introduced in the same diff that widens the set of participants the mask applies to.