blob: e1ca50cc313f7f2607636c326ad915ac532990cd [file] [edit]
/*
* 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. ``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.
*/
#include "config.h"
#include <wtf/LayeredHashMap.h>
#include <utility>
#include <wtf/Vector.h>
namespace TestWebKitAPI {
namespace {
struct TestKey {
unsigned value { 0 };
bool operator==(const TestKey& other) const { return value == other.value; }
// Deliberately collision-heavy, so that entries pile into shared probe runs and the
// ordering the map has to maintain across a rehash actually gets exercised.
unsigned hash() const { return value & 0x7; }
};
// Reserving UINT_MAX rather than the default-constructed key leaves zero usable below, and
// makes the map answer to traits it did not choose for itself.
struct TestKeyTraits : HashTraits<TestKey> {
static TestKey emptyValue() { return TestKey { UINT_MAX }; }
};
using TestMap = LayeredHashMap<TestKey, unsigned, DefaultHash<TestKey>, TestKeyTraits>;
// Enough keys to force several rehashes out of the smallest table.
constexpr unsigned keysPerLayer = 40;
constexpr unsigned keysPerSibling = 16;
} // anonymous namespace
TEST(WTF_LayeredHashMap, NestedLayers)
{
constexpr unsigned layerCount = 6;
TestMap map;
for (unsigned layer = 0; layer < layerCount; ++layer) {
map.startLayer();
for (unsigned i = 0; i < keysPerLayer; ++i) {
unsigned key = layer * keysPerLayer + i;
EXPECT_FALSE(map.find(TestKey { key }));
map.add(TestKey { key }, key * 3);
}
// Everything inserted so far, in this layer and every enclosing one, is still visible.
for (unsigned key = 0; key <= layer * keysPerLayer + keysPerLayer - 1; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key * 3);
}
// Dropping a layer removes exactly that layer's keys and leaves the enclosing ones findable,
// which is the property that breaks if a rehash reorders entries within a probe run.
for (unsigned layer = layerCount; layer--;) {
map.dropLastLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_FALSE(map.find(TestKey { layer * keysPerLayer + i }));
for (unsigned key = 0; key < layer * keysPerLayer; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key * 3);
}
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, AddDoesNotUpdate)
{
TestMap map;
map.startLayer();
map.add(TestKey { 1 }, 11);
map.clearEntries();
EXPECT_FALSE(map.find(TestKey { 1 }));
EXPECT_EQ(map.layerCount(), 1U);
map.add(TestKey { 1 }, 12);
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 12U);
// add() leaves a present entry alone.
EXPECT_FALSE(map.add(TestKey { 1 }, 13));
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 12U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 1 }));
}
TEST(WTF_LayeredHashMap, FindOrAdd)
{
// findOrAdd answers with the entry it left in place, and with nothing when it was the one to
// put the entry there.
TestMap map;
map.startLayer();
EXPECT_FALSE(map.findOrAdd(TestKey { 5 }, 50));
EXPECT_EQ(map.findOrAdd(TestKey { 5 }, 60).value_or(0), 50U);
EXPECT_EQ(map.find(TestKey { 5 }).value_or(0), 50U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 5 }));
}
TEST(WTF_LayeredHashMap, SiblingLayers)
{
// A tree walk pushes and pops at the same depth once per sibling, so a layer is opened over a
// table that a drop has already punched holes in, and the rehashes that matter happen after
// those drops rather than during a monotone run of pushes. Keep one enclosing layer's keys
// live throughout to catch a rehash that loses them.
constexpr unsigned siblingCount = 24;
constexpr unsigned enclosingKeyBase = 1000000;
TestMap map;
map.startLayer();
for (unsigned i = 0; i < keysPerSibling; ++i)
map.add(TestKey { enclosingKeyBase + i }, i);
for (unsigned sibling = 0; sibling < siblingCount; ++sibling) {
map.startLayer();
for (unsigned i = 0; i < keysPerSibling; ++i) {
unsigned key = sibling * keysPerSibling + i;
EXPECT_FALSE(map.find(TestKey { key }));
EXPECT_TRUE(map.add(TestKey { key }, key * 5));
}
for (unsigned i = 0; i < keysPerSibling; ++i) {
unsigned key = sibling * keysPerSibling + i;
EXPECT_EQ(map.find(TestKey { key }).value_or(1), key * 5);
EXPECT_EQ(map.find(TestKey { enclosingKeyBase + i }).value_or(~0U), i);
}
map.dropLastLayer();
// The sibling that just ended is gone, and every earlier sibling stayed gone.
for (unsigned earlier = 0; earlier <= sibling; ++earlier) {
for (unsigned i = 0; i < keysPerSibling; ++i)
EXPECT_FALSE(map.find(TestKey { earlier * keysPerSibling + i }));
}
for (unsigned i = 0; i < keysPerSibling; ++i)
EXPECT_EQ(map.find(TestKey { enclosingKeyBase + i }).value_or(~0U), i);
}
map.dropLastLayer();
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, ClearEntriesKeepsLayers)
{
// Clearing from a layer nested inside others that are still holding entries removes every
// entry but keeps the layers, and what comes after still unwinds to an empty map.
constexpr unsigned layerCount = 4;
TestMap map;
for (unsigned layer = 0; layer < layerCount; ++layer) {
map.startLayer();
for (unsigned i = 0; i < keysPerSibling; ++i)
EXPECT_TRUE(map.add(TestKey { layer * keysPerSibling + i }, layer + 1));
}
map.clearEntries();
EXPECT_EQ(map.layerCount(), layerCount);
for (unsigned key = 0; key < layerCount * keysPerSibling; ++key)
EXPECT_FALSE(map.find(TestKey { key }));
// Enough refilling to rehash over the region the clear emptied, then unwound one layer at a
// time.
map.startLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_TRUE(map.add(TestKey { i }, i + 100));
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_EQ(map.find(TestKey { i }).value_or(0), i + 100);
while (map.layerCount())
map.dropLastLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_FALSE(map.find(TestKey { i }));
}
TEST(WTF_LayeredHashMap, RandomizedAgainstModel)
{
// A randomized walk cross-checked against a plain list-of-layers model. The other tests fix
// the shape of the collisions; varying how pushes, pops, inserts, and overlays interleave is
// what decides where a drop punches a hole into a probe run that a later lookup has to cross.
constexpr unsigned keyCount = 64;
TestMap map;
Vector<Vector<std::pair<unsigned, unsigned>>> model;
uint32_t randomState = 20260918;
auto nextRandom = [&] {
randomState = randomState * 1664525 + 1013904223;
return randomState >> 8;
};
auto modelFind = [&](unsigned key) -> unsigned {
for (unsigned layer = model.size(); layer--;) {
for (unsigned i = model[layer].size(); i--;) {
if (model[layer][i].first == key)
return model[layer][i].second;
}
}
return 0;
};
auto checkAgainstModel = [&] {
EXPECT_EQ(map.layerCount(), model.size());
for (unsigned key = 0; key < keyCount; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), modelFind(key));
};
for (unsigned step = 0; step < 2000; ++step) {
unsigned action = nextRandom() % 16;
if (model.isEmpty() || !action) {
model.constructAndAppend();
map.startLayer();
} else if (action == 1) {
model.removeLast();
map.dropLastLayer();
} else if (action == 2 && !(nextRandom() % 4)) {
for (auto& layer : model)
layer.shrink(0);
map.clearEntries();
} else if (action == 3 && !(nextRandom() % 8)) {
model.clear();
map.clear();
} else if (action < 6) {
unsigned key = nextRandom() % keyCount;
unsigned value = step + 1;
map.overlay(TestKey { key }, value);
model.last().append({ key, value });
} else {
unsigned key = nextRandom() % keyCount;
unsigned value = step + 1;
unsigned existing = modelFind(key);
EXPECT_EQ(map.findOrAdd(TestKey { key }, value).value_or(0), existing);
if (!existing)
model.last().append({ key, value });
}
checkAgainstModel();
}
// Unwind what the walk above left open, which is the one drop sequence a deep interleave of
// pushes, pops and clears never reaches on its own.
while (!model.isEmpty()) {
model.removeLast();
map.dropLastLayer();
checkAgainstModel();
}
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, OverlayShadowsEnclosingLayer)
{
TestMap map;
map.startLayer();
EXPECT_TRUE(map.add(TestKey { 1 }, 10));
map.startLayer();
map.overlay(TestKey { 1 }, 20);
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 20U);
EXPECT_EQ(map.findOrAdd(TestKey { 1 }, 30).value_or(0), 20U);
EXPECT_FALSE(map.add(TestKey { 1 }, 30));
map.startLayer();
map.overlay(TestKey { 1 }, 40);
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 40U);
map.dropLastLayer();
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 20U);
map.dropLastLayer();
EXPECT_EQ(map.find(TestKey { 1 }).value_or(0), 10U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 1 }));
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, OverlayWithinOneLayer)
{
// Overlaying the same key repeatedly in one layer keeps only the newest value visible, and one
// drop undoes all of them, including the one that introduced the key.
TestMap map;
map.startLayer();
EXPECT_TRUE(map.add(TestKey { 2 }, 1));
map.startLayer();
map.overlay(TestKey { 3 }, 100);
for (unsigned value = 2; value <= 10; ++value) {
map.overlay(TestKey { 2 }, value);
map.overlay(TestKey { 3 }, value + 100);
EXPECT_EQ(map.find(TestKey { 2 }).value_or(0), value);
EXPECT_EQ(map.find(TestKey { 3 }).value_or(0), value + 100);
}
map.dropLastLayer();
EXPECT_EQ(map.find(TestKey { 2 }).value_or(0), 1U);
EXPECT_FALSE(map.find(TestKey { 3 }));
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 2 }));
}
TEST(WTF_LayeredHashMap, OverlayInLayerThatAddedKey)
{
TestMap map;
map.startLayer();
EXPECT_TRUE(map.add(TestKey { 6 }, 1));
map.overlay(TestKey { 6 }, 2);
EXPECT_EQ(map.find(TestKey { 6 }).value_or(0), 2U);
map.startLayer();
EXPECT_TRUE(map.add(TestKey { 7 }, 3));
map.overlay(TestKey { 7 }, 4);
map.overlay(TestKey { 6 }, 5);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 7 }));
EXPECT_EQ(map.find(TestKey { 6 }).value_or(0), 2U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 6 }));
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, OverlayAcrossRehash)
{
// Every layer overlays every key of the layers below it before adding keys of its own, so the
// rehashes those additions force have to rebuild slots holding a stack of shadowed entries.
constexpr unsigned layerCount = 5;
TestMap map;
for (unsigned layer = 0; layer < layerCount; ++layer) {
map.startLayer();
for (unsigned key = 0; key < layer * keysPerLayer; ++key)
map.overlay(TestKey { key }, key + layer * 1000);
for (unsigned i = 0; i < keysPerLayer; ++i) {
unsigned key = layer * keysPerLayer + i;
EXPECT_TRUE(map.add(TestKey { key }, key + layer * 1000));
}
for (unsigned key = 0; key < (layer + 1) * keysPerLayer; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key + layer * 1000);
}
for (unsigned layer = layerCount; layer--;) {
map.dropLastLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_FALSE(map.find(TestKey { layer * keysPerLayer + i }));
if (!layer)
break;
for (unsigned key = 0; key < layer * keysPerLayer; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key + (layer - 1) * 1000);
}
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, OverlayOfNewKeyRehashes)
{
// overlay() of keys no layer holds yet is what grows the table here, while the enclosing
// layer's keys sit shadowed in the same probe runs.
TestMap map;
map.startLayer();
for (unsigned i = 0; i < keysPerSibling; ++i)
EXPECT_TRUE(map.add(TestKey { i }, i + 1));
map.startLayer();
for (unsigned i = 0; i < keysPerSibling; ++i)
map.overlay(TestKey { i }, i + 100);
for (unsigned key = keysPerSibling; key < keysPerSibling + keysPerLayer; ++key) {
EXPECT_FALSE(map.find(TestKey { key }));
map.overlay(TestKey { key }, key + 100);
}
for (unsigned key = 0; key < keysPerSibling + keysPerLayer; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key + 100);
map.dropLastLayer();
for (unsigned i = 0; i < keysPerSibling; ++i)
EXPECT_EQ(map.find(TestKey { i }).value_or(0), i + 1);
for (unsigned key = keysPerSibling; key < keysPerSibling + keysPerLayer; ++key)
EXPECT_FALSE(map.find(TestKey { key }));
map.dropLastLayer();
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, ClearEntriesDropsOverlays)
{
TestMap map;
map.startLayer();
EXPECT_TRUE(map.add(TestKey { 4 }, 1));
map.startLayer();
map.overlay(TestKey { 4 }, 2);
map.clearEntries();
EXPECT_EQ(map.layerCount(), 2U);
EXPECT_FALSE(map.find(TestKey { 4 }));
map.overlay(TestKey { 4 }, 3);
EXPECT_EQ(map.find(TestKey { 4 }).value_or(0), 3U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 4 }));
EXPECT_TRUE(map.add(TestKey { 4 }, 5));
EXPECT_EQ(map.find(TestKey { 4 }).value_or(0), 5U);
map.dropLastLayer();
EXPECT_FALSE(map.find(TestKey { 4 }));
}
TEST(WTF_LayeredHashMap, DefaultTraits)
{
// With the default traits the default-constructed key is the reserved empty one, so the keys
// below all avoid it.
LayeredHashMap<TestKey, unsigned> map;
map.startLayer();
for (unsigned key = 1; key <= 64; ++key)
EXPECT_TRUE(map.add(TestKey { key }, key * 7));
for (unsigned key = 1; key <= 64; ++key)
EXPECT_EQ(map.find(TestKey { key }).value_or(0), key * 7);
map.dropLastLayer();
EXPECT_EQ(map.layerCount(), 0U);
}
TEST(WTF_LayeredHashMap, Clear)
{
constexpr unsigned layerCount = 3;
TestMap map;
for (unsigned layer = 0; layer < layerCount; ++layer) {
map.startLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_TRUE(map.add(TestKey { layer * keysPerLayer + i }, i + 1));
}
map.clear();
EXPECT_EQ(map.layerCount(), 0U);
for (unsigned key = 0; key < layerCount * keysPerLayer; ++key)
EXPECT_FALSE(map.find(TestKey { key }));
map.startLayer();
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_TRUE(map.add(TestKey { i }, i + 200));
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_EQ(map.find(TestKey { i }).value_or(0), i + 200);
map.dropLastLayer();
EXPECT_EQ(map.layerCount(), 0U);
for (unsigned i = 0; i < keysPerLayer; ++i)
EXPECT_FALSE(map.find(TestKey { i }));
}
TEST(WTF_LayeredHashMap, WellDistributedHash)
{
// TestKey's hash leaves every bit above the lowest three clear, so only a hash that spreads
// across the whole table exercises the initial-slot masking as the table grows.
constexpr unsigned layerCount = 4;
constexpr unsigned keysPerSpreadLayer = 1000;
LayeredHashMap<unsigned, unsigned> map;
for (unsigned layer = 0; layer < layerCount; ++layer) {
map.startLayer();
for (unsigned i = 0; i < keysPerSpreadLayer; ++i) {
unsigned key = layer * keysPerSpreadLayer + i + 1;
EXPECT_FALSE(map.find(key));
EXPECT_TRUE(map.add(key, key * 3));
}
for (unsigned key = 1; key <= (layer + 1) * keysPerSpreadLayer; ++key)
EXPECT_EQ(map.find(key).value_or(0), key * 3);
}
for (unsigned layer = layerCount; layer--;) {
map.dropLastLayer();
for (unsigned i = 0; i < keysPerSpreadLayer; ++i)
EXPECT_FALSE(map.find(layer * keysPerSpreadLayer + i + 1));
for (unsigned key = 1; key <= layer * keysPerSpreadLayer; ++key)
EXPECT_EQ(map.find(key).value_or(0), key * 3);
}
EXPECT_EQ(map.layerCount(), 0U);
}
} // namespace TestWebKitAPI