blob: 26411fd2603e7901d89d9f6fa606a0448e93c36f [file] [log] [blame]
//
// 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;
}
} // 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 <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>();
}
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, LockedType> resourceMap;
std::array<std::thread, kThreadCount> threads;
std::array<std::map<LockedType, 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 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<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 LockedType queryResultInt =
static_cast<LockedType>(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(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