/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "nsISupportsImpl.h" #include "mozilla/Atomics.h" #include "nsIThread.h" #include "nsThreadUtils.h" #include "gtest/gtest.h" #include "mozilla/gtest/MozAssertions.h" using namespace mozilla; class nsThreadSafeAutoRefCntRunner final : public Runnable { public: NS_IMETHOD Run() final { for (int i = 0; i < 10000; i++) { if (++sRefCnt == 1) { sIncToOne++; } if (--sRefCnt == 0) { sDecToZero++; } } return NS_OK; } static ThreadSafeAutoRefCnt sRefCnt; static Atomic sIncToOne; static Atomic sDecToZero; nsThreadSafeAutoRefCntRunner() : Runnable("nsThreadSafeAutoRefCntRunner") {} private: ~nsThreadSafeAutoRefCntRunner() = default; }; ThreadSafeAutoRefCnt nsThreadSafeAutoRefCntRunner::sRefCnt; Atomic nsThreadSafeAutoRefCntRunner::sIncToOne(0); Atomic nsThreadSafeAutoRefCntRunner::sDecToZero(0); class nsThreadSafeAutoRefCntDecrementWithLimitRunner final : public Runnable { public: static constexpr size_t kDecrementsPerThread = 1000; NS_IMETHOD Run() final { for (size_t i = 0; i < kDecrementsPerThread; i++) { auto [ok, count] = sRefCnt.DecrementWithLimit<1>(); if (!ok) { sLimitHits++; break; } } return NS_OK; } static ThreadSafeAutoRefCnt sRefCnt; // Relaxed ordering so sLimitHits doesn't affect the ordering properties of // DecrementWithLimit being tested. static Atomic sLimitHits; nsThreadSafeAutoRefCntDecrementWithLimitRunner() : Runnable("nsThreadSafeAutoRefCntDecrementWithLimitRunner") {} private: ~nsThreadSafeAutoRefCntDecrementWithLimitRunner() = default; }; ThreadSafeAutoRefCnt nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt; Atomic nsThreadSafeAutoRefCntDecrementWithLimitRunner::sLimitHits(0); // When a refcounted object is actually owned by a cache, we may not // want to release the object after last reference gets released. In // this pattern, the cache may rely on the balance of increment to one // and decrement to zero, so that it can maintain a counter for GC. TEST(AutoRefCnt, ThreadSafeAutoRefCntBalance) { static const size_t kThreadCount = 4; nsCOMPtr threads[kThreadCount]; for (auto& thread : threads) { nsresult rv = NS_NewNamedThread("AutoRefCnt Test", getter_AddRefs(thread), new nsThreadSafeAutoRefCntRunner); EXPECT_NS_SUCCEEDED(rv); } for (const auto& thread : threads) { thread->Shutdown(); } EXPECT_EQ(nsThreadSafeAutoRefCntRunner::sRefCnt, nsrefcnt(0)); EXPECT_EQ(nsThreadSafeAutoRefCntRunner::sIncToOne, nsThreadSafeAutoRefCntRunner::sDecToZero); } // Spawns kThreadCount threads each attempting kDecrementsPerThread // decrements, then verifies the final refcount is 1 and that exactly // aExpectedLimitHits threads hit the limit. // kInitial is derived from aExpectedLimitHits so that exactly // (kThreadCount - aExpectedLimitHits) threads can complete all their // decrements; the rest hit the limit. static void RunDecrementWithLimitThreaded(uint32_t aExpectedLimitHits) { static const size_t kThreadCount = 4; const nsrefcnt kInitial = (kThreadCount - aExpectedLimitHits) * nsThreadSafeAutoRefCntDecrementWithLimitRunner::kDecrementsPerThread + 1; nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt = kInitial; nsThreadSafeAutoRefCntDecrementWithLimitRunner::sLimitHits = 0; nsCOMPtr threads[kThreadCount]; for (auto& thread : threads) { nsresult rv = NS_NewNamedThread("AutoRefCnt Test", getter_AddRefs(thread), new nsThreadSafeAutoRefCntDecrementWithLimitRunner); EXPECT_NS_SUCCEEDED(rv); } for (const auto& thread : threads) { thread->Shutdown(); } EXPECT_EQ(nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt, nsrefcnt(1)); EXPECT_EQ(nsThreadSafeAutoRefCntDecrementWithLimitRunner::sLimitHits, uint32_t(aExpectedLimitHits)); } // Verify DecrementWithLimit with no lost updates when the limit is never hit. TEST(AutoRefCnt, ThreadSafeAutoRefCntDecrementWithLimitNoHit) { RunDecrementWithLimitThreaded(0); } // Verify DecrementWithLimit correctly enforces the limit. With kInitial == 1 // (equal to Limit), all threads hit the limit on their first attempt, // giving a deterministic sLimitHits == kThreadCount. TEST(AutoRefCnt, ThreadSafeAutoRefCntDecrementWithLimitHit) { static const size_t kThreadCount = 4; RunDecrementWithLimitThreaded(kThreadCount); } // Verify DecrementWithLimit performs exactly kDecrementsPerThread successful // decrements before hitting the limit. Single-threaded for determinism. TEST(AutoRefCnt, ThreadSafeAutoRefCntDecrementWithLimitSequential) { const nsrefcnt kDecrementsPerThread = nsThreadSafeAutoRefCntDecrementWithLimitRunner::kDecrementsPerThread; // Initialize so exactly kDecrementsPerThread decrements succeed before // hitting the limit at 1. nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt = kDecrementsPerThread + 1; size_t successes = 0; while (true) { auto [ok, count] = nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt .DecrementWithLimit<1>(); if (!ok) { break; } successes++; } EXPECT_EQ(successes, size_t(kDecrementsPerThread)); EXPECT_EQ(nsThreadSafeAutoRefCntDecrementWithLimitRunner::sRefCnt, nsrefcnt(1)); }