From 14094d97cfd06417bb563d3415d33aeadb18b82e Mon Sep 17 00:00:00 2001 From: Jan Varga Date: Thu, 13 Jun 2024 11:07:08 +0000 Subject: [PATCH] Bug 1901745 - Introduce mozilla::dom::quota::RunAfterProcessNextEvent; r=dom-storage-reviewers,jstutte, a=dmeehan Differential Revision: https://phabricator.services.mozilla.com/D213215 --- dom/quota/ThreadUtils.cpp | 84 ++++++++++ dom/quota/ThreadUtils.h | 46 ++++++ dom/quota/moz.build | 2 + dom/quota/test/gtest/TestThreadUtils.cpp | 191 +++++++++++++++++++++++ dom/quota/test/gtest/moz.build | 1 + 5 files changed, 324 insertions(+) create mode 100644 dom/quota/ThreadUtils.cpp create mode 100644 dom/quota/ThreadUtils.h create mode 100644 dom/quota/test/gtest/TestThreadUtils.cpp diff --git a/dom/quota/ThreadUtils.cpp b/dom/quota/ThreadUtils.cpp new file mode 100644 index 000000000000..25e05151195c --- /dev/null +++ b/dom/quota/ThreadUtils.cpp @@ -0,0 +1,84 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/quota/ThreadUtils.h" + +#include "mozilla/dom/quota/QuotaCommon.h" +#include "mozilla/dom/quota/ResultExtensions.h" +#include "nsIThreadInternal.h" +#include "nsThreadPool.h" +#include "nsThreadUtils.h" + +namespace mozilla::dom::quota { + +namespace { + +class RunAfterProcessingCurrentEventHelper final : public nsIThreadObserver { + public: + nsresult Init(std::function&& aCallback); + + NS_DECL_ISUPPORTS + NS_DECL_NSITHREADOBSERVER + + private: + ~RunAfterProcessingCurrentEventHelper() = default; + + std::function mCallback; +}; + +} // namespace + +nsresult RunAfterProcessingCurrentEventHelper::Init( + std::function&& aCallback) { + nsCOMPtr thread = do_QueryInterface(NS_GetCurrentThread()); + MOZ_ASSERT(thread); + + QM_TRY(MOZ_TO_RESULT(thread->AddObserver(this))); + + mCallback = std::move(aCallback); + + return NS_OK; +} + +NS_IMPL_ISUPPORTS(RunAfterProcessingCurrentEventHelper, nsIThreadObserver) + +NS_IMETHODIMP +RunAfterProcessingCurrentEventHelper::OnDispatchedEvent() { + MOZ_CRASH("Should never be called!"); +} + +NS_IMETHODIMP +RunAfterProcessingCurrentEventHelper::OnProcessNextEvent( + nsIThreadInternal* /* aThread */, bool /* aMayWait */) { + MOZ_CRASH("Should never be called!"); +} + +NS_IMETHODIMP +RunAfterProcessingCurrentEventHelper::AfterProcessNextEvent( + nsIThreadInternal* aThread, bool /* aEventWasProcessed */) { + MOZ_ASSERT(aThread); + + QM_WARNONLY_TRY(MOZ_TO_RESULT(aThread->RemoveObserver(this))); + + auto callback = std::move(mCallback); + callback(); + + return NS_OK; +} + +nsresult RunAfterProcessingCurrentEvent(std::function&& aCallback) { + MOZ_DIAGNOSTIC_ASSERT( + !nsThreadPool::GetCurrentThreadPool(), + "Call to RunAfterProcessingCurrentEvent() from thread pool!"); + + auto helper = MakeRefPtr(); + + QM_TRY(MOZ_TO_RESULT(helper->Init(std::move(aCallback)))); + + return NS_OK; +} + +} // namespace mozilla::dom::quota diff --git a/dom/quota/ThreadUtils.h b/dom/quota/ThreadUtils.h new file mode 100644 index 000000000000..b35a84691f29 --- /dev/null +++ b/dom/quota/ThreadUtils.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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/. */ + +#ifndef DOM_QUOTA_THREADUTILS_H_ +#define DOM_QUOTA_THREADUTILS_H_ + +#include +#include + +enum class nsresult : uint32_t; + +namespace mozilla::dom::quota { + +/** + * Add a temporary thread observer and listen for the "AfterProcessNextEvent" + * notification. Once the notification is received, remove the temporary thread + * observer and call aCallback. + * In practice, this calls aCallback immediately after the current thread is + * done with running and releasing recently popped event from thread's event + * queue. + * If called multiple times, all the callbacks will be executed, in the + * order in which RunAfterProcessingCurrentEvent() was called. + * Use this method if you need to dispatch the same or some other runnable to + * another thread in a way which prevents any race conditions (for example + * unpredictable releases of objects). + * This method should be used only in existing code which can't be easily + * converted to use MozPromise which doesn't have the problem with + * unpredictable releases of objects, see: + * https://searchfox.org/mozilla-central/rev/4582d908c17fbf7924f5699609fe4a12c28ddc4a/xpcom/threads/MozPromise.h#866 + * + * Note: Calling this method from a thread pool is not supported since thread + * pools don't fire the "AfterProcessNextEvent" notification. The method has + * a diagnostic assertion for that so any calls like that will be caught + * in builds with enabled diagnostic assertions. The callback will never + * get executed in other builds, such as release builds. The limitation can + * be removed completely when thread pool implementation gets support for firing + * the "AfterProcessNextEvent". + */ +nsresult RunAfterProcessingCurrentEvent(std::function&& aCallback); + +} // namespace mozilla::dom::quota + +#endif // DOM_QUOTA_THREADUTILS_H_ diff --git a/dom/quota/moz.build b/dom/quota/moz.build index 7cb586cd214e..2d527b4baeea 100644 --- a/dom/quota/moz.build +++ b/dom/quota/moz.build @@ -75,6 +75,7 @@ EXPORTS.mozilla.dom.quota += [ "StorageHelpers.h", "StreamUtils.h", "StringifyUtils.h", + "ThreadUtils.h", "UsageInfo.h", ] @@ -125,6 +126,7 @@ UNIFIED_SOURCES += [ "StorageOriginAttributes.cpp", "StreamUtils.cpp", "StringifyUtils.cpp", + "ThreadUtils.cpp", ] IPDL_SOURCES += [ diff --git a/dom/quota/test/gtest/TestThreadUtils.cpp b/dom/quota/test/gtest/TestThreadUtils.cpp new file mode 100644 index 000000000000..161326e0c06a --- /dev/null +++ b/dom/quota/test/gtest/TestThreadUtils.cpp @@ -0,0 +1,191 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "mozilla/dom/quota/QuotaManager.h" +#include "mozilla/dom/quota/ThreadUtils.h" +#include "mozilla/gtest/MozAssertions.h" +#include "QuotaManagerDependencyFixture.h" + +namespace mozilla::dom::quota::test { + +namespace { + +MozExternalRefCountType GetRefCount(nsISupports* aSupports) { + if (!aSupports) { + return 0; + } + aSupports->AddRef(); + return aSupports->Release(); +} + +class OneTimeRunnable final : public Runnable { + public: + enum class State { None, Initial, Created, Destroyed }; + + static void Init() { + ASSERT_TRUE(sState == State::None || sState == State::Destroyed); + ASSERT_FALSE(sCurrent); + + sState = State::Initial; + } + + static RefPtr Create(std::function&& aTask) { + EXPECT_EQ(sState, State::Initial); + + RefPtr runnable = new OneTimeRunnable(std::move(aTask)); + return runnable; + } + + static State GetState() { return sState; } + + static OneTimeRunnable* GetCurrent() { return sCurrent; } + + static MozExternalRefCountType GetCurrentRefCount() { + return GetRefCount(static_cast(sCurrent)); + } + + NS_IMETHOD + Run() override { + auto task = std::move(mTask); + task(); + + return NS_OK; + } + + private: + explicit OneTimeRunnable(std::function&& aTask) + : Runnable("dom::quota::test::OneTimeRunnable"), mTask(std::move(aTask)) { + sCurrent = this; + sState = State::Created; + } + + ~OneTimeRunnable() override { + sState = State::Destroyed; + sCurrent = nullptr; + } + + static Atomic sState; + static OneTimeRunnable* sCurrent; + + std::function mTask; +}; + +} // namespace + +Atomic OneTimeRunnable::sState( + OneTimeRunnable::State::None); +OneTimeRunnable* OneTimeRunnable::sCurrent(nullptr); + +class TestThreadUtils : public QuotaManagerDependencyFixture { + public: + static void SetUpTestCase() { ASSERT_NO_FATAL_FAILURE(InitializeFixture()); } + + static void TearDownTestCase() { ASSERT_NO_FATAL_FAILURE(ShutdownFixture()); } +}; + +TEST_F(TestThreadUtils, RunAfterProcessingCurrentEvent_Release) { + bool runnableCalled = false; + bool callbackCalled = false; + + QuotaManager* quotaManager = QuotaManager::Get(); + ASSERT_TRUE(quotaManager); + + OneTimeRunnable::Init(); + + auto runnable = OneTimeRunnable::Create([&runnableCalled, &callbackCalled]() { + runnableCalled = true; + + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Created); + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 2u); + + RunAfterProcessingCurrentEvent([&callbackCalled]() { + callbackCalled = true; + + // The runnable shouldn't be yet destroyed because we still have a strong + // reference to it in `runnable`. + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Created); + + // The runnable should be released once (by the event queue processing + // code). + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 1u); + }); + + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Created); + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 2u); + }); + + ASSERT_FALSE(runnableCalled); + ASSERT_FALSE(callbackCalled); + + // Note that we are keeping ownership of the runnable here. + ASSERT_EQ(NS_OK, + quotaManager->IOThread()->Dispatch(runnable, NS_DISPATCH_NORMAL)); + + // Do a round-trip to the QM IO thread to ensure the test doesn't finish + // before the OneTimeRunnable is fully processed. + // + // Note: In theory, we would use SyncRunnable wrapper here, but the code + // reads better when both tests use the same way for blocking the current + // thread. + ASSERT_NO_FATAL_FAILURE(PerformOnIOThread([]() {})); + + // Check that the runnable and the callback were actually called. + ASSERT_TRUE(runnableCalled); + ASSERT_TRUE(callbackCalled); +} + +TEST_F(TestThreadUtils, RunAfterProcessingCurrentEvent_ReleaseAndDestory) { + bool runnableCalled = false; + bool callbackCalled = false; + + QuotaManager* quotaManager = QuotaManager::Get(); + ASSERT_TRUE(quotaManager); + + OneTimeRunnable::Init(); + + auto runnable = OneTimeRunnable::Create([&runnableCalled, &callbackCalled]() { + runnableCalled = true; + + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Created); + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 1u); + + RunAfterProcessingCurrentEvent([&callbackCalled]() { + callbackCalled = true; + + // The runnable should be destroyed because we don't have any other strong + // references to it. + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Destroyed); + + // The runnable should be released once (by the event queue processing + // code). + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 0u); + }); + + EXPECT_EQ(OneTimeRunnable::GetState(), OneTimeRunnable::State::Created); + EXPECT_EQ(OneTimeRunnable::GetCurrentRefCount(), 1u); + }); + + ASSERT_FALSE(runnableCalled); + ASSERT_FALSE(callbackCalled); + + // Note that we are tranferring ownership of the runnable here. + ASSERT_EQ(NS_OK, quotaManager->IOThread()->Dispatch(runnable.forget(), + NS_DISPATCH_NORMAL)); + + // Do a round-trip to the QM IO thread to ensure the test doesn't finish + // before the OneTimeRunnable is fully processed. + // + // Note: SyncRunnable wrapper can't be used here because that would hold our + // runnable longer, and we couldn't test that our runnable is destroyed when + // the callback for RunAfterProcessingCurrentEvent is executed. + ASSERT_NO_FATAL_FAILURE(PerformOnIOThread([]() {})); + + // Check that the runnable and the callback were actually called. + ASSERT_TRUE(runnableCalled); + ASSERT_TRUE(callbackCalled); +} + +} // namespace mozilla::dom::quota::test diff --git a/dom/quota/test/gtest/moz.build b/dom/quota/test/gtest/moz.build index d983e3457e1c..8bce5536d011 100644 --- a/dom/quota/test/gtest/moz.build +++ b/dom/quota/test/gtest/moz.build @@ -34,6 +34,7 @@ UNIFIED_SOURCES = [ "TestStorageOriginAttributes.cpp", "TestStringifyUtils.cpp", "TestTelemetry.cpp", + "TestThreadUtils.cpp", "TestUsageInfo.cpp", ]