// // 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 #include #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; } } // 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; template void AssignAndErase() { constexpr size_t kSize = 300; ResourceMap resourceMap; std::vector 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(); } // Tests assigning slots in the map and then deleting elements. TEST(ResourceMapTest, AssignAndEraseLocked) { AssignAndErase(); } template void AssignAndClear() { constexpr size_t kSize = 280; ResourceMap resourceMap; std::vector 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(); } // Tests assigning slots in the map and then using clear() to free it. TEST(ResourceMapTest, AssignAndClearLocked) { AssignAndClear(); } template void BigGrowth() { constexpr size_t kSize = 8; ResourceMap resourceMap; std::vector 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(); } // Tests growing a map more than double the size. TEST(ResourceMapTest, BigGrowthLocked) { BigGrowth(); } template void QueryUnassigned() { constexpr size_t kSize = 8; constexpr T kZero = 0; constexpr T kIdInFlatRange = 10; constexpr T kIdOutOfFlatRange = 500; ResourceMap resourceMap; std::vector 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(); } // Tests querying unassigned or erased values. TEST(ResourceMapTest, QueryUnassignedLocked) { QueryUnassigned(); } void ConcurrentAccess(size_t iterations, size_t idCycleSize) { if (std::is_same_v) { GTEST_SKIP() << "Test skipped: Locking is disabled in build."; } constexpr size_t kThreadCount = 13; ResourceMap resourceMap; std::array threads; std::array, 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 LockedType id = (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(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 LockedType queryResultInt = static_cast(reinterpret_cast(queryResult) & 0xFFFF); const size_t queryResultIteration = reinterpret_cast(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 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(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(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(50'000, 128); } // Tests that concurrent access to thread-safe resource maps works for a wider range of ids. TEST(ResourceMapTest, ConcurrentAccessLargeIds) { ConcurrentAccess(10'000, 20'000); } } // anonymous namespace