blob: 67f9e0702bb79141cad392059546107c4caeff28 [file] [edit]
//
// Copyright 2018 The ANGLE Project Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// ResourceMap_unittest:
// Unit tests for the ResourceMap template class.
//
#include <gtest/gtest.h>
#include <map>
#include "libANGLE/ResourceMap.h"
using namespace gl;
namespace gl
{
template <>
inline GLuint GetIDValue(int id)
{
return id;
}
template <>
inline GLuint GetIDValue(unsigned int id)
{
return id;
}
template <>
inline GLuint GetIDValue(uint16_t id)
{
return id;
}
// For the purpose of unit testing, |int| is considered private (not needing lock), |unsigned int|
// is considered shared (needing lock), and |uint16_t| additionally has a small flat array to
// exercise all three storage levels.
template <>
struct ResourceMapParams<unsigned int>
{
static constexpr size_t kInitialFlatResourcesSize = 0xC0;
static constexpr size_t kAdditionalFlatResourcesSize = 0;
static constexpr bool kNeedsLock = true;
};
template <>
struct ResourceMapParams<uint16_t>
{
static constexpr size_t kInitialFlatResourcesSize = 3;
static constexpr size_t kAdditionalFlatResourcesSize = 7;
static constexpr bool kNeedsLock = true;
};
} // namespace gl
namespace
{
// The resourceMap class uses a lock for "unsigned int" types to support this unit test.
using LocklessType = int;
using LockedType = unsigned int;
using AdditionalFlatType = uint16_t;
template <typename T>
void AssignAndErase()
{
constexpr size_t kSize = 300;
ResourceMap<size_t, T> resourceMap;
std::vector<size_t> objects(kSize, 1);
for (size_t index = 0; index < kSize; ++index)
{
resourceMap.assign(index + 1, &objects[index]);
}
for (size_t index = 0; index < kSize; ++index)
{
size_t *found = nullptr;
ASSERT_TRUE(resourceMap.erase(index + 1, &found));
ASSERT_EQ(&objects[index], found);
}
ASSERT_TRUE(UnsafeResourceMapIter(resourceMap).empty());
}
// Tests assigning slots in the map and then deleting elements.
TEST(ResourceMapTest, AssignAndEraseLockless)
{
AssignAndErase<LocklessType>();
}
// Tests assigning slots in the map and then deleting elements.
TEST(ResourceMapTest, AssignAndEraseLocked)
{
AssignAndErase<LockedType>();
}
template <typename T>
void AssignAndClear()
{
constexpr size_t kSize = 280;
ResourceMap<size_t, T> resourceMap;
std::vector<size_t> objects(kSize, 1);
for (size_t index = 0; index < kSize; ++index)
{
resourceMap.assign(index + 1, &objects[index]);
}
resourceMap.clear();
ASSERT_TRUE(UnsafeResourceMapIter(resourceMap).empty());
}
// Tests assigning slots in the map and then using clear() to free it.
TEST(ResourceMapTest, AssignAndClearLockless)
{
AssignAndClear<LocklessType>();
}
// Tests assigning slots in the map and then using clear() to free it.
TEST(ResourceMapTest, AssignAndClearLocked)
{
AssignAndClear<LockedType>();
}
template <typename T>
void BigGrowth()
{
constexpr size_t kSize = 8;
ResourceMap<size_t, T> resourceMap;
std::vector<size_t> objects;
for (size_t index = 0; index < kSize; ++index)
{
objects.push_back(index);
}
// Assign a large value.
constexpr size_t kLargeIndex = 128;
objects.push_back(kLargeIndex);
for (size_t &object : objects)
{
resourceMap.assign(object, &object);
}
for (size_t object : objects)
{
size_t *found = nullptr;
ASSERT_TRUE(resourceMap.erase(object, &found));
ASSERT_EQ(object, *found);
}
ASSERT_TRUE(UnsafeResourceMapIter(resourceMap).empty());
}
// Tests growing a map more than double the size.
TEST(ResourceMapTest, BigGrowthLockless)
{
BigGrowth<LocklessType>();
}
// Tests growing a map more than double the size.
TEST(ResourceMapTest, BigGrowthLocked)
{
BigGrowth<LockedType>();
}
template <typename T>
void QueryUnassigned()
{
constexpr size_t kSize = 8;
constexpr T kZero = 0;
constexpr T kIdInFlatRange = 10;
constexpr T kIdOutOfFlatRange = 500;
ResourceMap<size_t, T> resourceMap;
std::vector<size_t> objects;
for (size_t index = 0; index < kSize; ++index)
{
objects.push_back(index);
}
ASSERT_FALSE(resourceMap.contains(kZero));
ASSERT_EQ(nullptr, resourceMap.query(kZero));
ASSERT_FALSE(resourceMap.contains(kIdOutOfFlatRange));
ASSERT_EQ(nullptr, resourceMap.query(kIdOutOfFlatRange));
for (size_t &object : objects)
{
resourceMap.assign(object, &object);
}
ASSERT_FALSE(UnsafeResourceMapIter(resourceMap).empty());
for (size_t &object : objects)
{
ASSERT_TRUE(resourceMap.contains(object));
ASSERT_EQ(&object, resourceMap.query(object));
}
ASSERT_FALSE(resourceMap.contains(kIdInFlatRange));
ASSERT_EQ(nullptr, resourceMap.query(kIdInFlatRange));
ASSERT_FALSE(resourceMap.contains(kIdOutOfFlatRange));
ASSERT_EQ(nullptr, resourceMap.query(kIdOutOfFlatRange));
for (size_t object : objects)
{
size_t *found = nullptr;
ASSERT_TRUE(resourceMap.erase(object, &found));
ASSERT_EQ(object, *found);
}
ASSERT_TRUE(UnsafeResourceMapIter(resourceMap).empty());
ASSERT_FALSE(resourceMap.contains(kZero));
ASSERT_EQ(nullptr, resourceMap.query(kZero));
ASSERT_FALSE(resourceMap.contains(kIdOutOfFlatRange));
ASSERT_EQ(nullptr, resourceMap.query(kIdOutOfFlatRange));
}
// Tests querying unassigned or erased values.
TEST(ResourceMapTest, QueryUnassignedLockless)
{
QueryUnassigned<LocklessType>();
}
// Tests querying unassigned or erased values.
TEST(ResourceMapTest, QueryUnassignedLocked)
{
QueryUnassigned<LockedType>();
}
template <typename IDType>
void ConcurrentAccess(size_t iterations, size_t idCycleSize)
{
if (std::is_same_v<ResourceMapMutex, angle::NoOpMutex>)
{
GTEST_SKIP() << "Test skipped: Locking is disabled in build.";
}
constexpr size_t kThreadCount = 13;
ResourceMap<size_t, IDType> resourceMap;
std::array<std::thread, kThreadCount> threads;
std::array<std::map<IDType, size_t>, kThreadCount> insertedIds;
for (size_t i = 0; i < kThreadCount; ++i)
{
threads[i] = std::thread([&, i]() {
// Each thread manipulates a different set of ids. The resource map guarantees that the
// data structure itself is thread-safe, not accesses to the same id.
for (size_t j = 0; j < iterations; ++j)
{
const IDType id =
static_cast<IDType>((j % (idCycleSize / kThreadCount)) * kThreadCount + i);
ASSERT_LE(id, 0xFFFFu);
ASSERT_LE(j, 0xFFFFu);
const size_t value = id | j << 16;
size_t *valuePtr = reinterpret_cast<size_t *>(value);
const size_t *queryResult = resourceMap.query(id);
const bool containsResult = resourceMap.contains(id);
const bool expectContains = insertedIds[i].count(id) > 0;
if (expectContains)
{
EXPECT_TRUE(containsResult);
const IDType queryResultInt =
static_cast<IDType>(reinterpret_cast<size_t>(queryResult) & 0xFFFF);
const size_t queryResultIteration = reinterpret_cast<size_t>(queryResult) >> 16;
EXPECT_EQ(queryResultInt, id);
EXPECT_LT(queryResultIteration, j);
size_t *erasedValue = nullptr;
const bool erased = resourceMap.erase(id, &erasedValue);
EXPECT_TRUE(erased);
EXPECT_EQ(erasedValue, queryResult);
insertedIds[i].erase(id);
}
else
{
EXPECT_FALSE(containsResult);
EXPECT_EQ(queryResult, nullptr);
resourceMap.assign(id, valuePtr);
EXPECT_TRUE(resourceMap.contains(id));
ASSERT_TRUE(insertedIds[i].count(id) == 0);
insertedIds[i][id] = value;
}
}
});
}
for (size_t i = 0; i < kThreadCount; ++i)
{
threads[i].join();
}
// Verify that every value that is expected to be there is actually there
std::map<size_t, size_t> allIds;
size_t allIdsPrevSize = 0;
for (size_t i = 0; i < kThreadCount; ++i)
{
// Merge all the sets together. The sets are disjoint, which is verified by the ASSERT_EQ.
allIds.insert(insertedIds[i].begin(), insertedIds[i].end());
ASSERT_EQ(allIds.size(), allIdsPrevSize + insertedIds[i].size());
allIdsPrevSize = allIds.size();
// Make sure every id that is expected to be there is actually there.
for (auto &idValue : insertedIds[i])
{
EXPECT_TRUE(resourceMap.contains(idValue.first));
EXPECT_EQ(resourceMap.query(idValue.first), reinterpret_cast<size_t *>(idValue.second));
}
}
// Verify that every value that is NOT expected to be there isn't actually there
for (auto &idValue : UnsafeResourceMapIter(resourceMap))
{
EXPECT_TRUE(allIds.count(idValue.first) == 1);
EXPECT_EQ(idValue.second, reinterpret_cast<size_t *>(allIds[idValue.first]));
}
resourceMap.clear();
}
// Tests that concurrent access to thread-safe resource maps works for small ids that are mostly in
// the flat map range.
TEST(ResourceMapTest, ConcurrentAccessSmallIds)
{
ConcurrentAccess<LockedType>(50'000, 128);
}
// Tests that concurrent access to thread-safe resource maps works for a wider range of ids.
TEST(ResourceMapTest, ConcurrentAccessLargeIds)
{
ConcurrentAccess<LockedType>(10'000, 20'000);
}
// Tests that concurrent access to thread-safe resource maps works for the initial flat array, the
// secondary flat array and the hashmap.
TEST(ResourceMapTest, ConcurrentAccessFlatArraysAndHashMap)
{
// Set idCycleSize to 26 (2 * kThreadCount) so that each thread gets 2 IDs and covers the flat
// arrays and the hashmap.
ConcurrentAccess<AdditionalFlatType>(20'000, 26);
}
// Tests growth across the initial flat array, the secondary flat array, and the hashmap.
TEST(ResourceMapTest, GrowthAcrossFlatArraysAndHashMap)
{
// Handle Layout
// - initial flat array : 1, 2
// - additional flat array : 3, 4, 5, 6, 7, 8, 9
// - hashmap : 10, 11, 12
constexpr size_t kTotalHandle = 12;
std::vector<size_t> objects(kTotalHandle);
for (size_t index = 0; index < kTotalHandle; ++index)
{
objects[index] = index + 1;
}
ResourceMap<size_t, AdditionalFlatType> resourceMap;
for (size_t index = 0; index < kTotalHandle; ++index)
{
resourceMap.assign(static_cast<AdditionalFlatType>(index + 1), &objects[index]);
}
for (size_t index = 0; index < kTotalHandle; ++index)
{
const AdditionalFlatType handle = static_cast<AdditionalFlatType>(index + 1);
EXPECT_TRUE(resourceMap.contains(handle)) << "handle=" << handle;
EXPECT_EQ(resourceMap.query(handle), &objects[index]) << "handle=" << handle;
}
// Handles that have never been assigned must not be found.
EXPECT_FALSE(resourceMap.contains(static_cast<AdditionalFlatType>(0)));
EXPECT_EQ(resourceMap.query(static_cast<AdditionalFlatType>(0)), nullptr);
EXPECT_FALSE(resourceMap.contains(static_cast<AdditionalFlatType>(kTotalHandle + 1)));
EXPECT_EQ(resourceMap.query(static_cast<AdditionalFlatType>(kTotalHandle + 1)), nullptr);
// The iterator must visit each assigned handle exactly once across all flat arrays and the
// hashmap.
std::map<GLuint, size_t *> visitedHandleMap;
for (const auto &idValue : UnsafeResourceMapIter(resourceMap))
{
EXPECT_EQ(visitedHandleMap.count(idValue.first), 0u) << "duplicate id=" << idValue.first;
visitedHandleMap[idValue.first] = idValue.second;
}
EXPECT_EQ(visitedHandleMap.size(), kTotalHandle);
for (size_t index = 0; index < kTotalHandle; ++index)
{
const GLuint handle = static_cast<GLuint>(index + 1);
ASSERT_EQ(visitedHandleMap.count(handle), 1u) << "missing handle=" << handle;
EXPECT_EQ(visitedHandleMap[handle], &objects[index]);
}
for (size_t index = 0; index < kTotalHandle; ++index)
{
const AdditionalFlatType handle = static_cast<AdditionalFlatType>(index + 1);
size_t *erased = nullptr;
EXPECT_TRUE(resourceMap.erase(handle, &erased)) << "handle=" << handle;
EXPECT_EQ(erased, &objects[index]) << "handle=" << handle;
EXPECT_FALSE(resourceMap.contains(handle)) << "handle is still present after erase";
}
EXPECT_TRUE(UnsafeResourceMapIter(resourceMap).empty());
}
// Tests that the iterator visits all valid handles when resources exist only in the hash map, and
// no ID in the additional flat array has ever been assigned.
TEST(ResourceMapTest, IteratorVisitsHashMapWithoutAdditionalFlatArray)
{
// Assign handles in the hashmap range to keep mAdditionalFlatResources nullptr.
// kHashmapIdx0, kHashmapIdx1 >= kInitialFlatResourcesSize + kAdditionalFlatResourcesSize
// >= kLocklessFlatResourcesLimit = 3 + 7 = 10
constexpr AdditionalFlatType kHashmapIdx0 = 10;
constexpr AdditionalFlatType kHashmapIdx1 = 11;
size_t hashmapValue0 = 100;
size_t hashmapValue1 = 200;
ResourceMap<size_t, AdditionalFlatType> resourceMap;
resourceMap.assign(kHashmapIdx0, &hashmapValue0);
resourceMap.assign(kHashmapIdx1, &hashmapValue1);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx0));
EXPECT_EQ(resourceMap.query(kHashmapIdx0), &hashmapValue0);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx1));
EXPECT_EQ(resourceMap.query(kHashmapIdx1), &hashmapValue1);
constexpr AdditionalFlatType kInitialArrayIdx = 1;
constexpr AdditionalFlatType kAdditionalArrayIdx = 5;
// Since IDs in the initial and additional flat arrays are never allocated, they should always
// return false or nullptr.
EXPECT_FALSE(resourceMap.contains(kInitialArrayIdx));
EXPECT_EQ(resourceMap.query(kInitialArrayIdx), nullptr);
EXPECT_FALSE(resourceMap.contains(kAdditionalArrayIdx));
EXPECT_EQ(resourceMap.query(kAdditionalArrayIdx), nullptr);
// The iterator must visit both hashmap entries.
std::map<GLuint, size_t *> visitedHandleMap;
for (const auto &idValue : UnsafeResourceMapIter(resourceMap))
{
visitedHandleMap[idValue.first] = idValue.second;
}
EXPECT_EQ(visitedHandleMap.size(), 2u);
EXPECT_EQ(visitedHandleMap.count(kHashmapIdx0), 1u);
EXPECT_EQ(visitedHandleMap[kHashmapIdx0], &hashmapValue0);
EXPECT_EQ(visitedHandleMap.count(kHashmapIdx1), 1u);
EXPECT_EQ(visitedHandleMap[kHashmapIdx1], &hashmapValue1);
resourceMap.clear();
}
// Tests that the iterator correctly visits all valid handles stored in the upper portion of the
// additional flat array.
TEST(ResourceMapTest, IteratorVisitsUpperAdditionalFlatArray)
{
// Additional flat range = [kInitialFlatResourcesSize, kLocklessFlatResourcesLimit) = [3, 10)
// Assign handles in the upper portion of the additional flat array.
constexpr AdditionalFlatType kFirstIdx = 7;
constexpr AdditionalFlatType kLastIdx = 9;
size_t value7 = 7;
size_t value8 = 8;
size_t value9 = 9;
ResourceMap<size_t, AdditionalFlatType> resourceMap;
resourceMap.assign(kFirstIdx, &value7);
resourceMap.assign(kFirstIdx + 1, &value8);
resourceMap.assign(kLastIdx, &value9);
EXPECT_TRUE(resourceMap.contains(kFirstIdx));
EXPECT_EQ(resourceMap.query(kFirstIdx), &value7);
EXPECT_TRUE(resourceMap.contains(kFirstIdx + 1));
EXPECT_EQ(resourceMap.query(kFirstIdx + 1), &value8);
EXPECT_TRUE(resourceMap.contains(kLastIdx));
EXPECT_EQ(resourceMap.query(kLastIdx), &value9);
// Iterator must visit all three handles.
std::map<GLuint, size_t *> visitedHandleMap;
for (const auto &idValue : UnsafeResourceMapIter(resourceMap))
{
visitedHandleMap[idValue.first] = idValue.second;
}
EXPECT_EQ(visitedHandleMap.size(), 3u);
EXPECT_EQ(visitedHandleMap[kFirstIdx], &value7);
EXPECT_EQ(visitedHandleMap[kFirstIdx + 1], &value8);
EXPECT_EQ(visitedHandleMap[kLastIdx], &value9);
resourceMap.clear();
}
// Tests that the iterator visits all valid handles when they are assigned only in the additional
// flat array and the hash map.
TEST(ResourceMapTest, IteratorVisitsAdditionalFlatArrayAndHashMap)
{
// Assign handles in the additional flat range [3, 10) and the hashmap (>= 10). No handles are
// assigned in the initial flat array [0, 3)
constexpr AdditionalFlatType kAdditionalIdx0 = 4;
constexpr AdditionalFlatType kAdditionalIdx1 = 8;
constexpr AdditionalFlatType kHashmapIdx0 = 20;
constexpr AdditionalFlatType kHashmapIdx1 = 21;
size_t additionalValue0 = 100;
size_t additionalValue1 = 200;
size_t hashmapValue0 = 300;
size_t hashmapValue1 = 400;
ResourceMap<size_t, AdditionalFlatType> resourceMap;
resourceMap.assign(kAdditionalIdx0, &additionalValue0);
resourceMap.assign(kAdditionalIdx1, &additionalValue1);
resourceMap.assign(kHashmapIdx0, &hashmapValue0);
resourceMap.assign(kHashmapIdx1, &hashmapValue1);
EXPECT_TRUE(resourceMap.contains(kAdditionalIdx0));
EXPECT_EQ(resourceMap.query(kAdditionalIdx0), &additionalValue0);
EXPECT_TRUE(resourceMap.contains(kAdditionalIdx1));
EXPECT_EQ(resourceMap.query(kAdditionalIdx1), &additionalValue1);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx0));
EXPECT_EQ(resourceMap.query(kHashmapIdx0), &hashmapValue0);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx1));
EXPECT_EQ(resourceMap.query(kHashmapIdx1), &hashmapValue1);
constexpr AdditionalFlatType kInitialArrayIdx = 1;
// Since IDs in the initial flat array are never allocated, the lookup should always return
// false or nullptr.
EXPECT_FALSE(resourceMap.contains(kInitialArrayIdx));
EXPECT_EQ(resourceMap.query(kInitialArrayIdx), nullptr);
// The iterator must visit all four handles.
std::map<GLuint, size_t *> visitedHandleMap;
for (const auto &idValue : UnsafeResourceMapIter(resourceMap))
{
visitedHandleMap[idValue.first] = idValue.second;
}
EXPECT_EQ(visitedHandleMap.size(), 4u);
EXPECT_EQ(visitedHandleMap[kAdditionalIdx0], &additionalValue0);
EXPECT_EQ(visitedHandleMap[kAdditionalIdx1], &additionalValue1);
EXPECT_EQ(visitedHandleMap[kHashmapIdx0], &hashmapValue0);
EXPECT_EQ(visitedHandleMap[kHashmapIdx1], &hashmapValue1);
resourceMap.clear();
}
// Tests that the iterator correctly transitions from the initial flat array to the hash map when
// the additional flat array has never been allocated. This verifies that nextResource() skips the
// additional flat range when additionalResources is nullptr, so updateValue() never dereferences a
// null pointer.
TEST(ResourceMapTest, IteratorVisitsInitialFlatArrayAndHashMap)
{
// Assign handles in the initial flat array (1, 2) and in the hashmap (>= 10). Since no handles
// are assigned in the additional flat range [3, 10), mAdditionalFlatResources remains nullptr.
constexpr AdditionalFlatType kInitialIdx0 = 1;
constexpr AdditionalFlatType kInitialIdx1 = 2;
constexpr AdditionalFlatType kHashmapIdx0 = 10;
constexpr AdditionalFlatType kHashmapIdx1 = 18;
size_t initialValue0 = 100;
size_t initialValue1 = 200;
size_t hashmapValue0 = 300;
size_t hashmapValue1 = 400;
ResourceMap<size_t, AdditionalFlatType> resourceMap;
resourceMap.assign(kInitialIdx0, &initialValue0);
resourceMap.assign(kInitialIdx1, &initialValue1);
resourceMap.assign(kHashmapIdx0, &hashmapValue0);
resourceMap.assign(kHashmapIdx1, &hashmapValue1);
EXPECT_TRUE(resourceMap.contains(kInitialIdx0));
EXPECT_EQ(resourceMap.query(kInitialIdx0), &initialValue0);
EXPECT_TRUE(resourceMap.contains(kInitialIdx1));
EXPECT_EQ(resourceMap.query(kInitialIdx1), &initialValue1);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx0));
EXPECT_EQ(resourceMap.query(kHashmapIdx0), &hashmapValue0);
EXPECT_TRUE(resourceMap.contains(kHashmapIdx1));
EXPECT_EQ(resourceMap.query(kHashmapIdx1), &hashmapValue1);
// Since IDs in the additional flat array are never allocated, the lookup should always return
// false or nullptr.
constexpr AdditionalFlatType kAdditionalArrayIdx = 5;
EXPECT_FALSE(resourceMap.contains(kAdditionalArrayIdx));
EXPECT_EQ(resourceMap.query(kAdditionalArrayIdx), nullptr);
// The iterator must visit all four handles.
std::map<GLuint, size_t *> visitedHandleMap;
for (const auto &idValue : UnsafeResourceMapIter(resourceMap))
{
visitedHandleMap[idValue.first] = idValue.second;
}
EXPECT_EQ(visitedHandleMap.size(), 4u);
EXPECT_EQ(visitedHandleMap[kInitialIdx0], &initialValue0);
EXPECT_EQ(visitedHandleMap[kInitialIdx1], &initialValue1);
EXPECT_EQ(visitedHandleMap[kHashmapIdx0], &hashmapValue0);
EXPECT_EQ(visitedHandleMap[kHashmapIdx1], &hashmapValue1);
resourceMap.clear();
}
} // anonymous namespace