| // |
| // Copyright 2023 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. |
| // |
| // Unit tests for ContextMutex. |
| // |
| |
| #include "gtest/gtest.h" |
| |
| #include "libANGLE/ContextMutex.h" |
| |
| namespace |
| { |
| |
| template <class GetContextMutex> |
| void runBasicContextMutexTest(bool expectToPass, GetContextMutex &&getContextMutex) |
| { |
| constexpr size_t kThreadCount = 16; |
| constexpr size_t kIterationCount = 50'000; |
| |
| std::array<std::thread, kThreadCount> threads; |
| std::array<egl::ContextMutex *, kThreadCount> contextMutexes = {}; |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| size_t readyCount = 0; |
| |
| std::atomic<size_t> testVar; |
| |
| for (size_t i = 0; i < kThreadCount; ++i) |
| { |
| threads[i] = std::thread([&, i]() { |
| { |
| std::unique_lock<std::mutex> lock(mutex); |
| contextMutexes[i] = getContextMutex(); |
| contextMutexes[i]->addRef(); |
| ++readyCount; |
| if (readyCount < kThreadCount) |
| { |
| condVar.wait(lock, [&]() { return readyCount == kThreadCount; }); |
| } |
| else |
| { |
| condVar.notify_all(); |
| } |
| } |
| for (size_t j = 0; j < kIterationCount; ++j) |
| { |
| egl::ScopedContextMutexLock lock(contextMutexes[i]); |
| const int local = testVar.load(std::memory_order_relaxed); |
| const int newValue = local + 1; |
| testVar.store(newValue, std::memory_order_relaxed); |
| } |
| }); |
| } |
| |
| for (size_t i = 0; i < kThreadCount; ++i) |
| { |
| threads[i].join(); |
| contextMutexes[i]->release(); |
| } |
| |
| if (expectToPass) |
| { |
| EXPECT_EQ(testVar.load(), kThreadCount * kIterationCount); |
| } |
| else |
| { |
| EXPECT_LE(testVar.load(), kThreadCount * kIterationCount); |
| } |
| } |
| |
| // Tests locking of single ContextMutex mutex. |
| TEST(ContextMutexTest, SingleMutexLock) |
| { |
| egl::ContextMutex *contextMutex = new egl::ContextMutex(); |
| contextMutex->addRef(); |
| runBasicContextMutexTest(true, [&]() { return contextMutex; }); |
| contextMutex->release(); |
| } |
| |
| // Tests locking of multiple merged ContextMutex mutexes. |
| TEST(ContextMutexTest, MultipleMergedMutexLock) |
| { |
| egl::ContextMutex *contextMutex = new egl::ContextMutex(); |
| contextMutex->addRef(); |
| runBasicContextMutexTest(true, [&]() { |
| egl::ScopedContextMutexLock lock(contextMutex); |
| egl::ContextMutex *threadMutex = new egl::ContextMutex(); |
| egl::ContextMutex::Merge(contextMutex, threadMutex); |
| return threadMutex; |
| }); |
| contextMutex->release(); |
| } |
| |
| // Tests locking of multiple unmerged ContextMutex mutexes. |
| TEST(ContextMutexTest, MultipleUnmergedMutexLock) |
| { |
| runBasicContextMutexTest(false, [&]() { return new egl::ContextMutex(); }); |
| } |
| |
| // Creates 2N mutexes and 2 threads, then merges N mutex pairs in each thread. Merging order of |
| // the first thread is reversed in the second thread. |
| TEST(ContextMutexTest, TwoThreadsCrossMerge) |
| { |
| constexpr size_t kThreadCount = 2; |
| constexpr size_t kIterationCount = 100; |
| static_assert(kThreadCount % 2 == 0); |
| |
| std::array<std::thread, kThreadCount> threads; |
| std::array<std::array<egl::ContextMutex *, kThreadCount>, kIterationCount> mutexParis = {}; |
| |
| // Create mutexes. |
| for (uint32_t i = 0; i < kIterationCount; ++i) |
| { |
| for (uint32_t j = 0; j < kThreadCount; ++j) |
| { |
| mutexParis[i][j] = new egl::ContextMutex(); |
| // Call without a lock because no concurrent access is possible. |
| mutexParis[i][j]->addRef(); |
| } |
| } |
| |
| std::mutex mutex; |
| std::condition_variable condVar; |
| size_t readyCount = 0; |
| bool flipFlop = false; |
| |
| auto threadJob = [&](size_t lockMutexIndex) { |
| for (size_t i = 0; i < kIterationCount; ++i) |
| { |
| // Lock the first mutex. |
| egl::ScopedContextMutexLock contextMutexLock(mutexParis[i][lockMutexIndex]); |
| // Wait until all threads are ready... |
| { |
| std::unique_lock<std::mutex> lock(mutex); |
| ++readyCount; |
| if (readyCount == kThreadCount) |
| { |
| readyCount = 0; |
| flipFlop = !flipFlop; |
| condVar.notify_all(); |
| } |
| else |
| { |
| const size_t prevFlipFlop = flipFlop; |
| condVar.wait(lock, [&]() { return flipFlop != prevFlipFlop; }); |
| } |
| } |
| // Merge mutexes. |
| egl::ContextMutex::Merge(mutexParis[i][lockMutexIndex], |
| mutexParis[i][kThreadCount - lockMutexIndex - 1]); |
| } |
| }; |
| |
| // Start threads... |
| for (size_t i = 0; i < kThreadCount; ++i) |
| { |
| threads[i] = std::thread([&, i]() { threadJob(i); }); |
| } |
| |
| // Join with threads... |
| for (std::thread &thread : threads) |
| { |
| thread.join(); |
| } |
| |
| // Destroy mutexes. |
| for (size_t i = 0; i < kIterationCount; ++i) |
| { |
| for (size_t j = 0; j < kThreadCount; ++j) |
| { |
| // Call without a lock because no concurrent access is possible. |
| mutexParis[i][j]->release(); |
| } |
| } |
| } |
| |
| } // anonymous namespace |