blob: 2bbd824e7f3d103036fc41201413187eff444790 [file] [log] [blame]
/*
* Copyright (C) 2024 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. ``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
* 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 "HashMapHelper.h"
#include "JSImmutableButterfly.h"
#include "JSObject.h"
WTF_ALLOW_UNSAFE_BUFFER_USAGE_BEGIN
namespace JSC {
struct MapTraits {
// DataTable: [ Key_0, Val_0, NextKeyIndexInChain_0, Key_1, Val_1, NextKeyIndexInChain_1, ... ]
// | <------------ Entry_0 -----------> | <------------ Entry_1 -----------> |
static constexpr uint8_t EntrySize = 3;
static constexpr bool hasValueData = true;
};
struct SetTraits {
// DataTable: [ Key_0, NextKeyIndexInChain_0, Key_1, NextKeyIndexInChain_1, ... ]
// | <-------- Entry_0 --------> | <-------- Entry_1 --------> |
static constexpr uint8_t EntrySize = 2;
static constexpr bool hasValueData = false;
};
template<typename Traits>
class OrderedHashTable;
// ################ NonObsolete Table ################
//
// Count Value(s) ValueType WriteBarrier
// -------------------------------------------------------------------------------------------------------------------------------------
// TableStart -> 1 | AliveEntryCount | TableSize | NO
// 1 | DeletedEntryCount | TableSize | NO
// 1 | Capacity | TableSize | NO
// 1 | IterationEntry | Entry | NO
// BucketCount | HashTable: { <BucketIndex, ChainStartKeyIndex>, ... } | <TableIndex, TableIndex> | NO
// TableEnd -> Capacity * EntrySize | DataTable: { <Entry_0, Data_0>, <Entry_1, Data_1>, ... } | <TableIndex, JSValue> | Yes (Key or Value)
//
// ################ Obsolete Table from Rehash ################
//
// Count Value(s) ValueType WriteBarrier
// -------------------------------------------------------------------------------------------------------------------------------------
// TableStart -> 1 | NextTable | JSCell | Yes
// 1 | DeletedEntryCount | TableSize | NO
// DeletedEntryCount | DeletedEntries: [DeletedEntry_i, ...] | Entry | NO
// TableEnd -> ... | NotUsed | ...
//
// ################ Obsolete Table from Clear ################
//
// Count Value(s) ValueType WriteBarrier
// -------------------------------------------------------------------------------------------------------------------------------------
// TableStart -> 1 | NextTable | JSCell | Yes
// 1 | ClearedTableSentinel | TableSize | NO
// TableEnd -> ... | NotUsed | ...
//
//
// This is based on the idea of CloseTable introduced in https://wiki.mozilla.org/User:Jorend/Deterministic_hash_tables.
//
// NonObsolete Table: A CloseTable flattened into a single array.
//
// - AliveEntryCount: The number of alive entries in the table.
// - DeletedEntryCount: The number of deleted entries in the table.
// - Capacity: The capacity of entries in the table.
// - IterationEntry: The temporary index only used for iteration.
// - HashTable: CloseTable is based on closed hashing. So, the each bucket in the HashTable points to the start EntryKeyIndex in the collision chain.
// - FIXME: We can get rid of HashTable by applying OpenTable.
// - DataTable: The actual data table holds the tuples of key, value, and NextKeyIndexInChain for each entry.
//
// Iterator Map/Set
// | |
// v v
// ObsoleteTable_0 --> ObsoleteTable_1 --> NonObsoleteTable_2
//
// Obsolete Table: Each rehash and clear would create a new table and update the obsolete one with additional info. Those info are used to
// transit the iterator (pointing to the obsolete table) to the latest alive table.
//
// - NextTable: A transition from the obsolete tables to the latest-non-obsolete one.
// - DeletedEntries: An array of the previously deleted entries used for updating iterator's index.
// - ClearedTableSentinel: The sentinel to indicate whether the obsolete table is retired due to a clearance.
//
// Note that all elements in the JSImmutableButterfly are in JSValue type. However, only the key and value in the DataTable are real JSValues.
// The others are used as unsigned integers wrapped by JSValue.
template<typename Traits>
class OrderedHashTableHelper {
public:
using Storage = JSImmutableButterfly;
using Helper = OrderedHashTableHelper<Traits>;
using HashTable = OrderedHashTable<Traits>;
using TableSize = uint32_t;
using Entry = TableSize;
using TableIndex = TableSize;
static constexpr TableSize ClearedTableSentinel = -1;
static constexpr TableIndex InvalidTableIndex = -1;
static constexpr uint8_t EntrySize = Traits::EntrySize;
static constexpr uint8_t ChainOffset = Traits::EntrySize - 1;
static constexpr uint8_t LoadFactor = 1;
static constexpr uint8_t InitialCapacity = 8;
static constexpr TableSize LargeCapacity = 2 << 15;
static_assert(EntrySize == MapTraits::EntrySize || EntrySize == SetTraits::EntrySize);
OrderedHashTableHelper() = delete;
ALWAYS_INLINE static TableSize toNumber(JSValue number) { return static_cast<TableSize>(number.asInt32()); }
ALWAYS_INLINE static constexpr TableSize asNumber(Storage& storage, TableIndex index) { return toNumber(get(storage, index)); }
ALWAYS_INLINE static JSValue toJSValue(Entry entry)
{
JSValue value = JSValue();
OrderedHashTableTraits::set(&value, entry);
return value;
}
ALWAYS_INLINE static bool isDeleted(VM& vm, JSValue value) { return value.isCell() && value.asCell() == vm.orderedHashTableDeletedValue(); }
ALWAYS_INLINE static constexpr bool isValidTableIndex(TableIndex index) { return index != InvalidTableIndex; }
ALWAYS_INLINE static JSValue invalidTableIndex() { return toJSValue(InvalidTableIndex); }
ALWAYS_INLINE static JSValue get(Storage& storage, TableIndex index) { return storage.toButterfly()->contiguous().atUnsafe(index).get(); }
ALWAYS_INLINE static JSValue* slot(Storage& storage, TableIndex index) { return storage.toButterfly()->contiguous().atUnsafe(index).slot(); }
ALWAYS_INLINE static void set(Storage& storage, TableIndex index, JSValue value) { return storage.toButterfly()->contiguous().atUnsafe(index).setWithoutWriteBarrier(value); }
ALWAYS_INLINE static void set(Storage& storage, TableIndex index, TableSize number) { OrderedHashTableTraits::set(slot(storage, index), number); }
ALWAYS_INLINE static void setWithWriteBarrier(VM& vm, Storage& storage, TableIndex index, JSValue value) { storage.toButterfly()->contiguous().atUnsafe(index).set(vm, &storage, value); }
/* -------------------------------- AliveEntryCount, DeletedEntryCount, Capacity, and IterationEntry -------------------------------- */
ALWAYS_INLINE static constexpr TableIndex aliveEntryCountIndex() { return 0; }
ALWAYS_INLINE static constexpr TableIndex deletedEntryCountIndex() { return aliveEntryCountIndex() + 1; }
ALWAYS_INLINE static constexpr TableIndex capacityIndex() { return deletedEntryCountIndex() + 1; }
ALWAYS_INLINE static constexpr TableIndex iterationEntryIndex() { return capacityIndex() + 1; }
ALWAYS_INLINE static constexpr TableSize aliveEntryCount(Storage& storage) { return asNumber(storage, aliveEntryCountIndex()); }
ALWAYS_INLINE static constexpr TableSize deletedEntryCount(Storage& storage) { return asNumber(storage, deletedEntryCountIndex()); }
ALWAYS_INLINE static constexpr TableSize usedCapacity(Storage& storage) { return aliveEntryCount(storage) + deletedEntryCount(storage); }
ALWAYS_INLINE static constexpr TableSize capacity(Storage& storage) { return asNumber(storage, capacityIndex()); }
ALWAYS_INLINE static constexpr TableSize iterationEntry(Storage& storage) { return asNumber(storage, iterationEntryIndex()); }
ALWAYS_INLINE static constexpr void incrementAliveEntryCount(Storage& storage) { OrderedHashTableTraits::increment(slot(storage, aliveEntryCountIndex())); }
ALWAYS_INLINE static constexpr void decrementAliveEntryCount(Storage& storage) { OrderedHashTableTraits::decrement(slot(storage, aliveEntryCountIndex())); }
ALWAYS_INLINE static constexpr void incrementDeletedEntryCount(Storage& storage) { OrderedHashTableTraits::increment(slot(storage, deletedEntryCountIndex())); }
/* -------------------------------- Hash table -------------------------------- */
ALWAYS_INLINE static constexpr TableSize bucketCount(TableSize capacity) { return capacity / LoadFactor; }
ALWAYS_INLINE static constexpr TableIndex hashTableStartIndex() { return iterationEntryIndex() + 1; }
ALWAYS_INLINE static constexpr TableIndex hashTableEndIndex(TableSize capacity) { return hashTableStartIndex() + bucketCount(capacity) - 1; }
ALWAYS_INLINE static constexpr TableIndex bucketIndex(TableIndex hashTableStartIndex, TableSize bucketCount, TableSize hash) { return hashTableStartIndex + (hash & (bucketCount - 1)); }
ALWAYS_INLINE static constexpr TableIndex bucketIndex(TableSize capacity, uint32_t hash) { return bucketIndex(hashTableStartIndex(), bucketCount(capacity), hash); }
/* -------------------------------- Data table -------------------------------- */
ALWAYS_INLINE static constexpr TableSize dataTableSize(TableSize capacity) { return capacity * EntrySize; }
ALWAYS_INLINE static constexpr TableIndex dataTableStartIndex(TableSize capacity) { return hashTableEndIndex(capacity) + 1; }
ALWAYS_INLINE static constexpr TableIndex dataTableEndIndex(TableSize capacity) { return dataTableStartIndex(capacity) + dataTableSize(capacity) - 1; }
ALWAYS_INLINE static constexpr TableIndex entryDataStartIndex(TableIndex dataTableStartIndex, Entry entry) { return dataTableStartIndex + entry * EntrySize; }
ALWAYS_INLINE static void setKeyOrValueData(VM& vm, Storage& storage, TableIndex index, JSValue value) { setWithWriteBarrier(vm, storage, index, value); }
ALWAYS_INLINE static constexpr void deleteData(VM& vm, Storage& storage, TableIndex index) { return set(storage, index, vm.orderedHashTableDeletedValue()); }
ALWAYS_INLINE static void addToChain(Storage& storage, TableIndex bucketIndex, TableIndex newChainStartKeyIndex)
{
JSValue prevChainStartKeyIndex = get(storage, bucketIndex);
set(storage, bucketIndex, newChainStartKeyIndex);
set(storage, newChainStartKeyIndex + ChainOffset, prevChainStartKeyIndex);
}
/* -------------------------------- OrderedHashTable -------------------------------- */
ALWAYS_INLINE static constexpr TableSize tableSize(TableSize capacity)
{
TableSize result = 4 /* AliveEntryCount, DeletedEntryCount, Capacity, and IterationEntry */
+ capacity / LoadFactor /* BucketCount */
+ capacity * EntrySize; /* DataTableSize */
ASSERT(result == tableSizeSlow(capacity));
return result;
}
ALWAYS_INLINE static constexpr TableSize tableSizeSlow(TableSize capacity) { return dataTableEndIndex(capacity) + 1; }
/* -------------------------------- Obsolete table -------------------------------- */
ALWAYS_INLINE static constexpr bool isObsolete(Storage& storage) { return !!nextTable(storage); }
ALWAYS_INLINE static constexpr Storage* nextTable(Storage& storage)
{
JSValue* value = slot(storage, aliveEntryCountIndex());
if (value->isCell()) {
ASSERT(jsDynamicCast<Storage*>(*value));
return jsCast<Storage*>(*value);
}
return nullptr;
}
ALWAYS_INLINE static constexpr void setNextTable(VM& vm, Storage& storage, Storage* next) { setWithWriteBarrier(vm, storage, aliveEntryCountIndex(), next); }
ALWAYS_INLINE static constexpr TableIndex deletedEntriesStartIndex() { return capacityIndex(); }
ALWAYS_INLINE static constexpr bool isCleared(Storage& storage) { return asNumber(storage, deletedEntryCountIndex()) == ClearedTableSentinel; }
ALWAYS_INLINE static constexpr void setClearedTableSentinel(Storage& storage) { set(storage, deletedEntryCountIndex(), ClearedTableSentinel); }
/* ------------------------------------------------------------------------------------ */
ALWAYS_INLINE static Storage* tryCreate(VM& vm, int length)
{
return Storage::tryCreate(vm, vm.immutableButterflyStructure(CopyOnWriteArrayWithContiguous), length);
}
ALWAYS_INLINE static Storage* tryCreate(JSGlobalObject* globalObject, TableSize aliveEntryCount = 0, TableSize deletedEntryCount = 0, TableSize capacity = InitialCapacity)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(!(capacity & (capacity - 1)));
TableSize length = tableSize(capacity);
if (length > IndexingHeader::maximumLength) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}
Storage* storage = tryCreate(vm, length);
if (!storage) [[unlikely]] {
throwOutOfMemoryError(globalObject, scope);
return nullptr;
}
Storage& storageRef = *storage;
set(storageRef, aliveEntryCountIndex(), aliveEntryCount);
set(storageRef, deletedEntryCountIndex(), deletedEntryCount);
set(storageRef, capacityIndex(), capacity);
return storage;
}
enum class UpdateDeletedEntries : uint8_t {
Yes,
No
};
template<UpdateDeletedEntries update = UpdateDeletedEntries::No>
ALWAYS_INLINE static Storage* copyImpl(JSGlobalObject* globalObject, Storage& base, TableSize newCapacity)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
TableSize baseCapacity = capacity(base);
TableSize baseAliveEntryCount = aliveEntryCount(base);
TableSize baseUsedCapacity = usedCapacity(base);
ASSERT(!isObsolete(base));
ASSERT_UNUSED(baseAliveEntryCount, newCapacity >= std::max(static_cast<TableSize>(InitialCapacity), baseAliveEntryCount));
ASSERT_UNUSED(baseCapacity, baseUsedCapacity <= baseCapacity);
Storage* copy = tryCreate(globalObject, baseAliveEntryCount, 0, newCapacity);
RETURN_IF_EXCEPTION(scope, nullptr);
TableIndex baseEntryKeyIndex = dataTableStartIndex(baseCapacity) - EntrySize;
TableIndex baseDeletedEntriesIndex = deletedEntriesStartIndex();
Storage& copyRef = *copy;
TableIndex newEntryKeyIndex = dataTableStartIndex(newCapacity) - EntrySize;
TableIndex newHashTableStartIndex = hashTableStartIndex();
TableIndex newBucketCount = bucketCount(newCapacity);
for (Entry baseEntry = 0; baseEntry < baseUsedCapacity; ++baseEntry) {
baseEntryKeyIndex += EntrySize;
JSValue baseKey = get(base, baseEntryKeyIndex);
ASSERT(!baseKey.isEmpty());
// Step 1: Copy DataTable only for the alive entries.
if (isDeleted(vm, baseKey)) {
if constexpr (update == UpdateDeletedEntries::Yes)
set(base, baseDeletedEntriesIndex++, baseEntry);
continue;
}
// Step 2: Copy the key and value from the base table to the new table.
newEntryKeyIndex += EntrySize;
setKeyOrValueData(vm, copyRef, newEntryKeyIndex, baseKey);
if constexpr (Traits::hasValueData) {
JSValue baseValue = get(base, baseEntryKeyIndex + 1);
setKeyOrValueData(vm, copyRef, newEntryKeyIndex + 1, baseValue);
}
// Step 3: Compute for the hash value and add to the chain in the new table. Note that the
// key stored in the base table is already normalized.
TableSize hash = jsMapHash(globalObject, vm, baseKey);
RETURN_IF_EXCEPTION(scope, nullptr);
TableIndex newBucketIndex = bucketIndex(newHashTableStartIndex, newBucketCount, hash);
addToChain(copyRef, newBucketIndex, newEntryKeyIndex);
}
return copy;
}
ALWAYS_INLINE static Storage* copy(JSGlobalObject* globalObject, Storage& base)
{
return copyImpl<>(globalObject, base, capacity(base));
}
enum class UpdateOwnerStorage : uint8_t {
Yes,
No
};
template<UpdateOwnerStorage update>
ALWAYS_INLINE static Storage* rehash(JSGlobalObject* globalObject, HashTable* owner, Storage& base, TableSize newCapacity)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
Storage* copy = copyImpl<UpdateDeletedEntries::Yes>(globalObject, base, newCapacity);
RETURN_IF_EXCEPTION(scope, nullptr);
setNextTable(vm, base, copy);
if constexpr (update == UpdateOwnerStorage::Yes)
owner->m_storage.set(vm, owner, copy);
return copy;
}
ALWAYS_INLINE static void clear(JSGlobalObject* globalObject, HashTable* owner, Storage& base)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
Storage* next = tryCreate(globalObject);
RETURN_IF_EXCEPTION(scope, void());
setClearedTableSentinel(base);
setNextTable(vm, base, next);
owner->m_storage.set(vm, owner, next);
}
struct FindResult {
JSValue normalizedKey;
uint32_t hash;
TableIndex bucketIndex;
TableIndex entryKeyIndex;
// The keyIndex and keySlot may be redundant here. Let's leave it for now
// since the following OpenTable patch can get rid of most of index related info for us.
JSValue* entryKeySlot;
};
ALWAYS_INLINE static FindResult find(JSGlobalObject* globalObject, Storage& storage, JSValue key)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(!isObsolete(storage));
if (!aliveEntryCount(storage))
return { JSValue(), 0, InvalidTableIndex, InvalidTableIndex, nullptr };
key = normalizeMapKey(key);
TableSize hash = jsMapHash(globalObject, vm, key);
RETURN_IF_EXCEPTION(scope, { });
return find(globalObject, storage, key, hash);
}
ALWAYS_INLINE static FindResult find(JSGlobalObject* globalObject, Storage& storage, JSValue normalizedKey, TableSize hash)
{
VM& vm = getVM(globalObject);
TableIndex bucketIndex = Helper::bucketIndex(capacity(storage), hash);
ASSERT(!isObsolete(storage) && normalizeMapKey(normalizedKey) == normalizedKey);
JSValue keyIndexValue = get(storage, bucketIndex);
while (!keyIndexValue.isEmpty()) {
TableIndex entryKeyIndex = toNumber(keyIndexValue);
JSValue* entryKeySlot = slot(storage, entryKeyIndex);
// Fixme: Maybe we can compress the searching path by updating the chain with non-deleted entry.
if (!isDeleted(vm, *entryKeySlot) && areKeysEqual(globalObject, normalizedKey, *entryKeySlot))
return { normalizedKey, hash, bucketIndex, entryKeyIndex, entryKeySlot };
keyIndexValue = get(storage, entryKeyIndex + ChainOffset);
}
return { normalizedKey, hash, bucketIndex, InvalidTableIndex, nullptr };
}
ALWAYS_INLINE static Storage* expandIfNeeded(JSGlobalObject* globalObject, HashTable* owner, Storage& base)
{
ASSERT(!isObsolete(base));
TableSize capacity = Helper::capacity(base);
TableSize deletedEntryCount = Helper::deletedEntryCount(base);
TableSize usedCapacity = Helper::aliveEntryCount(base) + deletedEntryCount;
bool isSmallCapacity = capacity < LargeCapacity;
TableSize expandFactor = isSmallCapacity ? 2 : 1;
if (isSmallCapacity) {
if (usedCapacity < (capacity >> 1))
return &base;
} else {
if (usedCapacity < ((capacity >> 2) * 3))
return &base;
}
TableSize newCapacity = Checked<TableSize>(capacity) << expandFactor;
if (deletedEntryCount >= (capacity >> 1))
newCapacity = capacity; // No need to expanded. Just clear the deleted entries.
return rehash<UpdateOwnerStorage::No>(globalObject, owner, base, newCapacity);
}
template<typename FindKeyFunctor>
ALWAYS_INLINE static void addImpl(JSGlobalObject* globalObject, HashTable* owner, Storage& base, JSValue key, JSValue value, const FindKeyFunctor& findKeyFunctor)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(!isObsolete(base));
auto result = findKeyFunctor();
RETURN_IF_EXCEPTION(scope, void());
if (isValidTableIndex(result.entryKeyIndex)) {
if constexpr (Traits::hasValueData)
setKeyOrValueData(vm, base, result.entryKeyIndex + 1, value);
return;
}
scope.release();
addImpl(globalObject, owner, base, key, value, result);
}
ALWAYS_INLINE static void addImpl(JSGlobalObject* globalObject, HashTable* owner, Storage& base, JSValue key, JSValue value, FindResult& result)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(!isObsolete(base));
ASSERT(!isValidTableIndex(result.entryKeyIndex));
Storage* candidate = expandIfNeeded(globalObject, owner, base);
RETURN_IF_EXCEPTION(scope, void());
Storage& candidateRef = *candidate;
TableSize capacity = Helper::capacity(candidateRef);
Entry newEntry = usedCapacity(candidateRef);
TableIndex newEntryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), newEntry);
incrementAliveEntryCount(candidateRef);
bool firstAliveEntry = result.normalizedKey.isEmpty();
if (firstAliveEntry) [[unlikely]] {
result.normalizedKey = normalizeMapKey(key);
result.hash = jsMapHash(globalObject, vm, result.normalizedKey);
RETURN_IF_EXCEPTION(scope, void());
}
bool rehashed = &base != candidate;
if (rehashed || firstAliveEntry) [[unlikely]]
result.bucketIndex = bucketIndex(capacity, result.hash);
addToChain(candidateRef, result.bucketIndex, newEntryKeyIndex);
setKeyOrValueData(vm, candidateRef, newEntryKeyIndex, result.normalizedKey);
if constexpr (Traits::hasValueData)
setKeyOrValueData(vm, candidateRef, newEntryKeyIndex + 1, value);
if (rehashed) [[unlikely]]
owner->m_storage.set(vm, owner, candidate);
}
ALWAYS_INLINE static void add(JSGlobalObject* globalObject, HashTable* owner, Storage& storage, JSValue key, JSValue value)
{
addImpl(globalObject, owner, storage, key, value, [&]() ALWAYS_INLINE_LAMBDA {
return find(globalObject, storage, key);
});
}
ALWAYS_INLINE static void addNormalized(JSGlobalObject* globalObject, HashTable* owner, Storage& storage, JSValue normalizedKey, JSValue value, TableSize hash)
{
ASSERT(normalizeMapKey(normalizedKey) == normalizedKey);
addImpl(globalObject, owner, storage, normalizedKey, value, [&]() ALWAYS_INLINE_LAMBDA {
return find(globalObject, storage, normalizedKey, hash);
});
}
ALWAYS_INLINE static void shrinkIfNeeded(JSGlobalObject* globalObject, HashTable* owner, Storage& base)
{
ASSERT(!isObsolete(base));
TableSize capacity = Helper::capacity(base);
TableSize aliveEntryCount = Helper::aliveEntryCount(base);
if (aliveEntryCount >= (capacity >> 3))
return;
if (capacity == InitialCapacity)
return;
rehash<UpdateOwnerStorage::Yes>(globalObject, owner, base, capacity >> 1);
}
template<typename FindKeyFunctor>
ALWAYS_INLINE static bool removeImpl(JSGlobalObject* globalObject, HashTable* owner, Storage& storage, const FindKeyFunctor& findKeyFunctor)
{
VM& vm = getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
ASSERT(!isObsolete(storage));
auto result = findKeyFunctor();
RETURN_IF_EXCEPTION(scope, false);
if (!isValidTableIndex(result.entryKeyIndex))
return false;
deleteData(vm, storage, result.entryKeyIndex);
if constexpr (Traits::hasValueData)
deleteData(vm, storage, result.entryKeyIndex + 1);
incrementDeletedEntryCount(storage);
decrementAliveEntryCount(storage);
scope.release();
shrinkIfNeeded(globalObject, owner, storage);
return true;
}
ALWAYS_INLINE static bool remove(JSGlobalObject* globalObject, HashTable* owner, Storage& storage, JSValue key)
{
return removeImpl(globalObject, owner, storage, [&]() ALWAYS_INLINE_LAMBDA {
return find(globalObject, storage, key);
});
}
ALWAYS_INLINE static bool removeNormalized(JSGlobalObject* globalObject, HashTable* owner, Storage& storage, JSValue normalizedKey, TableSize hash)
{
ASSERT(normalizeMapKey(normalizedKey) == normalizedKey);
return removeImpl(globalObject, owner, storage, [&]() ALWAYS_INLINE_LAMBDA {
return find(globalObject, storage, normalizedKey, hash);
});
}
template<typename Functor>
ALWAYS_INLINE static Storage& transit(Storage& storage, const Functor& functor)
{
Storage* ptr = &storage;
while (isObsolete(*ptr)) {
functor(*ptr);
ptr = nextTable(*ptr);
}
return *ptr;
}
struct TransitionResult {
Storage* storage;
Entry entry;
JSValue key;
JSValue value;
};
ALWAYS_INLINE static TransitionResult transitAndNext(VM& vm, Storage& storage, Entry from)
{
Storage& candidate = transit(storage, [&](Storage& obsolete) ALWAYS_INLINE_LAMBDA {
if (!from)
return;
if (isCleared(obsolete)) {
from = 0;
return;
}
TableIndex deletedEntryCount = Helper::deletedEntryCount(obsolete);
if (!deletedEntryCount)
return;
TableIndex start = deletedEntriesStartIndex();
TableIndex end = start + deletedEntryCount;
Entry fromCopy = from;
for (TableIndex i = start; i < end; ++i) {
Entry deletedEntry = toNumber(get(obsolete, i));
if (deletedEntry >= fromCopy)
break;
--from;
}
});
ASSERT(!isObsolete(candidate));
TableSize capacity = Helper::capacity(candidate);
TableSize usedCapacity = Helper::usedCapacity(candidate);
TableIndex entryKeyIndex = entryDataStartIndex(dataTableStartIndex(capacity), from) - EntrySize;
for (Entry entry = from; entry < usedCapacity; ++entry) {
entryKeyIndex += EntrySize;
JSValue key = get(candidate, entryKeyIndex);
if (isDeleted(vm, key))
continue;
JSValue value;
if constexpr (Traits::hasValueData)
value = get(candidate, entryKeyIndex + 1);
return { &candidate, entry, key, value };
}
return { };
}
ALWAYS_INLINE static JSValue getKey(Storage& storage, Entry entry)
{
ASSERT(!isObsolete(storage));
return get(storage, entryDataStartIndex(dataTableStartIndex(capacity(storage)), entry));
}
ALWAYS_INLINE static JSValue getValue(Storage& storage, Entry entry)
{
ASSERT(!isObsolete(storage) && EntrySize == 3);
return get(storage, entryDataStartIndex(dataTableStartIndex(capacity(storage)), entry) + 1);
}
ALWAYS_INLINE static JSCell* nextAndUpdateIterationEntry(VM& vm, Storage& storage, Entry entry)
{
auto result = transitAndNext(vm, storage, entry);
if (!result.storage)
return vm.orderedHashTableSentinel();
set(*result.storage, iterationEntryIndex(), result.entry);
return result.storage;
}
ALWAYS_INLINE static JSValue getIterationEntry(Storage& storage) { return toJSValue(iterationEntry(storage)); }
ALWAYS_INLINE static JSValue getIterationEntryKey(Storage& storage) { return getKey(storage, iterationEntry(storage)); }
ALWAYS_INLINE static JSValue getIterationEntryValue(Storage& storage) { return getValue(storage, iterationEntry(storage)); }
};
} // namespace JSC
WTF_ALLOW_UNSAFE_BUFFER_USAGE_END