blob: 4d7555a842927f7fb473752a2c19bc019fb1e614 [file]
/*
* Copyright (C) 2026 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <JavaScriptCore/SlotVisitorMacros.h>
#include <JavaScriptCore/StrongBlock.h>
#include <wtf/HashCountedSet.h>
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
class JSCell;
class VM;
// Owns the StrongBlocks holding a VM's Strong<> handle slots, as a slab allocator
// bump-allocating from one current block whose cursors are hoisted into this
// object. Block selection is cold by construction: every block it picks has at
// least s_reAdmissionWatermark free slots to hand out, and StrongBlock::capacity
// of them when that block is empty.
//
// allocate()/deallocate() and every other mutation must run with the JSLock held.
class StrongSet {
WTF_MAKE_NONCOPYABLE(StrongSet);
public:
// Re-admitting a full block on its first free would let the pattern "fill a
// block, free one slot, immediately reallocate it" transition it from full to
// eligible once per cycle forever. Waiting for this many free slots also
// amortizes the cursor save/reload over at least that many allocations, and
// at ~3% of capacity costs essentially no reuse.
static constexpr unsigned s_reAdmissionWatermark = 64;
static_assert(StrongBlock::capacity > s_reAdmissionWatermark);
static constexpr unsigned s_retiredNotifyThreshold = StrongBlock::capacity - s_reAdmissionWatermark;
// An empty current block counts against this: it is already warm and serves
// the same purpose as a spare.
static constexpr unsigned s_maxIdleBlocks = 1;
JS_EXPORT_PRIVATE explicit StrongSet(VM&);
JS_EXPORT_PRIVATE ~StrongSet();
VM& vm() { return m_vm; }
static StrongSet* setFor(HandleSlot);
HandleSlot allocate();
static void deallocate(HandleSlot);
DECLARE_VISIT_AGGREGATE;
// Runs with the JSLock held (see the class comment). The functor must not
// allocate or deallocate handles. See forEachSlot.
void forEachStrongHandle(const Invocable<void(JSCell*)> auto&, const HashCountedSet<JSCell*>& skipSet);
unsigned blockCount() const { return m_blockCount; }
// Test-only. Counts the transition s_reAdmissionWatermark exists to keep rare.
unsigned availabilityCount() const { return m_availabilityCount; }
// Test-only. Walks the chain, so a block freed while still linked here shows
// up as a count that outlives it rather than as silent corruption.
unsigned availableBlockCount() const
{
unsigned count = 0;
for (auto it = m_available.begin(); it != m_available.end(); ++it)
++count;
return count;
}
private:
JS_EXPORT_PRIVATE HandleSlot allocateSlow();
JS_EXPORT_PRIVATE void didFreeSlot(StrongBlock*, unsigned usedCount);
void deallocateFromCurrentBlock(StrongBlock*, HandleSlot);
void didBecomeEmpty(StrongBlock*);
void appendAvailable(StrongBlock*);
StrongBlock* takeAvailable();
void installCurrentBlock(StrongBlock*);
void retireCurrentBlock();
void destroyBlock(StrongBlock*);
HandleSlot tryAllocateFromCurrent();
void forEachSlot(const Invocable<void(const HandleSlot&)> auto&);
void forEachLiveCell(const Invocable<void(JSCell*)> auto&);
VM& m_vm;
// The hoisted cursors of m_currentBlock. All three stay null while there is
// no current block, which is what lets the fast path skip a null check on
// m_currentBlock: null cursors compare equal, so the first allocate() of a
// set's life falls through to allocateSlow().
HandleSlot m_bumpCursor { nullptr };
HandleSlot m_bumpEnd { nullptr };
HandleSlot m_freeListHead { nullptr };
StrongBlock* m_currentBlock { nullptr };
// Every block for this set's whole life, walked by marking. A block carries a
// separate embedded node per list, so being on this one does not stop it from
// also being on m_available.
StrongBlock::BlockList m_blocks;
// Blocks with at least s_reAdmissionWatermark free slots, appended at the
// tail so a block crossing the watermark does not displace the block under
// the hot cursor.
StrongBlock::BlockList m_available;
StrongBlock* m_spareBlock { nullptr };
unsigned m_blockCount { 0 };
unsigned m_availabilityCount { 0 };
};
inline StrongSet* StrongSet::setFor(HandleSlot slot)
{
return StrongBlock::blockFor(slot)->strongSet();
}
inline HandleSlot StrongSet::tryAllocateFromCurrent()
{
HandleSlot slot = m_freeListHead;
if (slot)
m_freeListHead = StrongBlock::decodeFreeListEntry(*slot);
else if (m_bumpCursor != m_bumpEnd) [[likely]] {
slot = m_bumpCursor;
m_bumpCursor = slot + 1;
} else [[unlikely]]
return nullptr;
m_currentBlock->incrementUsedCount();
*slot = JSValue();
return slot;
}
inline HandleSlot StrongSet::allocate()
{
if (HandleSlot slot = tryAllocateFromCurrent()) [[likely]]
return slot;
return allocateSlow();
}
// The current block's free list lives in the owning StrongSet, so freeing into
// it has to go through the set. Every other block owns its own free list and its
// own used count, so it is settled here without the set being touched at all --
// which is the whole reason the block carries an isCurrent() bit rather than the
// set being asked to compare against m_currentBlock.
inline void StrongSet::deallocate(HandleSlot slot)
{
StrongBlock* block = StrongBlock::blockFor(slot);
ASSERT(slot >= block->payload() && slot < block->payloadEnd());
if (block->isCurrent()) {
block->strongSet()->deallocateFromCurrentBlock(block, slot);
return;
}
block->pushFreeSlot(slot);
unsigned usedCount = block->decrementUsedCount();
if (usedCount <= block->freeNotifyThreshold()) [[unlikely]]
block->strongSet()->didFreeSlot(block, usedCount);
}
inline void StrongSet::deallocateFromCurrentBlock(StrongBlock* block, HandleSlot slot)
{
ASSERT(block == m_currentBlock);
*slot = StrongBlock::encodeFreeListEntry(m_freeListHead);
m_freeListHead = slot;
// The current block's threshold is always zero, so emptiness is the only
// thing left to notify about and the threshold need not be loaded.
ASSERT(!block->freeNotifyThreshold());
unsigned usedCount = block->decrementUsedCount();
if (!usedCount) [[unlikely]]
didFreeSlot(block, usedCount);
}
// Visits every slot handed out at least once, allocated or not, so consumers
// must filter to cells. The functor must not allocate or deallocate handles:
// deallocating the last live slot of a non-current block destroys that block
// mid-iteration, and the walk would continue into freed memory.
void StrongSet::forEachSlot(const Invocable<void(const HandleSlot&)> auto& functor)
{
for (StrongBlock::BlockListNode& node : m_blocks) {
StrongBlock& block = node.block();
// An empty block holds nothing live, and skipping it also skips the whole
// payload of the spare, whose slots are all free-list links.
if (block.isEmpty())
continue;
// Only the current block can have slots that have never been written, and
// its bump cursor is hoisted here. Any other block was full when it was
// retired, so every slot is either live or a free-list link.
HandleSlot end = &block == m_currentBlock ? m_bumpCursor : block.payloadEnd();
for (HandleSlot slot = block.payload(); slot < end; ++slot)
functor(slot);
}
}
void StrongSet::forEachLiveCell(const Invocable<void(JSCell*)> auto& functor)
{
forEachSlot([&](HandleSlot slot) {
JSValue value = *slot;
if (!value || !value.isCell())
return;
functor(value.asCell());
});
}
void StrongSet::forEachStrongHandle(const Invocable<void(JSCell*)> auto& functor, const HashCountedSet<JSCell*>& skipSet)
{
forEachLiveCell([&](JSCell* cell) {
if (!skipSet.contains(cell))
functor(cell);
});
}
} // namespace JSC
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END