From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- xpcom/threads/AbstractThread.cpp | 359 ++++++ xpcom/threads/AbstractThread.h | 129 ++ xpcom/threads/BlockingResourceBase.cpp | 547 ++++++++ xpcom/threads/BlockingResourceBase.h | 339 +++++ xpcom/threads/CPUUsageWatcher.cpp | 253 ++++ xpcom/threads/CPUUsageWatcher.h | 100 ++ xpcom/threads/CondVar.h | 139 +++ xpcom/threads/DataMutex.h | 130 ++ xpcom/threads/DeadlockDetector.h | 359 ++++++ xpcom/threads/DelayedRunnable.cpp | 113 ++ xpcom/threads/DelayedRunnable.h | 53 + xpcom/threads/EventQueue.cpp | 131 ++ xpcom/threads/EventQueue.h | 135 ++ xpcom/threads/EventTargetCapability.h | 95 ++ xpcom/threads/IdlePeriodState.cpp | 257 ++++ xpcom/threads/IdlePeriodState.h | 196 +++ xpcom/threads/IdleTaskRunner.cpp | 281 +++++ xpcom/threads/IdleTaskRunner.h | 125 ++ xpcom/threads/InputTaskManager.cpp | 156 +++ xpcom/threads/InputTaskManager.h | 141 +++ xpcom/threads/LazyIdleThread.cpp | 126 ++ xpcom/threads/LazyIdleThread.h | 93 ++ xpcom/threads/LeakRefPtr.h | 48 + xpcom/threads/MainThreadIdlePeriod.cpp | 77 ++ xpcom/threads/MainThreadIdlePeriod.h | 31 + xpcom/threads/MainThreadUtils.h | 60 + xpcom/threads/Monitor.h | 316 +++++ xpcom/threads/MozPromise.h | 1757 ++++++++++++++++++++++++++ xpcom/threads/Mutex.h | 485 ++++++++ xpcom/threads/Queue.h | 265 ++++ xpcom/threads/RWLock.cpp | 28 + xpcom/threads/RWLock.h | 243 ++++ xpcom/threads/RecursiveMutex.cpp | 85 ++ xpcom/threads/RecursiveMutex.h | 120 ++ xpcom/threads/ReentrantMonitor.h | 251 ++++ xpcom/threads/SchedulerGroup.cpp | 20 + xpcom/threads/SchedulerGroup.h | 26 + xpcom/threads/SharedThreadPool.cpp | 221 ++++ xpcom/threads/SharedThreadPool.h | 132 ++ xpcom/threads/SpinEventLoopUntil.h | 191 +++ xpcom/threads/StateMirroring.h | 453 +++++++ xpcom/threads/StateWatching.h | 302 +++++ xpcom/threads/SyncRunnable.h | 157 +++ xpcom/threads/SynchronizedEventQueue.cpp | 26 + xpcom/threads/SynchronizedEventQueue.h | 131 ++ xpcom/threads/TaskController.cpp | 1098 ++++++++++++++++ xpcom/threads/TaskController.h | 459 +++++++ xpcom/threads/TaskDispatcher.h | 304 +++++ xpcom/threads/TaskQueue.cpp | 345 ++++++ xpcom/threads/TaskQueue.h | 276 +++++ xpcom/threads/ThreadBound.h | 143 +++ xpcom/threads/ThreadDelay.cpp | 38 + xpcom/threads/ThreadDelay.h | 16 + xpcom/threads/ThreadEventQueue.cpp | 324 +++++ xpcom/threads/ThreadEventQueue.h | 95 ++ xpcom/threads/ThreadEventTarget.cpp | 137 ++ xpcom/threads/ThreadEventTarget.h | 63 + xpcom/threads/ThreadLocalVariables.cpp | 16 + xpcom/threads/ThrottledEventQueue.cpp | 459 +++++++ xpcom/threads/ThrottledEventQueue.h | 118 ++ xpcom/threads/TimerThread.cpp | 1565 +++++++++++++++++++++++ xpcom/threads/TimerThread.h | 247 ++++ xpcom/threads/VsyncTaskManager.cpp | 22 + xpcom/threads/VsyncTaskManager.h | 26 + xpcom/threads/WinHandleWatcher.cpp | 306 +++++ xpcom/threads/WinHandleWatcher.h | 117 ++ xpcom/threads/components.conf | 29 + xpcom/threads/moz.build | 144 +++ xpcom/threads/nsEnvironment.cpp | 136 ++ xpcom/threads/nsEnvironment.h | 34 + xpcom/threads/nsICancelableRunnable.h | 40 + xpcom/threads/nsIDirectTaskDispatcher.idl | 57 + xpcom/threads/nsIDiscardableRunnable.h | 41 + xpcom/threads/nsIEnvironment.idl | 54 + xpcom/threads/nsIEventTarget.idl | 227 ++++ xpcom/threads/nsIIdlePeriod.idl | 32 + xpcom/threads/nsIIdleRunnable.h | 48 + xpcom/threads/nsINamed.idl | 24 + xpcom/threads/nsIProcess.idl | 112 ++ xpcom/threads/nsIRunnable.idl | 45 + xpcom/threads/nsISerialEventTarget.idl | 27 + xpcom/threads/nsISupportsPriority.idl | 45 + xpcom/threads/nsITargetShutdownTask.h | 37 + xpcom/threads/nsIThread.idl | 222 ++++ xpcom/threads/nsIThreadInternal.idl | 110 ++ xpcom/threads/nsIThreadManager.idl | 182 +++ xpcom/threads/nsIThreadPool.idl | 115 ++ xpcom/threads/nsIThreadShutdown.idl | 57 + xpcom/threads/nsITimer.idl | 376 ++++++ xpcom/threads/nsMemoryPressure.cpp | 104 ++ xpcom/threads/nsMemoryPressure.h | 77 ++ xpcom/threads/nsProcess.h | 82 ++ xpcom/threads/nsProcessCommon.cpp | 600 +++++++++ xpcom/threads/nsProxyRelease.cpp | 30 + xpcom/threads/nsProxyRelease.h | 380 ++++++ xpcom/threads/nsThread.cpp | 1560 +++++++++++++++++++++++ xpcom/threads/nsThread.h | 364 ++++++ xpcom/threads/nsThreadManager.cpp | 841 +++++++++++++ xpcom/threads/nsThreadManager.h | 156 +++ xpcom/threads/nsThreadPool.cpp | 610 +++++++++ xpcom/threads/nsThreadPool.h | 68 + xpcom/threads/nsThreadSyncDispatch.h | 65 + xpcom/threads/nsThreadUtils.cpp | 769 ++++++++++++ xpcom/threads/nsThreadUtils.h | 1925 +++++++++++++++++++++++++++++ xpcom/threads/nsTimerImpl.cpp | 822 ++++++++++++ xpcom/threads/nsTimerImpl.h | 231 ++++ 106 files changed, 26734 insertions(+) create mode 100644 xpcom/threads/AbstractThread.cpp create mode 100644 xpcom/threads/AbstractThread.h create mode 100644 xpcom/threads/BlockingResourceBase.cpp create mode 100644 xpcom/threads/BlockingResourceBase.h create mode 100644 xpcom/threads/CPUUsageWatcher.cpp create mode 100644 xpcom/threads/CPUUsageWatcher.h create mode 100644 xpcom/threads/CondVar.h create mode 100644 xpcom/threads/DataMutex.h create mode 100644 xpcom/threads/DeadlockDetector.h create mode 100644 xpcom/threads/DelayedRunnable.cpp create mode 100644 xpcom/threads/DelayedRunnable.h create mode 100644 xpcom/threads/EventQueue.cpp create mode 100644 xpcom/threads/EventQueue.h create mode 100644 xpcom/threads/EventTargetCapability.h create mode 100644 xpcom/threads/IdlePeriodState.cpp create mode 100644 xpcom/threads/IdlePeriodState.h create mode 100644 xpcom/threads/IdleTaskRunner.cpp create mode 100644 xpcom/threads/IdleTaskRunner.h create mode 100644 xpcom/threads/InputTaskManager.cpp create mode 100644 xpcom/threads/InputTaskManager.h create mode 100644 xpcom/threads/LazyIdleThread.cpp create mode 100644 xpcom/threads/LazyIdleThread.h create mode 100644 xpcom/threads/LeakRefPtr.h create mode 100644 xpcom/threads/MainThreadIdlePeriod.cpp create mode 100644 xpcom/threads/MainThreadIdlePeriod.h create mode 100644 xpcom/threads/MainThreadUtils.h create mode 100644 xpcom/threads/Monitor.h create mode 100644 xpcom/threads/MozPromise.h create mode 100644 xpcom/threads/Mutex.h create mode 100644 xpcom/threads/Queue.h create mode 100644 xpcom/threads/RWLock.cpp create mode 100644 xpcom/threads/RWLock.h create mode 100644 xpcom/threads/RecursiveMutex.cpp create mode 100644 xpcom/threads/RecursiveMutex.h create mode 100644 xpcom/threads/ReentrantMonitor.h create mode 100644 xpcom/threads/SchedulerGroup.cpp create mode 100644 xpcom/threads/SchedulerGroup.h create mode 100644 xpcom/threads/SharedThreadPool.cpp create mode 100644 xpcom/threads/SharedThreadPool.h create mode 100644 xpcom/threads/SpinEventLoopUntil.h create mode 100644 xpcom/threads/StateMirroring.h create mode 100644 xpcom/threads/StateWatching.h create mode 100644 xpcom/threads/SyncRunnable.h create mode 100644 xpcom/threads/SynchronizedEventQueue.cpp create mode 100644 xpcom/threads/SynchronizedEventQueue.h create mode 100644 xpcom/threads/TaskController.cpp create mode 100644 xpcom/threads/TaskController.h create mode 100644 xpcom/threads/TaskDispatcher.h create mode 100644 xpcom/threads/TaskQueue.cpp create mode 100644 xpcom/threads/TaskQueue.h create mode 100644 xpcom/threads/ThreadBound.h create mode 100644 xpcom/threads/ThreadDelay.cpp create mode 100644 xpcom/threads/ThreadDelay.h create mode 100644 xpcom/threads/ThreadEventQueue.cpp create mode 100644 xpcom/threads/ThreadEventQueue.h create mode 100644 xpcom/threads/ThreadEventTarget.cpp create mode 100644 xpcom/threads/ThreadEventTarget.h create mode 100644 xpcom/threads/ThreadLocalVariables.cpp create mode 100644 xpcom/threads/ThrottledEventQueue.cpp create mode 100644 xpcom/threads/ThrottledEventQueue.h create mode 100644 xpcom/threads/TimerThread.cpp create mode 100644 xpcom/threads/TimerThread.h create mode 100644 xpcom/threads/VsyncTaskManager.cpp create mode 100644 xpcom/threads/VsyncTaskManager.h create mode 100644 xpcom/threads/WinHandleWatcher.cpp create mode 100644 xpcom/threads/WinHandleWatcher.h create mode 100644 xpcom/threads/components.conf create mode 100644 xpcom/threads/moz.build create mode 100644 xpcom/threads/nsEnvironment.cpp create mode 100644 xpcom/threads/nsEnvironment.h create mode 100644 xpcom/threads/nsICancelableRunnable.h create mode 100644 xpcom/threads/nsIDirectTaskDispatcher.idl create mode 100644 xpcom/threads/nsIDiscardableRunnable.h create mode 100644 xpcom/threads/nsIEnvironment.idl create mode 100644 xpcom/threads/nsIEventTarget.idl create mode 100644 xpcom/threads/nsIIdlePeriod.idl create mode 100644 xpcom/threads/nsIIdleRunnable.h create mode 100644 xpcom/threads/nsINamed.idl create mode 100644 xpcom/threads/nsIProcess.idl create mode 100644 xpcom/threads/nsIRunnable.idl create mode 100644 xpcom/threads/nsISerialEventTarget.idl create mode 100644 xpcom/threads/nsISupportsPriority.idl create mode 100644 xpcom/threads/nsITargetShutdownTask.h create mode 100644 xpcom/threads/nsIThread.idl create mode 100644 xpcom/threads/nsIThreadInternal.idl create mode 100644 xpcom/threads/nsIThreadManager.idl create mode 100644 xpcom/threads/nsIThreadPool.idl create mode 100644 xpcom/threads/nsIThreadShutdown.idl create mode 100644 xpcom/threads/nsITimer.idl create mode 100644 xpcom/threads/nsMemoryPressure.cpp create mode 100644 xpcom/threads/nsMemoryPressure.h create mode 100644 xpcom/threads/nsProcess.h create mode 100644 xpcom/threads/nsProcessCommon.cpp create mode 100644 xpcom/threads/nsProxyRelease.cpp create mode 100644 xpcom/threads/nsProxyRelease.h create mode 100644 xpcom/threads/nsThread.cpp create mode 100644 xpcom/threads/nsThread.h create mode 100644 xpcom/threads/nsThreadManager.cpp create mode 100644 xpcom/threads/nsThreadManager.h create mode 100644 xpcom/threads/nsThreadPool.cpp create mode 100644 xpcom/threads/nsThreadPool.h create mode 100644 xpcom/threads/nsThreadSyncDispatch.h create mode 100644 xpcom/threads/nsThreadUtils.cpp create mode 100644 xpcom/threads/nsThreadUtils.h create mode 100644 xpcom/threads/nsTimerImpl.cpp create mode 100644 xpcom/threads/nsTimerImpl.h (limited to 'xpcom/threads') diff --git a/xpcom/threads/AbstractThread.cpp b/xpcom/threads/AbstractThread.cpp new file mode 100644 index 0000000000..61289a6789 --- /dev/null +++ b/xpcom/threads/AbstractThread.cpp @@ -0,0 +1,359 @@ +/* -*- 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/AbstractThread.h" + +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/DelayedRunnable.h" +#include "mozilla/Maybe.h" +#include "mozilla/MozPromise.h" // We initialize the MozPromise logging in this file. +#include "mozilla/ProfilerRunnable.h" +#include "mozilla/StateWatching.h" // We initialize the StateWatching logging in this file. +#include "mozilla/StaticPtr.h" +#include "mozilla/TaskDispatcher.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/Unused.h" +#include "nsContentUtils.h" +#include "nsIDirectTaskDispatcher.h" +#include "nsIThreadInternal.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadManager.h" +#include "nsThreadUtils.h" +#include + +namespace mozilla { + +LazyLogModule gMozPromiseLog("MozPromise"); +LazyLogModule gStateWatchingLog("StateWatching"); + +StaticRefPtr sMainThread; +MOZ_THREAD_LOCAL(AbstractThread*) AbstractThread::sCurrentThreadTLS; + +class XPCOMThreadWrapper final : public AbstractThread, + public nsIThreadObserver, + public nsIDirectTaskDispatcher { + public: + XPCOMThreadWrapper(nsIThreadInternal* aThread, bool aRequireTailDispatch, + bool aOnThread) + : AbstractThread(aRequireTailDispatch), + mThread(aThread), + mDirectTaskDispatcher(do_QueryInterface(aThread)), + mOnThread(aOnThread) { + MOZ_DIAGNOSTIC_ASSERT(mThread && mDirectTaskDispatcher); + MOZ_DIAGNOSTIC_ASSERT(!aOnThread || IsCurrentThreadIn()); + if (aOnThread) { + MOZ_ASSERT(!sCurrentThreadTLS.get(), + "There can only be a single XPCOMThreadWrapper available on a " + "thread"); + // Set the default current thread so that GetCurrent() never returns + // nullptr. + sCurrentThreadTLS.set(this); + } + } + + NS_DECL_THREADSAFE_ISUPPORTS + + nsresult Dispatch(already_AddRefed aRunnable, + DispatchReason aReason = NormalDispatch) override { + nsCOMPtr r = aRunnable; + AbstractThread* currentThread; + if (aReason != TailDispatch && (currentThread = GetCurrent()) && + RequiresTailDispatch(currentThread) && + currentThread->IsTailDispatcherAvailable()) { + return currentThread->TailDispatcher().AddTask(this, r.forget()); + } + + // At a certain point during shutdown, we stop processing events from the + // main thread event queue (this happens long after all _other_ XPCOM + // threads have been shut down). However, various bits of subsequent + // teardown logic (the media shutdown blocker and the final shutdown cycle + // collection) can trigger state watching and state mirroring notifications + // that result in dispatch to the main thread. This causes shutdown leaks, + // because the |Runner| wrapper below creates a guaranteed cycle + // (Thread->EventQueue->Runnable->Thread) until the event is processed. So + // if we put the event into a queue that will never be processed, we'll wind + // up with a leak. + // + // We opt to just release the runnable in that case. Ordinarily, this + // approach could cause problems for runnables that are only safe to be + // released on the target thread (and not the dispatching thread). This is + // why XPCOM thread dispatch explicitly leaks the runnable when dispatch + // fails, rather than releasing it. But given that this condition only + // applies very late in shutdown when only one thread remains operational, + // that concern is unlikely to apply. + if (gXPCOMMainThreadEventsAreDoomed) { + return NS_ERROR_FAILURE; + } + + RefPtr runner = new Runner(this, r.forget()); + return mThread->Dispatch(runner.forget(), NS_DISPATCH_NORMAL); + } + + // Prevent a GCC warning about the other overload of Dispatch being hidden. + using AbstractThread::Dispatch; + + NS_IMETHOD RegisterShutdownTask(nsITargetShutdownTask* aTask) override { + return mThread->RegisterShutdownTask(aTask); + } + + NS_IMETHOD UnregisterShutdownTask(nsITargetShutdownTask* aTask) override { + return mThread->UnregisterShutdownTask(aTask); + } + + bool IsCurrentThreadIn() const override { + return mThread->IsOnCurrentThread(); + } + + TaskDispatcher& TailDispatcher() override { + MOZ_ASSERT(IsCurrentThreadIn()); + MOZ_ASSERT(IsTailDispatcherAvailable()); + if (!mTailDispatcher) { + mTailDispatcher = + std::make_unique(mDirectTaskDispatcher, + /* aIsTailDispatcher = */ true); + mThread->AddObserver(this); + } + + return *mTailDispatcher; + } + + bool IsTailDispatcherAvailable() override { + // Our tail dispatching implementation relies on nsIThreadObserver + // callbacks. If we're not doing event processing, it won't work. + bool inEventLoop = + static_cast(mThread.get())->RecursionDepth() > 0; + return inEventLoop; + } + + bool MightHaveTailTasks() override { return !!mTailDispatcher; } + + nsIEventTarget* AsEventTarget() override { return mThread; } + + //----------------------------------------------------------------------------- + // nsIThreadObserver + //----------------------------------------------------------------------------- + NS_IMETHOD OnDispatchedEvent() override { return NS_OK; } + + NS_IMETHOD AfterProcessNextEvent(nsIThreadInternal* thread, + bool eventWasProcessed) override { + // This is the primary case. + MaybeFireTailDispatcher(); + return NS_OK; + } + + NS_IMETHOD OnProcessNextEvent(nsIThreadInternal* thread, + bool mayWait) override { + // In general, the tail dispatcher is handled at the end of the current in + // AfterProcessNextEvent() above. However, if start spinning a nested event + // loop, it's generally better to fire the tail dispatcher before the first + // nested event, rather than after it. This check handles that case. + MaybeFireTailDispatcher(); + return NS_OK; + } + + //----------------------------------------------------------------------------- + // nsIDirectTaskDispatcher + //----------------------------------------------------------------------------- + // Forward calls to nsIDirectTaskDispatcher to the underlying nsThread object. + // We can't use the generated NS_FORWARD_NSIDIRECTTASKDISPATCHER macro + // as already_AddRefed type must be moved. + NS_IMETHOD DispatchDirectTask(already_AddRefed aEvent) override { + return mDirectTaskDispatcher->DispatchDirectTask(std::move(aEvent)); + } + NS_IMETHOD DrainDirectTasks() override { + return mDirectTaskDispatcher->DrainDirectTasks(); + } + NS_IMETHOD HaveDirectTasks(bool* aResult) override { + return mDirectTaskDispatcher->HaveDirectTasks(aResult); + } + + private: + const RefPtr mThread; + const nsCOMPtr mDirectTaskDispatcher; + std::unique_ptr mTailDispatcher; + const bool mOnThread; + + ~XPCOMThreadWrapper() { + if (mOnThread) { + MOZ_DIAGNOSTIC_ASSERT(IsCurrentThreadIn(), + "Must be destroyed on the thread it was created"); + sCurrentThreadTLS.set(nullptr); + } + } + + void MaybeFireTailDispatcher() { + if (mTailDispatcher) { + mTailDispatcher->DrainDirectTasks(); + mThread->RemoveObserver(this); + mTailDispatcher.reset(); + } + } + + class Runner : public Runnable { + public: + explicit Runner(XPCOMThreadWrapper* aThread, + already_AddRefed aRunnable) + : Runnable("XPCOMThreadWrapper::Runner"), + mThread(aThread), + mRunnable(aRunnable) {} + + NS_IMETHOD Run() override { + MOZ_ASSERT(mThread == AbstractThread::GetCurrent()); + MOZ_ASSERT(mThread->IsCurrentThreadIn()); + SerialEventTargetGuard guard(mThread); + AUTO_PROFILE_FOLLOWING_RUNNABLE(mRunnable); + return mRunnable->Run(); + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("AbstractThread::Runner"); + if (nsCOMPtr named = do_QueryInterface(mRunnable)) { + nsAutoCString name; + named->GetName(name); + if (!name.IsEmpty()) { + aName.AppendLiteral(" for "); + aName.Append(name); + } + } + return NS_OK; + } +#endif + + private: + const RefPtr mThread; + const RefPtr mRunnable; + }; +}; + +NS_IMPL_ISUPPORTS(XPCOMThreadWrapper, nsIThreadObserver, + nsIDirectTaskDispatcher, nsISerialEventTarget, nsIEventTarget) + +NS_IMETHODIMP_(bool) +AbstractThread::IsOnCurrentThreadInfallible() { return IsCurrentThreadIn(); } + +NS_IMETHODIMP +AbstractThread::IsOnCurrentThread(bool* aResult) { + *aResult = IsCurrentThreadIn(); + return NS_OK; +} + +NS_IMETHODIMP +AbstractThread::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + nsCOMPtr event(aEvent); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +AbstractThread::Dispatch(already_AddRefed aEvent, + uint32_t aFlags) { + return Dispatch(std::move(aEvent), NormalDispatch); +} + +NS_IMETHODIMP +AbstractThread::DelayedDispatch(already_AddRefed aEvent, + uint32_t aDelayMs) { + nsCOMPtr event = aEvent; + NS_ENSURE_TRUE(!!aDelayMs, NS_ERROR_UNEXPECTED); + + RefPtr r = + new DelayedRunnable(do_AddRef(this), event.forget(), aDelayMs); + nsresult rv = r->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + return Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +nsresult AbstractThread::TailDispatchTasksFor(AbstractThread* aThread) { + if (MightHaveTailTasks()) { + return TailDispatcher().DispatchTasksFor(aThread); + } + + return NS_OK; +} + +bool AbstractThread::HasTailTasksFor(AbstractThread* aThread) { + if (!MightHaveTailTasks()) { + return false; + } + return TailDispatcher().HasTasksFor(aThread); +} + +bool AbstractThread::RequiresTailDispatch(AbstractThread* aThread) const { + MOZ_ASSERT(aThread); + // We require tail dispatch if both the source and destination + // threads support it. + return SupportsTailDispatch() && aThread->SupportsTailDispatch(); +} + +bool AbstractThread::RequiresTailDispatchFromCurrentThread() const { + AbstractThread* current = GetCurrent(); + return current && RequiresTailDispatch(current); +} + +AbstractThread* AbstractThread::MainThread() { + MOZ_ASSERT(sMainThread); + return sMainThread; +} + +void AbstractThread::InitTLS() { + if (!sCurrentThreadTLS.init()) { + MOZ_CRASH(); + } +} + +void AbstractThread::InitMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!sMainThread); + nsCOMPtr mainThread = + do_QueryInterface(nsThreadManager::get().GetMainThreadWeak()); + MOZ_DIAGNOSTIC_ASSERT(mainThread); + + if (!sCurrentThreadTLS.init()) { + MOZ_CRASH(); + } + sMainThread = new XPCOMThreadWrapper(mainThread.get(), + /* aRequireTailDispatch = */ true, + true /* onThread */); +} + +void AbstractThread::ShutdownMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + sMainThread = nullptr; +} + +void AbstractThread::DispatchStateChange( + already_AddRefed aRunnable) { + AbstractThread* currentThread = GetCurrent(); + MOZ_DIAGNOSTIC_ASSERT(currentThread, "An AbstractThread must exist"); + if (currentThread->IsTailDispatcherAvailable()) { + currentThread->TailDispatcher().AddStateChangeTask(this, + std::move(aRunnable)); + } else { + // If the tail dispatcher isn't available, we just avoid sending state + // updates. + // + // This happens, specifically (1) During async shutdown (via the media + // shutdown blocker), and (2) During the final shutdown cycle collection. + // Both of these trigger changes to various watched and mirrored state. + nsCOMPtr neverDispatched = aRunnable; + } +} + +/* static */ +void AbstractThread::DispatchDirectTask( + already_AddRefed aRunnable) { + AbstractThread* currentThread = GetCurrent(); + MOZ_DIAGNOSTIC_ASSERT(currentThread, "An AbstractThread must exist"); + if (currentThread->IsTailDispatcherAvailable()) { + currentThread->TailDispatcher().AddDirectTask(std::move(aRunnable)); + } else { + // If the tail dispatcher isn't available, we post as a regular task. + currentThread->Dispatch(std::move(aRunnable)); + } +} + +} // namespace mozilla diff --git a/xpcom/threads/AbstractThread.h b/xpcom/threads/AbstractThread.h new file mode 100644 index 0000000000..b53bcf8ca3 --- /dev/null +++ b/xpcom/threads/AbstractThread.h @@ -0,0 +1,129 @@ +/* -*- 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/. */ + +#if !defined(AbstractThread_h_) +# define AbstractThread_h_ + +# include "mozilla/AlreadyAddRefed.h" +# include "mozilla/ThreadLocal.h" +# include "nscore.h" +# include "nsISerialEventTarget.h" +# include "nsISupports.h" + +class nsIEventTarget; +class nsIRunnable; +class nsIThread; + +namespace mozilla { + +class TaskDispatcher; + +/* + * We often want to run tasks on a target that guarantees that events will never + * run in parallel. There are various target types that achieve this - namely + * nsIThread and TaskQueue. Note that nsIThreadPool (which implements + * nsIEventTarget) does not have this property, so we do not want to use + * nsIEventTarget for this purpose. This class encapsulates the specifics of + * the structures we might use here and provides a consistent interface. + * + * At present, the supported AbstractThread implementations are TaskQueue, + * AbstractThread::MainThread() and XPCOMThreadWrapper which can wrap any + * nsThread. + * + * The primary use of XPCOMThreadWrapper is to allow any threads to provide + * Direct Task dispatching which is similar (but not identical to) the microtask + * semantics of JS promises. Instantiating a XPCOMThreadWrapper on the current + * nsThread is sufficient to enable direct task dispatching. + * + * You shouldn't use pointers when comparing AbstractThread or nsIThread to + * determine if you are currently on the thread, but instead use the + * nsISerialEventTarget::IsOnCurrentThread() method. + */ +class AbstractThread : public nsISerialEventTarget { + public: + // Returns the AbstractThread that the caller is currently running in, or null + // if the caller is not running in an AbstractThread. + static AbstractThread* GetCurrent() { return sCurrentThreadTLS.get(); } + + AbstractThread(bool aSupportsTailDispatch) + : mSupportsTailDispatch(aSupportsTailDispatch) {} + + // We don't use NS_DECL_NSIEVENTTARGET so that we can remove the default + // |flags| parameter from Dispatch. Otherwise, a single-argument Dispatch call + // would be ambiguous. + using nsISerialEventTarget::IsOnCurrentThread; + NS_IMETHOD_(bool) IsOnCurrentThreadInfallible(void) override; + NS_IMETHOD IsOnCurrentThread(bool* _retval) override; + NS_IMETHOD Dispatch(already_AddRefed event, + uint32_t flags) override; + NS_IMETHOD DispatchFromScript(nsIRunnable* event, uint32_t flags) override; + NS_IMETHOD DelayedDispatch(already_AddRefed event, + uint32_t delay) override; + + enum DispatchReason { NormalDispatch, TailDispatch }; + virtual nsresult Dispatch(already_AddRefed aRunnable, + DispatchReason aReason = NormalDispatch) = 0; + + virtual bool IsCurrentThreadIn() const = 0; + + // Returns a TaskDispatcher that will dispatch its tasks when the currently- + // running tasks pops off the stack. + // + // May only be called when running within the it is invoked up, and only on + // threads which support it. + virtual TaskDispatcher& TailDispatcher() = 0; + + // Returns true if we have tail tasks scheduled, or if this isn't known. + // Returns false if we definitely don't have any tail tasks. + virtual bool MightHaveTailTasks() { return true; } + + // Returns true if the tail dispatcher is available. In certain edge cases + // like shutdown, it might not be. + virtual bool IsTailDispatcherAvailable() { return true; } + + // Helper functions for methods on the tail TasklDispatcher. These check + // HasTailTasks to avoid allocating a TailDispatcher if it isn't + // needed. + nsresult TailDispatchTasksFor(AbstractThread* aThread); + bool HasTailTasksFor(AbstractThread* aThread); + + // Returns true if this supports the tail dispatcher. + bool SupportsTailDispatch() const { return mSupportsTailDispatch; } + + // Returns true if this thread requires all dispatches originating from + // aThread go through the tail dispatcher. + bool RequiresTailDispatch(AbstractThread* aThread) const; + bool RequiresTailDispatchFromCurrentThread() const; + + virtual nsIEventTarget* AsEventTarget() { MOZ_CRASH("Not an event target!"); } + + // Returns the non-DocGroup version of AbstractThread on the main thread. + // A DocGroup-versioned one is available in + // DispatcherTrait::AbstractThreadFor(). Note: + // DispatcherTrait::AbstractThreadFor() SHALL be used when possible. + static AbstractThread* MainThread(); + + // Must be called exactly once during startup. + static void InitTLS(); + static void InitMainThread(); + static void ShutdownMainThread(); + + void DispatchStateChange(already_AddRefed aRunnable); + + static void DispatchDirectTask(already_AddRefed aRunnable); + + protected: + virtual ~AbstractThread() = default; + static MOZ_THREAD_LOCAL(AbstractThread*) sCurrentThreadTLS; + + // True if we want to require that every task dispatched from tasks running in + // this queue go through our queue's tail dispatcher. + const bool mSupportsTailDispatch; +}; + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/BlockingResourceBase.cpp b/xpcom/threads/BlockingResourceBase.cpp new file mode 100644 index 0000000000..c2ba82e07a --- /dev/null +++ b/xpcom/threads/BlockingResourceBase.cpp @@ -0,0 +1,547 @@ +/* -*- 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/BlockingResourceBase.h" + +#ifdef DEBUG +# include "prthread.h" + +# ifndef MOZ_CALLSTACK_DISABLED +# include "CodeAddressService.h" +# include "nsHashKeys.h" +# include "mozilla/StackWalk.h" +# include "nsTHashtable.h" +# endif + +# include "mozilla/Attributes.h" +# include "mozilla/CondVar.h" +# include "mozilla/DeadlockDetector.h" +# include "mozilla/RecursiveMutex.h" +# include "mozilla/ReentrantMonitor.h" +# include "mozilla/Mutex.h" +# include "mozilla/RWLock.h" +# include "mozilla/UniquePtr.h" + +# if defined(MOZILLA_INTERNAL_API) +# include "mozilla/ProfilerThreadSleep.h" +# endif // MOZILLA_INTERNAL_API + +#endif // ifdef DEBUG + +namespace mozilla { +// +// BlockingResourceBase implementation +// + +// static members +const char* const BlockingResourceBase::kResourceTypeName[] = { + // needs to be kept in sync with BlockingResourceType + "Mutex", "ReentrantMonitor", "CondVar", "RecursiveMutex"}; + +#ifdef DEBUG + +PRCallOnceType BlockingResourceBase::sCallOnce; +MOZ_THREAD_LOCAL(BlockingResourceBase*) +BlockingResourceBase::sResourceAcqnChainFront; +BlockingResourceBase::DDT* BlockingResourceBase::sDeadlockDetector; + +void BlockingResourceBase::StackWalkCallback(uint32_t aFrameNumber, void* aPc, + void* aSp, void* aClosure) { +# ifndef MOZ_CALLSTACK_DISABLED + AcquisitionState* state = (AcquisitionState*)aClosure; + state->ref().AppendElement(aPc); +# endif +} + +void BlockingResourceBase::GetStackTrace(AcquisitionState& aState, + const void* aFirstFramePC) { +# ifndef MOZ_CALLSTACK_DISABLED + // Clear the array... + aState.reset(); + // ...and create a new one; this also puts the state to 'acquired' status + // regardless of whether we obtain a stack trace or not. + aState.emplace(); + + MozStackWalk(StackWalkCallback, aFirstFramePC, kAcquisitionStateStackSize, + aState.ptr()); +# endif +} + +/** + * PrintCycle + * Append to |aOut| detailed information about the circular + * dependency in |aCycle|. Returns true if it *appears* that this + * cycle may represent an imminent deadlock, but this is merely a + * heuristic; the value returned may be a false positive or false + * negative. + * + * *NOT* thread safe. Calls |Print()|. + * + * FIXME bug 456272 hack alert: because we can't write call + * contexts into strings, all info is written to stderr, but only + * some info is written into |aOut| + */ +static bool PrintCycle( + const BlockingResourceBase::DDT::ResourceAcquisitionArray& aCycle, + nsACString& aOut) { + NS_ASSERTION(aCycle.Length() > 1, "need > 1 element for cycle!"); + + bool maybeImminent = true; + + fputs("=== Cyclical dependency starts at\n", stderr); + aOut += "Cyclical dependency starts at\n"; + + const BlockingResourceBase::DDT::ResourceAcquisitionArray::value_type res = + aCycle.ElementAt(0); + maybeImminent &= res->Print(aOut); + + BlockingResourceBase::DDT::ResourceAcquisitionArray::index_type i; + BlockingResourceBase::DDT::ResourceAcquisitionArray::size_type len = + aCycle.Length(); + const BlockingResourceBase::DDT::ResourceAcquisitionArray::value_type* it = + 1 + aCycle.Elements(); + for (i = 1; i < len - 1; ++i, ++it) { + fputs("\n--- Next dependency:\n", stderr); + aOut += "\nNext dependency:\n"; + + maybeImminent &= (*it)->Print(aOut); + } + + fputs("\n=== Cycle completed at\n", stderr); + aOut += "Cycle completed at\n"; + (*it)->Print(aOut); + + return maybeImminent; +} + +bool BlockingResourceBase::Print(nsACString& aOut) const { + fprintf(stderr, "--- %s : %s", kResourceTypeName[mType], mName); + aOut += BlockingResourceBase::kResourceTypeName[mType]; + aOut += " : "; + aOut += mName; + + bool acquired = IsAcquired(); + + if (acquired) { + fputs(" (currently acquired)\n", stderr); + aOut += " (currently acquired)\n"; + } + + fputs(" calling context\n", stderr); +# ifdef MOZ_CALLSTACK_DISABLED + fputs(" [stack trace unavailable]\n", stderr); +# else + const AcquisitionState& state = acquired ? mAcquired : mFirstSeen; + + CodeAddressService<> addressService; + + for (uint32_t i = 0; i < state.ref().Length(); i++) { + const size_t kMaxLength = 1024; + char buffer[kMaxLength]; + addressService.GetLocation(i + 1, state.ref()[i], buffer, kMaxLength); + const char* fmt = " %s\n"; + aOut.AppendLiteral(" "); + aOut.Append(buffer); + aOut.AppendLiteral("\n"); + fprintf(stderr, fmt, buffer); + } + +# endif + + return acquired; +} + +BlockingResourceBase::BlockingResourceBase( + const char* aName, BlockingResourceBase::BlockingResourceType aType) + : mName(aName), + mType(aType) +# ifdef MOZ_CALLSTACK_DISABLED + , + mAcquired(false) +# else + , + mAcquired() +# endif +{ + MOZ_ASSERT(mName, "Name must be nonnull"); + // PR_CallOnce guaranatees that InitStatics is called in a + // thread-safe way + if (PR_SUCCESS != PR_CallOnce(&sCallOnce, InitStatics)) { + MOZ_CRASH("can't initialize blocking resource static members"); + } + + mChainPrev = 0; + sDeadlockDetector->Add(this); +} + +BlockingResourceBase::~BlockingResourceBase() { + // we don't check for really obviously bad things like freeing + // Mutexes while they're still locked. it is assumed that the + // base class, or its underlying primitive, will check for such + // stupid mistakes. + mChainPrev = 0; // racy only for stupidly buggy client code + if (sDeadlockDetector) { + sDeadlockDetector->Remove(this); + } +} + +size_t BlockingResourceBase::SizeOfDeadlockDetector( + MallocSizeOf aMallocSizeOf) { + return sDeadlockDetector + ? sDeadlockDetector->SizeOfIncludingThis(aMallocSizeOf) + : 0; +} + +PRStatus BlockingResourceBase::InitStatics() { + MOZ_ASSERT(sResourceAcqnChainFront.init()); + sDeadlockDetector = new DDT(); + if (!sDeadlockDetector) { + MOZ_CRASH("can't allocate deadlock detector"); + } + return PR_SUCCESS; +} + +void BlockingResourceBase::Shutdown() { + delete sDeadlockDetector; + sDeadlockDetector = 0; +} + +MOZ_NEVER_INLINE void BlockingResourceBase::CheckAcquire() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow CheckAcquire()ing condvars"); + return; + } + + BlockingResourceBase* chainFront = ResourceChainFront(); + mozilla::UniquePtr cycle( + sDeadlockDetector->CheckAcquisition(chainFront ? chainFront : 0, this)); + if (!cycle) { + return; + } + +# ifndef MOZ_CALLSTACK_DISABLED + // Update the current stack before printing. + GetStackTrace(mAcquired, CallerPC()); +# endif + + fputs("###!!! ERROR: Potential deadlock detected:\n", stderr); + nsAutoCString out("Potential deadlock detected:\n"); + bool maybeImminent = PrintCycle(*cycle, out); + + if (maybeImminent) { + fputs("\n###!!! Deadlock may happen NOW!\n\n", stderr); + out.AppendLiteral("\n###!!! Deadlock may happen NOW!\n\n"); + } else { + fputs("\nDeadlock may happen for some other execution\n\n", stderr); + out.AppendLiteral("\nDeadlock may happen for some other execution\n\n"); + } + + // Only error out if we think a deadlock is imminent. + if (maybeImminent) { + NS_ERROR(out.get()); + } else { + NS_WARNING(out.get()); + } +} + +MOZ_NEVER_INLINE void BlockingResourceBase::Acquire() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow Acquire()ing condvars"); + return; + } + NS_ASSERTION(!IsAcquired(), "reacquiring already acquired resource"); + + ResourceChainAppend(ResourceChainFront()); + +# ifdef MOZ_CALLSTACK_DISABLED + mAcquired = true; +# else + // Take a stack snapshot. + GetStackTrace(mAcquired, CallerPC()); + MOZ_ASSERT(IsAcquired()); + + if (!mFirstSeen) { + mFirstSeen = mAcquired.map( + [](AcquisitionState::ValueType& state) { return state.Clone(); }); + } +# endif +} + +void BlockingResourceBase::Release() { + if (mType == eCondVar) { + MOZ_ASSERT_UNREACHABLE( + "FIXME bug 456272: annots. to allow Release()ing condvars"); + return; + } + + BlockingResourceBase* chainFront = ResourceChainFront(); + NS_ASSERTION(chainFront && IsAcquired(), + "Release()ing something that hasn't been Acquire()ed"); + + if (chainFront == this) { + ResourceChainRemove(); + } else { + // remove this resource from wherever it lives in the chain + // we walk backwards in order of acquisition: + // (1) ...node<-prev<-curr... + // / / + // (2) ...prev<-curr... + BlockingResourceBase* curr = chainFront; + BlockingResourceBase* prev = nullptr; + while (curr && (prev = curr->mChainPrev) && (prev != this)) { + curr = prev; + } + if (prev == this) { + curr->mChainPrev = prev->mChainPrev; + } + } + + ClearAcquisitionState(); +} + +// +// Debug implementation of (OffTheBooks)Mutex +void OffTheBooksMutex::Lock() { + CheckAcquire(); + this->lock(); + mOwningThread = PR_GetCurrentThread(); + Acquire(); +} + +bool OffTheBooksMutex::TryLock() { + bool locked = this->tryLock(); + if (locked) { + mOwningThread = PR_GetCurrentThread(); + Acquire(); + } + return locked; +} + +void OffTheBooksMutex::Unlock() { + Release(); + mOwningThread = nullptr; + this->unlock(); +} + +void OffTheBooksMutex::AssertCurrentThreadOwns() const { + MOZ_ASSERT(IsAcquired() && mOwningThread == PR_GetCurrentThread()); +} + +// +// Debug implementation of RWLock +// + +bool RWLock::TryReadLock() { + bool locked = this->detail::RWLockImpl::tryReadLock(); + MOZ_ASSERT_IF(locked, mOwningThread == nullptr); + return locked; +} + +void RWLock::ReadLock() { + // All we want to ensure here is that we're not attempting to acquire the + // read lock while this thread is holding the write lock. + CheckAcquire(); + this->detail::RWLockImpl::readLock(); + MOZ_ASSERT(mOwningThread == nullptr); +} + +void RWLock::ReadUnlock() { + MOZ_ASSERT(mOwningThread == nullptr); + this->detail::RWLockImpl::readUnlock(); +} + +bool RWLock::TryWriteLock() { + bool locked = this->detail::RWLockImpl::tryWriteLock(); + if (locked) { + mOwningThread = PR_GetCurrentThread(); + Acquire(); + } + return locked; +} + +void RWLock::WriteLock() { + CheckAcquire(); + this->detail::RWLockImpl::writeLock(); + mOwningThread = PR_GetCurrentThread(); + Acquire(); +} + +void RWLock::WriteUnlock() { + Release(); + mOwningThread = nullptr; + this->detail::RWLockImpl::writeUnlock(); +} + +// +// Debug implementation of ReentrantMonitor +void ReentrantMonitor::Enter() { + BlockingResourceBase* chainFront = ResourceChainFront(); + + // the code below implements monitor reentrancy semantics + + if (this == chainFront) { + // immediately re-entered the monitor: acceptable + PR_EnterMonitor(mReentrantMonitor); + ++mEntryCount; + return; + } + + // this is sort of a hack around not recording the thread that + // owns this monitor + if (chainFront) { + for (BlockingResourceBase* br = ResourceChainPrev(chainFront); br; + br = ResourceChainPrev(br)) { + if (br == this) { + NS_WARNING( + "Re-entering ReentrantMonitor after acquiring other resources."); + + // show the caller why this is potentially bad + CheckAcquire(); + + PR_EnterMonitor(mReentrantMonitor); + ++mEntryCount; + return; + } + } + } + + CheckAcquire(); + PR_EnterMonitor(mReentrantMonitor); + NS_ASSERTION(mEntryCount == 0, "ReentrantMonitor isn't free!"); + Acquire(); // protected by mReentrantMonitor + mEntryCount = 1; +} + +void ReentrantMonitor::Exit() { + if (--mEntryCount == 0) { + Release(); // protected by mReentrantMonitor + } + PRStatus status = PR_ExitMonitor(mReentrantMonitor); + NS_ASSERTION(PR_SUCCESS == status, "bad ReentrantMonitor::Exit()"); +} + +nsresult ReentrantMonitor::Wait(PRIntervalTime aInterval) { + AssertCurrentThreadIn(); + + // save monitor state and reset it to empty + int32_t savedEntryCount = mEntryCount; + AcquisitionState savedAcquisitionState = TakeAcquisitionState(); + BlockingResourceBase* savedChainPrev = mChainPrev; + mEntryCount = 0; + mChainPrev = 0; + + nsresult rv; + { +# if defined(MOZILLA_INTERNAL_API) + AUTO_PROFILER_THREAD_SLEEP; +# endif + // give up the monitor until we're back from Wait() + rv = PR_Wait(mReentrantMonitor, aInterval) == PR_SUCCESS ? NS_OK + : NS_ERROR_FAILURE; + } + + // restore saved state + mEntryCount = savedEntryCount; + SetAcquisitionState(std::move(savedAcquisitionState)); + mChainPrev = savedChainPrev; + + return rv; +} + +// +// Debug implementation of RecursiveMutex +void RecursiveMutex::Lock() { + BlockingResourceBase* chainFront = ResourceChainFront(); + + // the code below implements mutex reentrancy semantics + + if (this == chainFront) { + // immediately re-entered the mutex: acceptable + LockInternal(); + ++mEntryCount; + return; + } + + // this is sort of a hack around not recording the thread that + // owns this monitor + if (chainFront) { + for (BlockingResourceBase* br = ResourceChainPrev(chainFront); br; + br = ResourceChainPrev(br)) { + if (br == this) { + NS_WARNING( + "Re-entering RecursiveMutex after acquiring other resources."); + + // show the caller why this is potentially bad + CheckAcquire(); + + LockInternal(); + ++mEntryCount; + return; + } + } + } + + CheckAcquire(); + LockInternal(); + NS_ASSERTION(mEntryCount == 0, "RecursiveMutex isn't free!"); + Acquire(); // protected by us + mOwningThread = PR_GetCurrentThread(); + mEntryCount = 1; +} + +void RecursiveMutex::Unlock() { + if (--mEntryCount == 0) { + Release(); // protected by us + mOwningThread = nullptr; + } + UnlockInternal(); +} + +void RecursiveMutex::AssertCurrentThreadIn() const { + MOZ_ASSERT(IsAcquired() && mOwningThread == PR_GetCurrentThread()); +} + +// +// Debug implementation of CondVar +void OffTheBooksCondVar::Wait() { + // Forward to the timed version of OffTheBooksCondVar::Wait to avoid code + // duplication. + CVStatus status = Wait(TimeDuration::Forever()); + MOZ_ASSERT(status == CVStatus::NoTimeout); +} + +CVStatus OffTheBooksCondVar::Wait(TimeDuration aDuration) { + AssertCurrentThreadOwnsMutex(); + + // save mutex state and reset to empty + AcquisitionState savedAcquisitionState = mLock->TakeAcquisitionState(); + BlockingResourceBase* savedChainPrev = mLock->mChainPrev; + PRThread* savedOwningThread = mLock->mOwningThread; + mLock->mChainPrev = 0; + mLock->mOwningThread = nullptr; + + // give up mutex until we're back from Wait() + CVStatus status; + { +# if defined(MOZILLA_INTERNAL_API) + AUTO_PROFILER_THREAD_SLEEP; +# endif + status = mImpl.wait_for(*mLock, aDuration); + } + + // restore saved state + mLock->SetAcquisitionState(std::move(savedAcquisitionState)); + mLock->mChainPrev = savedChainPrev; + mLock->mOwningThread = savedOwningThread; + + return status; +} + +#endif // ifdef DEBUG + +} // namespace mozilla diff --git a/xpcom/threads/BlockingResourceBase.h b/xpcom/threads/BlockingResourceBase.h new file mode 100644 index 0000000000..8bb7a78f6f --- /dev/null +++ b/xpcom/threads/BlockingResourceBase.h @@ -0,0 +1,339 @@ +/* -*- 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 mozilla_BlockingResourceBase_h +#define mozilla_BlockingResourceBase_h + +#include "mozilla/MemoryReporting.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/Attributes.h" + +#include "nscore.h" +#include "nsDebug.h" + +#include "prtypes.h" + +#ifdef DEBUG + +// NB: Comment this out to enable callstack tracking. +# define MOZ_CALLSTACK_DISABLED + +# include "prinit.h" + +# ifndef MOZ_CALLSTACK_DISABLED +# include "mozilla/Maybe.h" +# include "nsTArray.h" +# endif + +#endif + +// +// This header is not meant to be included by client code. +// + +namespace mozilla { + +#ifdef DEBUG +template +class DeadlockDetector; +#endif + +/** + * BlockingResourceBase + * Base class of resources that might block clients trying to acquire them. + * Does debugging and deadlock detection in DEBUG builds. + **/ +class BlockingResourceBase { + public: + // Needs to be kept in sync with kResourceTypeNames. + enum BlockingResourceType { + eMutex, + eReentrantMonitor, + eCondVar, + eRecursiveMutex + }; + + /** + * kResourceTypeName + * Human-readable version of BlockingResourceType enum. + */ + static const char* const kResourceTypeName[]; + +#ifdef DEBUG + + static size_t SizeOfDeadlockDetector(MallocSizeOf aMallocSizeOf); + + /** + * Print + * Write a description of this blocking resource to |aOut|. If + * the resource appears to be currently acquired, the current + * acquisition context is printed and true is returned. + * Otherwise, we print the context from |aFirstSeen|, the + * first acquisition from which the code calling |Print()| + * became interested in us, and return false. + * + * *NOT* thread safe. Reads |mAcquisitionContext| without + * synchronization, but this will not cause correctness + * problems. + * + * FIXME bug 456272: hack alert: because we can't write call + * contexts into strings, all info is written to stderr, but + * only some info is written into |aOut| + */ + bool Print(nsACString& aOut) const; + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + // NB: |mName| is not reported as it's expected to be a static string. + // If we switch to a nsString it should be added to the tally. + // |mChainPrev| is not reported because its memory is not owned. + size_t n = aMallocSizeOf(this); + return n; + } + + // ``DDT'' = ``Deadlock Detector Type'' + typedef DeadlockDetector DDT; + + protected: +# ifdef MOZ_CALLSTACK_DISABLED + typedef bool AcquisitionState; +# else + // Using maybe to use emplacement as the acquisition state flag; we may not + // always get a stack trace because of possible stack walk suppression or + // errors, hence can't use !IsEmpty() on the array itself as indication. + static size_t const kAcquisitionStateStackSize = 24; + typedef Maybe > + AcquisitionState; +# endif + + /** + * BlockingResourceBase + * Initialize this blocking resource. Also hooks the resource into + * instrumentation code. + * + * Thread safe. + * + * @param aName A meaningful, unique name that can be used in + * error messages, et al. + * @param aType The specific type of |this|, if any. + **/ + BlockingResourceBase(const char* aName, BlockingResourceType aType); + + ~BlockingResourceBase(); + + /** + * CheckAcquire + * + * Thread safe. + **/ + void CheckAcquire(); + + /** + * Acquire + * + * *NOT* thread safe. Requires ownership of underlying resource. + **/ + void Acquire(); // NS_NEEDS_RESOURCE(this) + + /** + * Release + * Remove this resource from the current thread's acquisition chain. + * The resource does not have to be at the front of the chain, although + * it is confusing to release resources in a different order than they + * are acquired. This generates a warning. + * + * *NOT* thread safe. Requires ownership of underlying resource. + **/ + void Release(); // NS_NEEDS_RESOURCE(this) + + /** + * ResourceChainFront + * + * Thread safe. + * + * @return the front of the resource acquisition chain, i.e., the last + * resource acquired. + */ + static BlockingResourceBase* ResourceChainFront() { + return sResourceAcqnChainFront.get(); + } + + /** + * ResourceChainPrev + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + static BlockingResourceBase* ResourceChainPrev( + const BlockingResourceBase* aResource) { + return aResource->mChainPrev; + } // NS_NEEDS_RESOURCE(this) + + /** + * ResourceChainAppend + * Set |this| to the front of the resource acquisition chain, and link + * |this| to |aPrev|. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + void ResourceChainAppend(BlockingResourceBase* aPrev) { + mChainPrev = aPrev; + sResourceAcqnChainFront.set(this); + } // NS_NEEDS_RESOURCE(this) + + /** + * ResourceChainRemove + * Remove |this| from the front of the resource acquisition chain. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + void ResourceChainRemove() { + NS_ASSERTION(this == ResourceChainFront(), "not at chain front"); + sResourceAcqnChainFront.set(mChainPrev); + } // NS_NEEDS_RESOURCE(this) + + /** + * TakeAcquisitionState + * Return whether or not this resource was acquired and mark the resource + * as not acquired for subsequent uses. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + AcquisitionState TakeAcquisitionState() { +# ifdef MOZ_CALLSTACK_DISABLED + bool acquired = mAcquired; + ClearAcquisitionState(); + return acquired; +# else + return mAcquired.take(); +# endif + } + + /** + * SetAcquisitionState + * Set whether or not this resource was acquired. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + void SetAcquisitionState(AcquisitionState&& aAcquisitionState) { + mAcquired = std::move(aAcquisitionState); + } + + /** + * ClearAcquisitionState + * Indicate this resource is not acquired. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + void ClearAcquisitionState() { +# ifdef MOZ_CALLSTACK_DISABLED + mAcquired = false; +# else + mAcquired.reset(); +# endif + } + + /** + * IsAcquired + * Indicates if this resource is acquired. + * + * *NOT* thread safe. Requires ownership of underlying resource. + */ + bool IsAcquired() const { return (bool)mAcquired; } + + /** + * mChainPrev + * A series of resource acquisitions creates a chain of orders. This + * chain is implemented as a linked list; |mChainPrev| points to the + * resource most recently Acquire()'d before this one. + **/ + BlockingResourceBase* mChainPrev; + + private: + /** + * mName + * A descriptive name for this resource. Used in error + * messages etc. + */ + const char* mName; + + /** + * mType + * The more specific type of this resource. Used to implement + * special semantics (e.g., reentrancy of monitors). + **/ + BlockingResourceType mType; + + /** + * mAcquired + * Indicates if this resource is currently acquired. + */ + AcquisitionState mAcquired; + +# ifndef MOZ_CALLSTACK_DISABLED + /** + * mFirstSeen + * Inidicates where this resource was first acquired. + */ + AcquisitionState mFirstSeen; +# endif + + /** + * sCallOnce + * Ensures static members are initialized only once, and in a + * thread-safe way. + */ + static PRCallOnceType sCallOnce; + + /** + * Thread-private pointer to the front of each thread's resource + * acquisition chain. + */ + static MOZ_THREAD_LOCAL(BlockingResourceBase*) sResourceAcqnChainFront; + + /** + * sDeadlockDetector + * Does as named. + */ + static DDT* sDeadlockDetector; + + /** + * InitStatics + * Inititialize static members of BlockingResourceBase that can't + * be statically initialized. + * + * *NOT* thread safe. + */ + static PRStatus InitStatics(); + + /** + * Shutdown + * Free static members. + * + * *NOT* thread safe. + */ + static void Shutdown(); + + static void StackWalkCallback(uint32_t aFrameNumber, void* aPc, void* aSp, + void* aClosure); + static void GetStackTrace(AcquisitionState& aState, + const void* aFirstFramePC); + +# ifdef MOZILLA_INTERNAL_API + // so it can call BlockingResourceBase::Shutdown() + friend void LogTerm(); +# endif // ifdef MOZILLA_INTERNAL_API + +#else // non-DEBUG implementation + + BlockingResourceBase(const char* aName, BlockingResourceType aType) {} + + ~BlockingResourceBase() {} + +#endif +}; + +} // namespace mozilla + +#endif // mozilla_BlockingResourceBase_h diff --git a/xpcom/threads/CPUUsageWatcher.cpp b/xpcom/threads/CPUUsageWatcher.cpp new file mode 100644 index 0000000000..66579b6f44 --- /dev/null +++ b/xpcom/threads/CPUUsageWatcher.cpp @@ -0,0 +1,253 @@ +/* -*- 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/CPUUsageWatcher.h" +#include "mozilla/Try.h" + +#include "prsystem.h" + +#ifdef XP_MACOSX +# include +# include +# include +#endif + +#ifdef CPU_USAGE_WATCHER_ACTIVE +# include "mozilla/BackgroundHangMonitor.h" +#endif + +namespace mozilla { + +#ifdef CPU_USAGE_WATCHER_ACTIVE + +// Even if the machine only has one processor, tolerate up to 50% +// external CPU usage. +static const float kTolerableExternalCPUUsageFloor = 0.5f; + +struct CPUStats { + // The average CPU usage time, which can be summed across all cores in the + // system, or averaged between them. Whichever it is, it needs to be in the + // same units as updateTime. + uint64_t usageTime; + // A monotonically increasing value in the same units as usageTime, which can + // be used to determine the percentage of active vs idle time + uint64_t updateTime; +}; + +# ifdef XP_MACOSX + +static const uint64_t kMicrosecondsPerSecond = 1000000LL; +static const uint64_t kNanosecondsPerMicrosecond = 1000LL; + +static uint64_t GetMicroseconds(timeval time) { + return ((uint64_t)time.tv_sec) * kMicrosecondsPerSecond + + (uint64_t)time.tv_usec; +} + +static uint64_t GetMicroseconds(mach_timespec_t time) { + return ((uint64_t)time.tv_sec) * kMicrosecondsPerSecond + + ((uint64_t)time.tv_nsec) / kNanosecondsPerMicrosecond; +} + +static Result GetProcessCPUStats( + int32_t numCPUs) { + CPUStats result = {}; + rusage usage; + int32_t rusageResult = getrusage(RUSAGE_SELF, &usage); + if (rusageResult == -1) { + return Err(GetProcessTimesError); + } + result.usageTime = + GetMicroseconds(usage.ru_utime) + GetMicroseconds(usage.ru_stime); + + clock_serv_t realtimeClock; + kern_return_t errorResult = + host_get_clock_service(mach_host_self(), REALTIME_CLOCK, &realtimeClock); + if (errorResult != KERN_SUCCESS) { + return Err(GetProcessTimesError); + } + mach_timespec_t time; + errorResult = clock_get_time(realtimeClock, &time); + if (errorResult != KERN_SUCCESS) { + return Err(GetProcessTimesError); + } + result.updateTime = GetMicroseconds(time); + + // getrusage will give us the sum of the values across all + // of our cores. Divide by the number of CPUs to get an average. + result.usageTime /= numCPUs; + return result; +} + +static Result GetGlobalCPUStats() { + CPUStats result = {}; + host_cpu_load_info_data_t loadInfo; + mach_msg_type_number_t loadInfoCount = HOST_CPU_LOAD_INFO_COUNT; + kern_return_t statsResult = + host_statistics(mach_host_self(), HOST_CPU_LOAD_INFO, + (host_info_t)&loadInfo, &loadInfoCount); + if (statsResult != KERN_SUCCESS) { + return Err(HostStatisticsError); + } + + result.usageTime = loadInfo.cpu_ticks[CPU_STATE_USER] + + loadInfo.cpu_ticks[CPU_STATE_NICE] + + loadInfo.cpu_ticks[CPU_STATE_SYSTEM]; + result.updateTime = result.usageTime + loadInfo.cpu_ticks[CPU_STATE_IDLE]; + return result; +} + +# endif // XP_MACOSX + +# ifdef XP_WIN + +// A FILETIME represents the number of 100-nanosecond ticks since 1/1/1601 UTC +uint64_t FiletimeToInteger(FILETIME filetime) { + return ((uint64_t)filetime.dwLowDateTime) | (uint64_t)filetime.dwHighDateTime + << 32; +} + +Result GetProcessCPUStats(int32_t numCPUs) { + CPUStats result = {}; + FILETIME creationFiletime; + FILETIME exitFiletime; + FILETIME kernelFiletime; + FILETIME userFiletime; + bool success = GetProcessTimes(GetCurrentProcess(), &creationFiletime, + &exitFiletime, &kernelFiletime, &userFiletime); + if (!success) { + return Err(GetProcessTimesError); + } + + result.usageTime = + FiletimeToInteger(kernelFiletime) + FiletimeToInteger(userFiletime); + + FILETIME nowFiletime; + GetSystemTimeAsFileTime(&nowFiletime); + result.updateTime = FiletimeToInteger(nowFiletime); + + result.usageTime /= numCPUs; + + return result; +} + +Result GetGlobalCPUStats() { + CPUStats result = {}; + FILETIME idleFiletime; + FILETIME kernelFiletime; + FILETIME userFiletime; + bool success = GetSystemTimes(&idleFiletime, &kernelFiletime, &userFiletime); + + if (!success) { + return Err(GetSystemTimesError); + } + + result.usageTime = + FiletimeToInteger(kernelFiletime) + FiletimeToInteger(userFiletime); + result.updateTime = result.usageTime + FiletimeToInteger(idleFiletime); + + return result; +} + +# endif // XP_WIN + +Result CPUUsageWatcher::Init() { + mNumCPUs = PR_GetNumberOfProcessors(); + if (mNumCPUs <= 0) { + mExternalUsageThreshold = 1.0f; + return Err(GetNumberOfProcessorsError); + } + mExternalUsageThreshold = + std::max(1.0f - 1.0f / (float)mNumCPUs, kTolerableExternalCPUUsageFloor); + + CPUStats processTimes; + MOZ_TRY_VAR(processTimes, GetProcessCPUStats(mNumCPUs)); + mProcessUpdateTime = processTimes.updateTime; + mProcessUsageTime = processTimes.usageTime; + + CPUStats globalTimes; + MOZ_TRY_VAR(globalTimes, GetGlobalCPUStats()); + mGlobalUpdateTime = globalTimes.updateTime; + mGlobalUsageTime = globalTimes.usageTime; + + mInitialized = true; + + CPUUsageWatcher* self = this; + NS_DispatchToMainThread(NS_NewRunnableFunction( + "CPUUsageWatcher::Init", + [=]() { BackgroundHangMonitor::RegisterAnnotator(*self); })); + + return Ok(); +} + +void CPUUsageWatcher::Uninit() { + if (mInitialized) { + BackgroundHangMonitor::UnregisterAnnotator(*this); + } + mInitialized = false; +} + +Result CPUUsageWatcher::CollectCPUUsage() { + if (!mInitialized) { + return Ok(); + } + + mExternalUsageRatio = 0.0f; + + CPUStats processTimes; + MOZ_TRY_VAR(processTimes, GetProcessCPUStats(mNumCPUs)); + CPUStats globalTimes; + MOZ_TRY_VAR(globalTimes, GetGlobalCPUStats()); + + uint64_t processUsageDelta = processTimes.usageTime - mProcessUsageTime; + uint64_t processUpdateDelta = processTimes.updateTime - mProcessUpdateTime; + float processUsageNormalized = + processUsageDelta > 0 + ? (float)processUsageDelta / (float)processUpdateDelta + : 0.0f; + + uint64_t globalUsageDelta = globalTimes.usageTime - mGlobalUsageTime; + uint64_t globalUpdateDelta = globalTimes.updateTime - mGlobalUpdateTime; + float globalUsageNormalized = + globalUsageDelta > 0 ? (float)globalUsageDelta / (float)globalUpdateDelta + : 0.0f; + + mProcessUsageTime = processTimes.usageTime; + mProcessUpdateTime = processTimes.updateTime; + mGlobalUsageTime = globalTimes.usageTime; + mGlobalUpdateTime = globalTimes.updateTime; + + mExternalUsageRatio = + std::max(0.0f, globalUsageNormalized - processUsageNormalized); + + return Ok(); +} + +void CPUUsageWatcher::AnnotateHang(BackgroundHangAnnotations& aAnnotations) { + if (!mInitialized) { + return; + } + + if (mExternalUsageRatio > mExternalUsageThreshold) { + aAnnotations.AddAnnotation(u"ExternalCPUHigh"_ns, true); + } +} + +#else // !CPU_USAGE_WATCHER_ACTIVE + +Result CPUUsageWatcher::Init() { return Ok(); } + +void CPUUsageWatcher::Uninit() {} + +Result CPUUsageWatcher::CollectCPUUsage() { + return Ok(); +} + +void CPUUsageWatcher::AnnotateHang(BackgroundHangAnnotations& aAnnotations) {} + +#endif // CPU_USAGE_WATCHER_ACTIVE + +} // namespace mozilla diff --git a/xpcom/threads/CPUUsageWatcher.h b/xpcom/threads/CPUUsageWatcher.h new file mode 100644 index 0000000000..c3a643378a --- /dev/null +++ b/xpcom/threads/CPUUsageWatcher.h @@ -0,0 +1,100 @@ +/* -*- 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 mozilla_CPUUsageWatcher_h +#define mozilla_CPUUsageWatcher_h + +#include + +#include "mozilla/HangAnnotations.h" +#include "mozilla/Result.h" + +// We only support OSX and Windows, because on Linux we're forced to read +// from /proc/stat in order to get global CPU values. We would prefer to not +// eat that cost for this. +#if defined(NIGHTLY_BUILD) && (defined(XP_WIN) || defined(XP_MACOSX)) +# define CPU_USAGE_WATCHER_ACTIVE +#endif + +namespace mozilla { + +// Start error values at 1 to allow using the UnusedZero Result +// optimization. +enum CPUUsageWatcherError : uint8_t { + ClockGetTimeError = 1, + GetNumberOfProcessorsError, + GetProcessTimesError, + GetSystemTimesError, + HostStatisticsError, + ProcStatError, +}; + +namespace detail { + +template <> +struct UnusedZero : UnusedZeroEnum { +}; + +} // namespace detail + +class CPUUsageHangAnnotator : public BackgroundHangAnnotator { + public: +}; + +class CPUUsageWatcher : public BackgroundHangAnnotator { + public: +#ifdef CPU_USAGE_WATCHER_ACTIVE + CPUUsageWatcher() + : mInitialized(false), + mExternalUsageThreshold(0), + mExternalUsageRatio(0), + mProcessUsageTime(0), + mProcessUpdateTime(0), + mGlobalUsageTime(0), + mGlobalUpdateTime(0), + mNumCPUs(0) {} +#endif + + Result Init(); + + void Uninit(); + + // Updates necessary values to allow AnnotateHang to function. This must be + // called on some semi-regular basis, as it will calculate the mean CPU + // usage values between now and the last time it was called. + Result CollectCPUUsage(); + + void AnnotateHang(BackgroundHangAnnotations& aAnnotations) final; + + private: +#ifdef CPU_USAGE_WATCHER_ACTIVE + bool mInitialized; + // The threshold above which we will mark a hang as occurring under high + // external CPU usage conditions + float mExternalUsageThreshold; + // The CPU usage (0-1) external to our process, averaged between the two + // most recent monitor thread runs + float mExternalUsageRatio; + // The total cumulative CPU usage time by our process as of the last + // CollectCPUUsage or Startup + uint64_t mProcessUsageTime; + // A time value in the same units as mProcessUsageTime used to + // determine the ratio of CPU usage time to idle time + uint64_t mProcessUpdateTime; + // The total cumulative CPU usage time by all processes as of the last + // CollectCPUUsage or Startup + uint64_t mGlobalUsageTime; + // A time value in the same units as mGlobalUsageTime used to + // determine the ratio of CPU usage time to idle time + uint64_t mGlobalUpdateTime; + // The number of virtual cores on our machine + uint64_t mNumCPUs; +#endif +}; + +} // namespace mozilla + +#endif // mozilla_CPUUsageWatcher_h diff --git a/xpcom/threads/CondVar.h b/xpcom/threads/CondVar.h new file mode 100644 index 0000000000..e427fc2d9e --- /dev/null +++ b/xpcom/threads/CondVar.h @@ -0,0 +1,139 @@ +/* -*- 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 mozilla_CondVar_h +#define mozilla_CondVar_h + +#include "mozilla/BlockingResourceBase.h" +#include "mozilla/PlatformConditionVariable.h" +#include "mozilla/Mutex.h" +#include "mozilla/TimeStamp.h" + +#if defined(MOZILLA_INTERNAL_API) && !defined(DEBUG) +# include "mozilla/ProfilerThreadSleep.h" +#endif // defined( MOZILLA_INTERNAL_API) && !defined(DEBUG) + +namespace mozilla { + +/** + * Similarly to OffTheBooksMutex, OffTheBooksCondvar is identical to CondVar, + * except that OffTheBooksCondVar doesn't include leak checking. Sometimes + * you want to intentionally "leak" a CondVar until shutdown; in these cases, + * OffTheBooksCondVar is for you. + */ +class OffTheBooksCondVar : BlockingResourceBase { + public: + /** + * OffTheBooksCondVar + * + * The CALLER owns |aLock|. + * + * @param aLock A Mutex to associate with this condition variable. + * @param aName A name which can reference this monitor + * @returns If failure, nullptr. + * If success, a valid Monitor* which must be destroyed + * by Monitor::DestroyMonitor() + **/ + OffTheBooksCondVar(OffTheBooksMutex& aLock, const char* aName) + : BlockingResourceBase(aName, eCondVar), mLock(&aLock) {} + + /** + * ~OffTheBooksCondVar + * Clean up after this OffTheBooksCondVar, but NOT its associated Mutex. + **/ + ~OffTheBooksCondVar() = default; + + /** + * Wait + * @see prcvar.h + **/ +#ifndef DEBUG + void Wait() { +# ifdef MOZILLA_INTERNAL_API + AUTO_PROFILER_THREAD_SLEEP; +# endif // MOZILLA_INTERNAL_API + mImpl.wait(*mLock); + } + + CVStatus Wait(TimeDuration aDuration) { +# ifdef MOZILLA_INTERNAL_API + AUTO_PROFILER_THREAD_SLEEP; +# endif // MOZILLA_INTERNAL_API + return mImpl.wait_for(*mLock, aDuration); + } +#else + // NOTE: debug impl is in BlockingResourceBase.cpp + void Wait(); + CVStatus Wait(TimeDuration aDuration); +#endif + + /** + * Notify + * @see prcvar.h + **/ + void Notify() { mImpl.notify_one(); } + + /** + * NotifyAll + * @see prcvar.h + **/ + void NotifyAll() { mImpl.notify_all(); } + +#ifdef DEBUG + /** + * AssertCurrentThreadOwnsMutex + * @see Mutex::AssertCurrentThreadOwns + **/ + void AssertCurrentThreadOwnsMutex() const MOZ_ASSERT_CAPABILITY(mLock) { + mLock->AssertCurrentThreadOwns(); + } + + /** + * AssertNotCurrentThreadOwnsMutex + * @see Mutex::AssertNotCurrentThreadOwns + **/ + void AssertNotCurrentThreadOwnsMutex() const MOZ_ASSERT_CAPABILITY(!mLock) { + mLock->AssertNotCurrentThreadOwns(); + } + +#else + void AssertCurrentThreadOwnsMutex() const MOZ_ASSERT_CAPABILITY(mLock) {} + void AssertNotCurrentThreadOwnsMutex() const MOZ_ASSERT_CAPABILITY(!mLock) {} + +#endif // ifdef DEBUG + + private: + OffTheBooksCondVar(); + OffTheBooksCondVar(const OffTheBooksCondVar&) = delete; + OffTheBooksCondVar& operator=(const OffTheBooksCondVar&) = delete; + + OffTheBooksMutex* mLock; + detail::ConditionVariableImpl mImpl; +}; + +/** + * CondVar + * Vanilla condition variable. Please don't use this unless you have a + * compelling reason --- Monitor provides a simpler API. + */ +class CondVar : public OffTheBooksCondVar { + public: + CondVar(OffTheBooksMutex& aLock, const char* aName) + : OffTheBooksCondVar(aLock, aName) { + MOZ_COUNT_CTOR(CondVar); + } + + MOZ_COUNTED_DTOR(CondVar) + + private: + CondVar(); + CondVar(const CondVar&); + CondVar& operator=(const CondVar&); +}; + +} // namespace mozilla + +#endif // ifndef mozilla_CondVar_h diff --git a/xpcom/threads/DataMutex.h b/xpcom/threads/DataMutex.h new file mode 100644 index 0000000000..44f0a35762 --- /dev/null +++ b/xpcom/threads/DataMutex.h @@ -0,0 +1,130 @@ +/* -*- 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 DataMutex_h__ +#define DataMutex_h__ + +#include +#include "mozilla/Mutex.h" +#include "mozilla/StaticMutex.h" + +namespace mozilla { + +// A template to wrap a type with a mutex so that accesses to the type's +// data are required to take the lock before accessing it. This ensures +// that a mutex is explicitly associated with the data that it protects, +// and makes it impossible to access the data without first taking the +// associated mutex. +// +// This is based on Rust's std::sync::Mutex, which operates under the +// strategy of locking data, rather than code. +// +// Examples: +// +// DataMutex u32DataMutex(1, "u32DataMutex"); +// auto x = u32DataMutex.Lock(); +// *x = 4; +// assert(*x, 4u); +// +// DataMutex> arrayDataMutex("arrayDataMutex"); +// auto a = arrayDataMutex.Lock(); +// auto& x = a.ref(); +// x.AppendElement(1u); +// assert(x[0], 1u); +// +template +class DataMutexBase { + public: + template + class MOZ_STACK_CLASS AutoLockBase { + public: + V* operator->() const& { return &ref(); } + V* operator->() const&& = delete; + + V& operator*() const& { return ref(); } + V& operator*() const&& = delete; + + // Like RefPtr, make this act like its underlying raw pointer type + // whenever it is used in a context where a raw pointer is expected. + operator V*() const& { return &ref(); } + + // Like RefPtr, don't allow implicit conversion of temporary to raw pointer. + operator V*() const&& = delete; + + V& ref() const& { + MOZ_ASSERT(mOwner); + return mOwner->mValue; + } + V& ref() const&& = delete; + + AutoLockBase(AutoLockBase&& aOther) : mOwner(aOther.mOwner) { + aOther.mOwner = nullptr; + } + + ~AutoLockBase() { + if (mOwner) { + mOwner->mMutex.Unlock(); + mOwner = nullptr; + } + } + + private: + friend class DataMutexBase; + + AutoLockBase(const AutoLockBase& aOther) = delete; + + explicit AutoLockBase(DataMutexBase* aDataMutex) + : mOwner(aDataMutex) { + MOZ_ASSERT(!!mOwner); + mOwner->mMutex.Lock(); + } + + DataMutexBase* mOwner; + }; + + using AutoLock = AutoLockBase; + using ConstAutoLock = AutoLockBase; + + explicit DataMutexBase(const char* aName) : mMutex(aName) {} + + DataMutexBase(T&& aValue, const char* aName) + : mMutex(aName), mValue(std::move(aValue)) {} + + AutoLock Lock() { return AutoLock(this); } + ConstAutoLock ConstLock() { return ConstAutoLock(this); } + + const MutexType& Mutex() const { return mMutex; } + + private: + MutexType mMutex; + T mValue; +}; + +// Craft a version of StaticMutex that takes a const char* in its ctor. +// We need this so it works interchangeably with Mutex which requires a const +// char* aName in its ctor. +class StaticMutexNameless : public StaticMutex { + public: + explicit StaticMutexNameless(const char* aName) : StaticMutex() {} + + private: + // Disallow copy construction, `=`, `new`, and `delete` like BaseStaticMutex. +#ifdef DEBUG + StaticMutexNameless(StaticMutexNameless& aOther); +#endif // DEBUG + StaticMutexNameless& operator=(StaticMutexNameless* aRhs); + static void* operator new(size_t) noexcept(true); + static void operator delete(void*); +}; + +template +using DataMutex = DataMutexBase; +template +using StaticDataMutex = DataMutexBase; + +} // namespace mozilla + +#endif // DataMutex_h__ diff --git a/xpcom/threads/DeadlockDetector.h b/xpcom/threads/DeadlockDetector.h new file mode 100644 index 0000000000..5c40941328 --- /dev/null +++ b/xpcom/threads/DeadlockDetector.h @@ -0,0 +1,359 @@ +/* -*- 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 mozilla_DeadlockDetector_h +#define mozilla_DeadlockDetector_h + +#include "mozilla/Attributes.h" + +#include + +#include "prlock.h" + +#include "nsClassHashtable.h" +#include "nsTArray.h" + +namespace mozilla { + +/** + * DeadlockDetector + * + * The following is an approximate description of how the deadlock detector + * works. + * + * The deadlock detector ensures that all blocking resources are + * acquired according to a partial order P. One type of blocking + * resource is a lock. If a lock l1 is acquired (locked) before l2, + * then we say that |l1 <_P l2|. The detector flags an error if two + * locks l1 and l2 have an inconsistent ordering in P; that is, if + * both |l1 <_P l2| and |l2 <_P l1|. This is a potential error + * because a thread acquiring l1,l2 according to the first order might + * race with a thread acquiring them according to the second order. + * If this happens under the right conditions, then the acquisitions + * will deadlock. + * + * This deadlock detector doesn't know at compile-time what P is. So, + * it tries to discover the order at run time. More precisely, it + * finds some order P, then tries to find chains of resource + * acquisitions that violate P. An example acquisition sequence, and + * the orders they impose, is + * l1.lock() // current chain: [ l1 ] + * // order: { } + * + * l2.lock() // current chain: [ l1, l2 ] + * // order: { l1 <_P l2 } + * + * l3.lock() // current chain: [ l1, l2, l3 ] + * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 } + * // (note: <_P is transitive, so also |l1 <_P l3|) + * + * l2.unlock() // current chain: [ l1, l3 ] + * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3 } + * // (note: it's OK, but weird, that l2 was unlocked out + * // of order. we still have l1 <_P l3). + * + * l2.lock() // current chain: [ l1, l3, l2 ] + * // order: { l1 <_P l2, l2 <_P l3, l1 <_P l3, + * l3 <_P l2 (!!!) } + * BEEP BEEP! Here the detector will flag a potential error, since + * l2 and l3 were used inconsistently (and potentially in ways that + * would deadlock). + */ +template +class DeadlockDetector { + public: + typedef nsTArray ResourceAcquisitionArray; + + private: + struct OrderingEntry; + typedef nsTArray HashEntryArray; + typedef typename HashEntryArray::index_type index_type; + typedef typename HashEntryArray::size_type size_type; + static const index_type NoIndex = HashEntryArray::NoIndex; + + /** + * Value type for the ordering table. Contains the other + * resources on which an ordering constraint |key < other| + * exists. The catch is that we also store the calling context at + * which the other resource was acquired; this improves the + * quality of error messages when potential deadlock is detected. + */ + struct OrderingEntry { + explicit OrderingEntry(const T* aResource) + : mOrderedLT() // FIXME bug 456272: set to empirical dep size? + , + mExternalRefs(), + mResource(aResource) {} + ~OrderingEntry() {} + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + n += mOrderedLT.ShallowSizeOfExcludingThis(aMallocSizeOf); + n += mExternalRefs.ShallowSizeOfExcludingThis(aMallocSizeOf); + return n; + } + + HashEntryArray mOrderedLT; // this <_o Other + HashEntryArray mExternalRefs; // hash entries that reference this + const T* mResource; + }; + + // Throwaway RAII lock to make the following code safer. + struct PRAutoLock { + explicit PRAutoLock(PRLock* aLock) : mLock(aLock) { PR_Lock(mLock); } + ~PRAutoLock() { PR_Unlock(mLock); } + PRLock* mLock; + }; + + public: + static const uint32_t kDefaultNumBuckets; + + /** + * DeadlockDetector + * Create a new deadlock detector. + * + * @param aNumResourcesGuess Guess at approximate number of resources + * that will be checked. + */ + explicit DeadlockDetector(uint32_t aNumResourcesGuess = kDefaultNumBuckets) + : mOrdering(aNumResourcesGuess) { + mLock = PR_NewLock(); + if (!mLock) { + MOZ_CRASH("couldn't allocate deadlock detector lock"); + } + } + + /** + * ~DeadlockDetector + * + * *NOT* thread safe. + */ + ~DeadlockDetector() { PR_DestroyLock(mLock); } + + size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = aMallocSizeOf(this); + + { + PRAutoLock _(mLock); + n += mOrdering.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (const auto& data : mOrdering.Values()) { + // NB: Key is accounted for in the entry. + n += data->SizeOfIncludingThis(aMallocSizeOf); + } + } + + return n; + } + + /** + * Add + * Make the deadlock detector aware of |aResource|. + * + * WARNING: The deadlock detector owns |aResource|. + * + * Thread safe. + * + * @param aResource Resource to make deadlock detector aware of. + */ + void Add(const T* aResource) { + PRAutoLock _(mLock); + mOrdering.InsertOrUpdate(aResource, MakeUnique(aResource)); + } + + void Remove(const T* aResource) { + PRAutoLock _(mLock); + + OrderingEntry* entry = mOrdering.Get(aResource); + + // Iterate the external refs and remove the entry from them. + HashEntryArray& refs = entry->mExternalRefs; + for (index_type i = 0; i < refs.Length(); i++) { + refs[i]->mOrderedLT.RemoveElementSorted(entry); + } + + // Iterate orders and remove this entry from their refs. + HashEntryArray& orders = entry->mOrderedLT; + for (index_type i = 0; i < orders.Length(); i++) { + orders[i]->mExternalRefs.RemoveElementSorted(entry); + } + + // Now the entry can be safely removed. + mOrdering.Remove(aResource); + } + + /** + * CheckAcquisition This method is called after acquiring |aLast|, + * but before trying to acquire |aProposed|. + * It determines whether actually trying to acquire |aProposed| + * will create problems. It is OK if |aLast| is nullptr; this is + * interpreted as |aProposed| being the thread's first acquisition + * of its current chain. + * + * Iff acquiring |aProposed| may lead to deadlock for some thread + * interleaving (including the current one!), the cyclical + * dependency from which this was deduced is returned. Otherwise, + * 0 is returned. + * + * If a potential deadlock is detected and a resource cycle is + * returned, it is the *caller's* responsibility to free it. + * + * Thread safe. + * + * @param aLast Last resource acquired by calling thread (or 0). + * @param aProposed Resource calling thread proposes to acquire. + */ + ResourceAcquisitionArray* CheckAcquisition(const T* aLast, + const T* aProposed) { + if (!aLast) { + // don't check if |0 < aProposed|; just vamoose + return 0; + } + + NS_ASSERTION(aProposed, "null resource"); + PRAutoLock _(mLock); + + OrderingEntry* proposed = mOrdering.Get(aProposed); + NS_ASSERTION(proposed, "missing ordering entry"); + + OrderingEntry* current = mOrdering.Get(aLast); + NS_ASSERTION(current, "missing ordering entry"); + + // this is the crux of the deadlock detector algorithm + + if (current == proposed) { + // reflexive deadlock. fastpath b/c InTransitiveClosure is + // not applicable here. + ResourceAcquisitionArray* cycle = new ResourceAcquisitionArray(); + if (!cycle) { + MOZ_CRASH("can't allocate dep. cycle array"); + } + cycle->AppendElement(current->mResource); + cycle->AppendElement(aProposed); + return cycle; + } + if (InTransitiveClosure(current, proposed)) { + // we've already established |aLast < aProposed|. all is well. + return 0; + } + if (InTransitiveClosure(proposed, current)) { + // the order |aProposed < aLast| has been deduced, perhaps + // transitively. we're attempting to violate that + // constraint by acquiring resources in the order + // |aLast < aProposed|, and thus we may deadlock under the + // right conditions. + ResourceAcquisitionArray* cycle = GetDeductionChain(proposed, current); + // show how acquiring |aProposed| would complete the cycle + cycle->AppendElement(aProposed); + return cycle; + } + // |aLast|, |aProposed| are unordered according to our + // poset. this is fine, but we now need to add this + // ordering constraint. + current->mOrderedLT.InsertElementSorted(proposed); + proposed->mExternalRefs.InsertElementSorted(current); + return 0; + } + + /** + * Return true iff |aTarget| is in the transitive closure of |aStart| + * over the ordering relation `<_this'. + * + * @precondition |aStart != aTarget| + */ + bool InTransitiveClosure(const OrderingEntry* aStart, + const OrderingEntry* aTarget) const { + // NB: Using a static comparator rather than default constructing one shows + // a 9% improvement in scalability tests on some systems. + static nsDefaultComparator comp; + if (aStart->mOrderedLT.BinaryIndexOf(aTarget, comp) != NoIndex) { + return true; + } + + index_type i = 0; + size_type len = aStart->mOrderedLT.Length(); + for (auto it = aStart->mOrderedLT.Elements(); i < len; ++i, ++it) { + if (InTransitiveClosure(*it, aTarget)) { + return true; + } + } + return false; + } + + /** + * Return an array of all resource acquisitions + * aStart <_this r1 <_this r2 <_ ... <_ aTarget + * from which |aStart <_this aTarget| was deduced, including + * |aStart| and |aTarget|. + * + * Nb: there may be multiple deductions of |aStart <_this + * aTarget|. This function returns the first ordering found by + * depth-first search. + * + * Nb: |InTransitiveClosure| could be replaced by this function. + * However, this one is more expensive because we record the DFS + * search stack on the heap whereas the other doesn't. + * + * @precondition |aStart != aTarget| + */ + ResourceAcquisitionArray* GetDeductionChain(const OrderingEntry* aStart, + const OrderingEntry* aTarget) { + ResourceAcquisitionArray* chain = new ResourceAcquisitionArray(); + if (!chain) { + MOZ_CRASH("can't allocate dep. cycle array"); + } + chain->AppendElement(aStart->mResource); + + NS_ASSERTION(GetDeductionChain_Helper(aStart, aTarget, chain), + "GetDeductionChain called when there's no deadlock"); + return chain; + } + + // precondition: |aStart != aTarget| + // invariant: |aStart| is the last element in |aChain| + bool GetDeductionChain_Helper(const OrderingEntry* aStart, + const OrderingEntry* aTarget, + ResourceAcquisitionArray* aChain) { + if (aStart->mOrderedLT.BinaryIndexOf(aTarget) != NoIndex) { + aChain->AppendElement(aTarget->mResource); + return true; + } + + index_type i = 0; + size_type len = aStart->mOrderedLT.Length(); + for (auto it = aStart->mOrderedLT.Elements(); i < len; ++i, ++it) { + aChain->AppendElement((*it)->mResource); + if (GetDeductionChain_Helper(*it, aTarget, aChain)) { + return true; + } + aChain->RemoveLastElement(); + } + return false; + } + + /** + * The partial order on resource acquisitions used by the deadlock + * detector. + */ + nsClassHashtable, OrderingEntry> mOrdering; + + /** + * Protects contentious methods. + * Nb: can't use mozilla::Mutex since we are used as its deadlock + * detector. + */ + PRLock* mLock; + + private: + DeadlockDetector(const DeadlockDetector& aDD) = delete; + DeadlockDetector& operator=(const DeadlockDetector& aDD) = delete; +}; + +template +// FIXME bug 456272: tune based on average workload +const uint32_t DeadlockDetector::kDefaultNumBuckets = 32; + +} // namespace mozilla + +#endif // ifndef mozilla_DeadlockDetector_h diff --git a/xpcom/threads/DelayedRunnable.cpp b/xpcom/threads/DelayedRunnable.cpp new file mode 100644 index 0000000000..a9231442a7 --- /dev/null +++ b/xpcom/threads/DelayedRunnable.cpp @@ -0,0 +1,113 @@ +/* -*- 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 "DelayedRunnable.h" + +#include "mozilla/ProfilerRunnable.h" + +namespace mozilla { + +DelayedRunnable::DelayedRunnable(already_AddRefed aTarget, + already_AddRefed aRunnable, + uint32_t aDelay) + : mozilla::Runnable("DelayedRunnable"), + mTarget(aTarget), + mDelayedFrom(TimeStamp::NowLoRes()), + mDelay(aDelay), + mWrappedRunnable(aRunnable) {} + +nsresult DelayedRunnable::Init() { + MutexAutoLock lock(mMutex); + if (!mWrappedRunnable) { + MOZ_ASSERT_UNREACHABLE(); + return NS_ERROR_INVALID_ARG; + } + + nsresult rv = mTarget->RegisterShutdownTask(this); + if (NS_FAILED(rv)) { + MOZ_DIAGNOSTIC_ASSERT( + rv == NS_ERROR_UNEXPECTED, + "DelayedRunnable target must support RegisterShutdownTask"); + NS_WARNING("DelayedRunnable init after target is shutdown"); + return rv; + } + + rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, mDelay, + nsITimer::TYPE_ONE_SHOT, mTarget); + if (NS_WARN_IF(NS_FAILED(rv))) { + mTarget->UnregisterShutdownTask(this); + } + return rv; +} + +NS_IMETHODIMP DelayedRunnable::Run() { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + nsCOMPtr runnable; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTimer, "Init() must have been called"); + + // Already ran? + if (!mWrappedRunnable) { + return NS_OK; + } + + // Are we too early? + if ((mozilla::TimeStamp::NowLoRes() - mDelayedFrom).ToMilliseconds() < + mDelay) { + return NS_OK; // Let the nsITimer run us. + } + + mTimer->Cancel(); + mTarget->UnregisterShutdownTask(this); + runnable = mWrappedRunnable.forget(); + } + + AUTO_PROFILE_FOLLOWING_RUNNABLE(runnable); + return runnable->Run(); +} + +NS_IMETHODIMP DelayedRunnable::Notify(nsITimer* aTimer) { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + nsCOMPtr runnable; + { + MutexAutoLock lock(mMutex); + MOZ_ASSERT(mTimer, "Init() must have been called"); + + // We may have already run due to races + if (!mWrappedRunnable) { + return NS_OK; + } + + mTarget->UnregisterShutdownTask(this); + runnable = mWrappedRunnable.forget(); + } + + AUTO_PROFILE_FOLLOWING_RUNNABLE(runnable); + return runnable->Run(); +} + +void DelayedRunnable::TargetShutdown() { + MOZ_ASSERT(mTarget->IsOnCurrentThread()); + + // Called at shutdown + MutexAutoLock lock(mMutex); + if (!mWrappedRunnable) { + return; + } + mWrappedRunnable = nullptr; + + if (mTimer) { + mTimer->Cancel(); + } +} + +NS_IMPL_ISUPPORTS_INHERITED(DelayedRunnable, Runnable, nsITimerCallback, + nsITargetShutdownTask) + +} // namespace mozilla diff --git a/xpcom/threads/DelayedRunnable.h b/xpcom/threads/DelayedRunnable.h new file mode 100644 index 0000000000..242278fa5b --- /dev/null +++ b/xpcom/threads/DelayedRunnable.h @@ -0,0 +1,53 @@ +/* -*- 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 XPCOM_THREADS_DELAYEDRUNNABLE_H_ +#define XPCOM_THREADS_DELAYEDRUNNABLE_H_ + +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" +#include "nsIRunnable.h" +#include "nsITargetShutdownTask.h" +#include "nsITimer.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +class DelayedRunnable : public Runnable, + public nsITimerCallback, + public nsITargetShutdownTask { + public: + DelayedRunnable(already_AddRefed aTarget, + already_AddRefed aRunnable, uint32_t aDelay); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSITIMERCALLBACK + + nsresult Init(); + + /** + * Called when the target is going away so the runnable can be released safely + * on the target thread. + */ + void TargetShutdown() override; + + private: + ~DelayedRunnable() = default; + nsresult DoRun(); + + const nsCOMPtr mTarget; + const TimeStamp mDelayedFrom; + const uint32_t mDelay; + + mozilla::Mutex mMutex{"DelayedRunnable"}; + nsCOMPtr mWrappedRunnable; + nsCOMPtr mTimer; +}; + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/EventQueue.cpp b/xpcom/threads/EventQueue.cpp new file mode 100644 index 0000000000..0decc3ce4c --- /dev/null +++ b/xpcom/threads/EventQueue.cpp @@ -0,0 +1,131 @@ +/* -*- 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/EventQueue.h" + +#include "GeckoProfiler.h" +#include "InputTaskManager.h" +#include "VsyncTaskManager.h" +#include "nsIRunnable.h" +#include "TaskController.h" + +using namespace mozilla; +using namespace mozilla::detail; + +template +void EventQueueInternal::PutEvent( + already_AddRefed&& aEvent, EventQueuePriority aPriority, + const MutexAutoLock& aProofOfLock, mozilla::TimeDuration* aDelay) { + nsCOMPtr event(aEvent); + + static_assert(static_cast(nsIRunnablePriority::PRIORITY_IDLE) == + static_cast(EventQueuePriority::Idle)); + static_assert(static_cast(nsIRunnablePriority::PRIORITY_NORMAL) == + static_cast(EventQueuePriority::Normal)); + static_assert( + static_cast(nsIRunnablePriority::PRIORITY_MEDIUMHIGH) == + static_cast(EventQueuePriority::MediumHigh)); + static_assert( + static_cast(nsIRunnablePriority::PRIORITY_INPUT_HIGH) == + static_cast(EventQueuePriority::InputHigh)); + static_assert(static_cast(nsIRunnablePriority::PRIORITY_VSYNC) == + static_cast(EventQueuePriority::Vsync)); + static_assert( + static_cast(nsIRunnablePriority::PRIORITY_RENDER_BLOCKING) == + static_cast(EventQueuePriority::RenderBlocking)); + static_assert(static_cast(nsIRunnablePriority::PRIORITY_CONTROL) == + static_cast(EventQueuePriority::Control)); + + if (mForwardToTC) { + TaskController* tc = TaskController::Get(); + + TaskManager* manager = nullptr; + if (aPriority == EventQueuePriority::InputHigh) { + manager = InputTaskManager::Get(); + } else if (aPriority == EventQueuePriority::DeferredTimers || + aPriority == EventQueuePriority::Idle) { + manager = TaskController::Get()->GetIdleTaskManager(); + } else if (aPriority == EventQueuePriority::Vsync) { + manager = VsyncTaskManager::Get(); + } + + tc->DispatchRunnable(event.forget(), static_cast(aPriority), + manager); + return; + } + + if (profiler_thread_is_being_profiled(ThreadProfilingFeatures::Sampling)) { + // check to see if the profiler has been enabled since the last PutEvent + while (mDispatchTimes.Count() < mQueue.Count()) { + mDispatchTimes.Push(TimeStamp()); + } + mDispatchTimes.Push(aDelay ? TimeStamp::Now() - *aDelay : TimeStamp::Now()); + } + + mQueue.Push(std::move(event)); +} + +template +already_AddRefed EventQueueInternal::GetEvent( + const MutexAutoLock& aProofOfLock, mozilla::TimeDuration* aLastEventDelay) { + if (mQueue.IsEmpty()) { + if (aLastEventDelay) { + *aLastEventDelay = TimeDuration(); + } + return nullptr; + } + + // We always want to clear the dispatch times, even if the profiler is turned + // off, because we want to empty the (previously-collected) dispatch times, if + // any, from when the profiler was turned on. We only want to do something + // interesting with the dispatch times if the profiler is turned on, though. + if (!mDispatchTimes.IsEmpty()) { + TimeStamp dispatch_time = mDispatchTimes.Pop(); + if (profiler_is_active()) { + if (!dispatch_time.IsNull()) { + if (aLastEventDelay) { + *aLastEventDelay = TimeStamp::Now() - dispatch_time; + } + } + } + } else if (profiler_is_active()) { + if (aLastEventDelay) { + // if we just turned on the profiler, we don't have dispatch + // times for events already in the queue. + *aLastEventDelay = TimeDuration(); + } + } + + nsCOMPtr result = mQueue.Pop(); + return result.forget(); +} + +template +bool EventQueueInternal::IsEmpty( + const MutexAutoLock& aProofOfLock) { + return mQueue.IsEmpty(); +} + +template +bool EventQueueInternal::HasReadyEvent( + const MutexAutoLock& aProofOfLock) { + return !IsEmpty(aProofOfLock); +} + +template +size_t EventQueueInternal::Count( + const MutexAutoLock& aProofOfLock) const { + return mQueue.Count(); +} + +namespace mozilla { +template class EventQueueSized<16>; // Used by ThreadEventQueue +template class EventQueueSized<64>; // Used by ThrottledEventQueue +namespace detail { +template class EventQueueInternal<16>; // Used by ThreadEventQueue +template class EventQueueInternal<64>; // Used by ThrottledEventQueue +} // namespace detail +} // namespace mozilla diff --git a/xpcom/threads/EventQueue.h b/xpcom/threads/EventQueue.h new file mode 100644 index 0000000000..b7af71be82 --- /dev/null +++ b/xpcom/threads/EventQueue.h @@ -0,0 +1,135 @@ +/* -*- 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 mozilla_EventQueue_h +#define mozilla_EventQueue_h + +#include "mozilla/Mutex.h" +#include "mozilla/Queue.h" +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" +#include "nsIRunnable.h" + +namespace mozilla { + +#define EVENT_QUEUE_PRIORITY_LIST(EVENT_PRIORITY) \ + EVENT_PRIORITY(Idle, 0) \ + EVENT_PRIORITY(DeferredTimers, 1) \ + EVENT_PRIORITY(Low, 2) \ + EVENT_PRIORITY(InputLow, 3) \ + EVENT_PRIORITY(Normal, 4) \ + EVENT_PRIORITY(MediumHigh, 5) \ + EVENT_PRIORITY(InputHigh, 6) \ + EVENT_PRIORITY(Vsync, 7) \ + EVENT_PRIORITY(InputHighest, 8) \ + EVENT_PRIORITY(RenderBlocking, 9) \ + EVENT_PRIORITY(Control, 10) + +enum class EventQueuePriority { +#define EVENT_PRIORITY(NAME, VALUE) NAME = VALUE, + EVENT_QUEUE_PRIORITY_LIST(EVENT_PRIORITY) +#undef EVENT_PRIORITY + Invalid +}; + +class IdlePeriodState; + +namespace detail { + +// EventQueue is our unsynchronized event queue implementation. It is a queue +// of runnables used for non-main thread, as well as optionally providing +// forwarding to TaskController. +// +// Since EventQueue is unsynchronized, it should be wrapped in an outer +// SynchronizedEventQueue implementation (like ThreadEventQueue). +template +class EventQueueInternal { + public: + explicit EventQueueInternal(bool aForwardToTC) : mForwardToTC(aForwardToTC) {} + + // Add an event to the end of the queue. Implementors are free to use + // aPriority however they wish. If the runnable supports + // nsIRunnablePriority and the implementing class supports + // prioritization, aPriority represents the result of calling + // nsIRunnablePriority::GetPriority(). *aDelay is time the event has + // already been delayed (used when moving an event from one queue to + // another) + void PutEvent(already_AddRefed&& aEvent, + EventQueuePriority aPriority, const MutexAutoLock& aProofOfLock, + mozilla::TimeDuration* aDelay = nullptr); + + // Get an event from the front of the queue. This should return null if the + // queue is non-empty but the event in front is not ready to run. + // *aLastEventDelay is the time the event spent in queues before being + // retrieved. + already_AddRefed GetEvent( + const MutexAutoLock& aProofOfLock, + mozilla::TimeDuration* aLastEventDelay = nullptr); + + // Returns true if the queue is empty. Implies !HasReadyEvent(). + bool IsEmpty(const MutexAutoLock& aProofOfLock); + + // Returns true if the queue is non-empty and if the event in front is ready + // to run. Implies !IsEmpty(). This should return true iff GetEvent returns a + // non-null value. + bool HasReadyEvent(const MutexAutoLock& aProofOfLock); + + // Returns the number of events in the queue. + size_t Count(const MutexAutoLock& aProofOfLock) const; + // For some reason, if we put this in the .cpp file the linker can't find it + already_AddRefed PeekEvent(const MutexAutoLock& aProofOfLock) { + if (mQueue.IsEmpty()) { + return nullptr; + } + + nsCOMPtr result = mQueue.FirstElement(); + return result.forget(); + } + + void EnableInputEventPrioritization(const MutexAutoLock& aProofOfLock) {} + void FlushInputEventPrioritization(const MutexAutoLock& aProofOfLock) {} + void SuspendInputEventPrioritization(const MutexAutoLock& aProofOfLock) {} + void ResumeInputEventPrioritization(const MutexAutoLock& aProofOfLock) {} + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t size = mQueue.ShallowSizeOfExcludingThis(aMallocSizeOf); + size += mDispatchTimes.ShallowSizeOfExcludingThis(aMallocSizeOf); + return size; + } + + private: + mozilla::Queue, ItemsPerPage> mQueue; + // This queue is only populated when the profiler is turned on. + mozilla::Queue mDispatchTimes; + TimeDuration mLastEventDelay; + // This indicates PutEvent forwards runnables to the TaskController. This + // should be true for the top level event queue on the main thread. + bool mForwardToTC; +}; + +} // namespace detail + +class EventQueue final : public mozilla::detail::EventQueueInternal<16> { + public: + explicit EventQueue(bool aForwardToTC = false) + : mozilla::detail::EventQueueInternal<16>(aForwardToTC) {} +}; + +template +class EventQueueSized final + : public mozilla::detail::EventQueueInternal { + public: + explicit EventQueueSized(bool aForwardToTC = false) + : mozilla::detail::EventQueueInternal(aForwardToTC) {} +}; + +} // namespace mozilla + +#endif // mozilla_EventQueue_h diff --git a/xpcom/threads/EventTargetCapability.h b/xpcom/threads/EventTargetCapability.h new file mode 100644 index 0000000000..0cd85a7523 --- /dev/null +++ b/xpcom/threads/EventTargetCapability.h @@ -0,0 +1,95 @@ +/* -*- 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 XPCOM_THREADS_EVENTTARGETCAPABILITY_H_ +#define XPCOM_THREADS_EVENTTARGETCAPABILITY_H_ + +#include "mozilla/ThreadSafety.h" +#include "nsIEventTarget.h" + +namespace mozilla { + +// A helper to ensure that data access and function usage only take place on a +// specific nsIEventTarget. +// +// This class works with Clang's thread safety analysis so that static analysis +// can ensure AssertOnCurrentThread is called before using guarded data and +// functions. +// +// This means using the class is similar to calling +// `MOZ_ASSERT(mTarget->IsOnCurrentThread())` +// prior to accessing things we expect to be on `mTarget`. However, using this +// helper has the added benefit that static analysis will warn you if you +// fail to assert prior to usage. +// +// The following is a basic example of a class using this to ensure +// a data member is only accessed on a specific target. +// +// class SomeMediaHandlerThing { +// public: +// SomeMediaHandlerThing(nsIEventTarget* aTarget) : mTargetCapability(aTarget) +// {} +// +// void UpdateMediaState() { +// mTargetCapability.Dispatch( +// NS_NewRunnableFunction("UpdateMediaState", [this] { +// mTargetCapability.AssertOnCurrentThread(); +// IncreaseMediaCount(); +// })); +// } +// +// private: +// void IncreaseMediaCount() MOZ_REQUIRES(mTargetCapability) { mMediaCount += +// 1; } +// +// uint32_t mMediaCount MOZ_GUARDED_BY(mTargetCapability) = 0; +// EventTargetCapability mTargetCapability; +// }; +// +// NOTE: If you need a thread-safety capability for specifically the main +// thread, the static `mozilla::sMainThreadCapability` capability exists, and +// can be asserted using `AssertIsOnMainThread()`. + +template +class MOZ_CAPABILITY("event target") EventTargetCapability final { + static_assert(std::is_base_of_v, + "T must derive from nsIEventTarget"); + + public: + explicit EventTargetCapability(T* aTarget) : mTarget(aTarget) { + MOZ_ASSERT(mTarget, "mTarget should be non-null"); + } + ~EventTargetCapability() = default; + + EventTargetCapability(const EventTargetCapability&) = default; + EventTargetCapability(EventTargetCapability&&) = default; + EventTargetCapability& operator=(const EventTargetCapability&) = default; + EventTargetCapability& operator=(EventTargetCapability&&) = default; + + void AssertOnCurrentThread() const MOZ_ASSERT_CAPABILITY(this) { + MOZ_ASSERT(IsOnCurrentThread()); + } + + // Allow users to check if we're on the same thread as the event target. + bool IsOnCurrentThread() const { return mTarget->IsOnCurrentThread(); } + + // Allow users to get the event target, so classes don't have to store the + // target as a separate member to use it. + T* GetEventTarget() const { return mTarget; } + + // Helper to simplify dispatching to mTarget. + nsresult Dispatch(already_AddRefed aRunnable, + uint32_t aFlags = NS_DISPATCH_NORMAL) const { + return mTarget->Dispatch(std::move(aRunnable), aFlags); + } + + private: + RefPtr mTarget; +}; + +} // namespace mozilla + +#endif // XPCOM_THREADS_EVENTTARGETCAPABILITY_H_ diff --git a/xpcom/threads/IdlePeriodState.cpp b/xpcom/threads/IdlePeriodState.cpp new file mode 100644 index 0000000000..ff1a9f4096 --- /dev/null +++ b/xpcom/threads/IdlePeriodState.cpp @@ -0,0 +1,257 @@ +/* -*- 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/AppShutdown.h" +#include "mozilla/IdlePeriodState.h" +#include "mozilla/StaticPrefs_idle_period.h" +#include "mozilla/ipc/IdleSchedulerChild.h" +#include "mozilla/dom/ContentChild.h" +#include "nsIIdlePeriod.h" +#include "nsThreadManager.h" +#include "nsXPCOM.h" +#include "nsXULAppAPI.h" + +static uint64_t sIdleRequestCounter = 0; + +namespace mozilla { + +IdlePeriodState::IdlePeriodState(already_AddRefed&& aIdlePeriod) + : mIdlePeriod(aIdlePeriod) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); +} + +IdlePeriodState::~IdlePeriodState() { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + if (mIdleScheduler) { + mIdleScheduler->Disconnect(); + } +} + +size_t IdlePeriodState::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + if (mIdlePeriod) { + n += aMallocSizeOf(mIdlePeriod); + } + + return n; +} + +void IdlePeriodState::FlagNotIdle() { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + + EnsureIsActive(); + if (mIdleToken && mIdleToken < TimeStamp::Now()) { + ClearIdleToken(); + } +} + +void IdlePeriodState::RanOutOfTasks(const MutexAutoUnlock& aProofOfUnlock) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + MOZ_ASSERT(!mHasPendingEventsPromisedIdleEvent); + EnsureIsPaused(aProofOfUnlock); + ClearIdleToken(); +} + +TimeStamp IdlePeriodState::GetIdleDeadlineInternal( + bool aIsPeek, const MutexAutoUnlock& aProofOfUnlock) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + + bool shuttingDown; + TimeStamp localIdleDeadline = + GetLocalIdleDeadline(shuttingDown, aProofOfUnlock); + if (!localIdleDeadline) { + if (!aIsPeek) { + EnsureIsPaused(aProofOfUnlock); + ClearIdleToken(); + } + return TimeStamp(); + } + + TimeStamp idleDeadline = + mHasPendingEventsPromisedIdleEvent || shuttingDown + ? localIdleDeadline + : GetIdleToken(localIdleDeadline, aProofOfUnlock); + if (!idleDeadline) { + if (!aIsPeek) { + EnsureIsPaused(aProofOfUnlock); + + // Don't call ClearIdleToken() here, since we may have a pending + // request already. + // + // RequestIdleToken can do all sorts of IPC stuff that might + // take mutexes. This is one reason why we need the + // MutexAutoUnlock reference! + RequestIdleToken(localIdleDeadline); + } + return TimeStamp(); + } + + if (!aIsPeek) { + EnsureIsActive(); + } + return idleDeadline; +} + +TimeStamp IdlePeriodState::GetLocalIdleDeadline( + bool& aShuttingDown, const MutexAutoUnlock& aProofOfUnlock) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + // If we are shutting down, we won't honor the idle period, and we will + // always process idle runnables. This will ensure that the idle queue + // gets exhausted at shutdown time to prevent intermittently leaking + // some runnables inside that queue and even worse potentially leaving + // some important cleanup work unfinished. + if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdownThreads) || + nsThreadManager::get().GetCurrentThread()->ShuttingDown()) { + aShuttingDown = true; + return TimeStamp::Now(); + } + + aShuttingDown = false; + TimeStamp idleDeadline; + // This GetIdlePeriodHint() call is the reason we need a MutexAutoUnlock here. + mIdlePeriod->GetIdlePeriodHint(&idleDeadline); + + // If HasPendingEvents() has been called and it has returned true because of + // pending idle events, there is a risk that we may decide here that we aren't + // idle and return null, in which case HasPendingEvents() has effectively + // lied. Since we can't go back and fix the past, we have to adjust what we + // do here and forcefully pick the idle queue task here. Note that this means + // that we are choosing to run a task from the idle queue when we would + // normally decide that we aren't in an idle period, but this can only happen + // if we fall out of the idle period in between the call to HasPendingEvents() + // and here, which should hopefully be quite rare. We are effectively + // choosing to prioritize the sanity of our API semantics over the optimal + // scheduling. + if (!mHasPendingEventsPromisedIdleEvent && + (!idleDeadline || idleDeadline < TimeStamp::Now())) { + return TimeStamp(); + } + if (mHasPendingEventsPromisedIdleEvent && !idleDeadline) { + // If HasPendingEvents() has been called and it has returned true, but we're + // no longer in the idle period, we must return a valid timestamp to pretend + // that we are still in the idle period. + return TimeStamp::Now(); + } + return idleDeadline; +} + +TimeStamp IdlePeriodState::GetIdleToken(TimeStamp aLocalIdlePeriodHint, + const MutexAutoUnlock& aProofOfUnlock) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + + if (!ShouldGetIdleToken()) { + // If the process was in background, it may have an idle token, but it can + // be cleared now. + ClearIdleToken(); + return aLocalIdlePeriodHint; + } + + if (mIdleToken) { + TimeStamp now = TimeStamp::Now(); + if (mIdleToken < now) { + ClearIdleToken(); + return mIdleToken; + } + return mIdleToken < aLocalIdlePeriodHint ? mIdleToken + : aLocalIdlePeriodHint; + } + return TimeStamp(); +} + +void IdlePeriodState::RequestIdleToken(TimeStamp aLocalIdlePeriodHint) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + MOZ_ASSERT(!mActive); + + if (!mIdleScheduler && ShouldGetIdleToken()) { + // For now cross-process idle scheduler is supported only on the main + // threads of the child processes. + mIdleScheduler = ipc::IdleSchedulerChild::GetMainThreadIdleScheduler(); + if (mIdleScheduler) { + mIdleScheduler->Init(this); + } + } + + if (mIdleScheduler && !mIdleRequestId) { + TimeStamp now = TimeStamp::Now(); + if (aLocalIdlePeriodHint <= now) { + return; + } + + mIdleRequestId = ++sIdleRequestCounter; + mIdleScheduler->SendRequestIdleTime(mIdleRequestId, + aLocalIdlePeriodHint - now); + } +} + +void IdlePeriodState::SetIdleToken(uint64_t aId, TimeDuration aDuration) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + + // We check the request ID. It's possible that the server may be granting a + // an ealier request that the client has since cancelled and re-requested. + if (mIdleRequestId == aId) { + mIdleToken = TimeStamp::Now() + aDuration; + } +} + +void IdlePeriodState::SetActive() { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + MOZ_ASSERT(!mActive); + if (mIdleScheduler) { + mIdleScheduler->SetActive(); + } + mActive = true; +} + +void IdlePeriodState::SetPaused(const MutexAutoUnlock& aProofOfUnlock) { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + MOZ_ASSERT(mActive); + if (mIdleScheduler && mIdleScheduler->SetPaused()) { + // We may have gotten a free cpu core for running idle tasks. + // We don't try to catch the case when there are prioritized processes + // running. + + // This SendSchedule call is why we need the MutexAutoUnlock here, because + // IPC can do weird things with mutexes. + mIdleScheduler->SendSchedule(); + } + mActive = false; +} + +void IdlePeriodState::ClearIdleToken() { + MOZ_ASSERT(NS_IsMainThread(), + "Why are we touching idle state off the main thread?"); + + if (mIdleRequestId) { + if (mIdleScheduler) { + // This SendIdleTimeUsed call is why we need to not be holding + // any locks here, because IPC can do weird things with mutexes. + // Ideally we'd have a MutexAutoUnlock& reference here, but some + // callers end up here while just not holding any locks at all. + mIdleScheduler->SendIdleTimeUsed(mIdleRequestId); + } + mIdleRequestId = 0; + mIdleToken = TimeStamp(); + } +} + +bool IdlePeriodState::ShouldGetIdleToken() { + return StaticPrefs::idle_period_cross_process_scheduling() && + dom::ContentChild::GetSingleton() && + dom::ContentChild::GetSingleton()->GetProcessPriority() < + hal::ProcessPriority::PROCESS_PRIORITY_FOREGROUND; +} +} // namespace mozilla diff --git a/xpcom/threads/IdlePeriodState.h b/xpcom/threads/IdlePeriodState.h new file mode 100644 index 0000000000..94d6615677 --- /dev/null +++ b/xpcom/threads/IdlePeriodState.h @@ -0,0 +1,196 @@ +/* -*- 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 mozilla_IdlePeriodState_h +#define mozilla_IdlePeriodState_h + +/** + * A class for tracking the state of our idle period. This includes keeping + * track of both the state of our process-local idle period estimate and, for + * content processes, managing communication with the parent process for + * cross-pprocess idle detection. + */ + +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TimeStamp.h" +#include "nsCOMPtr.h" + +#include + +class nsIIdlePeriod; + +namespace mozilla { +class TaskManager; +namespace ipc { +class IdleSchedulerChild; +} // namespace ipc + +class IdlePeriodState { + public: + explicit IdlePeriodState(already_AddRefed&& aIdlePeriod); + + ~IdlePeriodState(); + + // Integration with memory reporting. + size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const; + + // Notification that whoever we are tracking idle state for has found a + // non-idle task to process. + // + // Must not be called while holding any locks. + void FlagNotIdle(); + + // Notification that whoever we are tracking idle state for has no more + // tasks (idle or not) to process. + // + // aProofOfUnlock is the proof that our caller unlocked its mutex. + void RanOutOfTasks(const MutexAutoUnlock& aProofOfUnlock); + + // Notification that whoever we are tracking idle state has idle tasks that + // they are considering ready to run and that we should keep claiming they are + // ready to run until they call ForgetPendingTaskGuarantee(). + void EnforcePendingTaskGuarantee() { + mHasPendingEventsPromisedIdleEvent = true; + } + + // Notification that whoever we are tracking idle state for is done with our + // "we have an idle event ready to run" guarantee. When this happens, we can + // reset mHasPendingEventsPromisedIdleEvent to false, because we have + // fulfilled our contract. + void ForgetPendingTaskGuarantee() { + mHasPendingEventsPromisedIdleEvent = false; + } + + // Update our cached idle deadline so consumers can use it while holding + // locks. Consumers must ClearCachedIdleDeadline() once they are done. + void UpdateCachedIdleDeadline(const MutexAutoUnlock& aProofOfUnlock) { + mCachedIdleDeadline = GetIdleDeadlineInternal(false, aProofOfUnlock); + } + + // If we have local idle deadline, but don't have an idle token, this will + // request such from the parent process when this is called in a child + // process. + void RequestIdleDeadlineIfNeeded(const MutexAutoUnlock& aProofOfUnlock) { + GetIdleDeadlineInternal(false, aProofOfUnlock); + } + + // Reset our cached idle deadline, so we stop allowing idle runnables to run. + void ClearCachedIdleDeadline() { mCachedIdleDeadline = TimeStamp(); } + + // Get the current cached idle deadline. This may return a null timestamp. + TimeStamp GetCachedIdleDeadline() { return mCachedIdleDeadline; } + + // Peek our current idle deadline into mCachedIdleDeadline. This can cause + // mCachedIdleDeadline to be a null timestamp (which means we are not idle + // right now). This method does not have any side-effects on our state, apart + // from guaranteeing that if it returns non-null then GetDeadlineForIdleTask + // will return non-null until ForgetPendingTaskGuarantee() is called, and its + // effects on mCachedIdleDeadline. + // + // aProofOfUnlock is the proof that our caller unlocked its mutex. + void CachePeekedIdleDeadline(const MutexAutoUnlock& aProofOfUnlock) { + mCachedIdleDeadline = GetIdleDeadlineInternal(true, aProofOfUnlock); + } + + void SetIdleToken(uint64_t aId, TimeDuration aDuration); + + bool IsActive() { return mActive; } + + protected: + void EnsureIsActive() { + if (!mActive) { + SetActive(); + } + } + + void EnsureIsPaused(const MutexAutoUnlock& aProofOfUnlock) { + if (mActive) { + SetPaused(aProofOfUnlock); + } + } + + // Returns a null TimeStamp if we're not in the idle period. + TimeStamp GetLocalIdleDeadline(bool& aShuttingDown, + const MutexAutoUnlock& aProofOfUnlock); + + // Gets the idle token, which is the end time of the idle period. + // + // aProofOfUnlock is the proof that our caller unlocked its mutex. + TimeStamp GetIdleToken(TimeStamp aLocalIdlePeriodHint, + const MutexAutoUnlock& aProofOfUnlock); + + // In case of child processes, requests idle time from the cross-process + // idle scheduler. + void RequestIdleToken(TimeStamp aLocalIdlePeriodHint); + + // Mark that we don't have idle time to use, nor are expecting to get an idle + // token from the idle scheduler. This must be called while not holding any + // locks, but some of the callers aren't holding locks to start with, so + // consumers just need to make sure they are not holding locks when they call + // this. + void ClearIdleToken(); + + // SetActive should be called when the event queue is running any type of + // tasks. + void SetActive(); + // SetPaused should be called once the event queue doesn't have more + // tasks to process, or is waiting for the idle token. + // + // aProofOfUnlock is the proof that our caller unlocked its mutex. + void SetPaused(const MutexAutoUnlock& aProofOfUnlock); + + // Get or peek our idle deadline. When peeking, we generally don't change any + // of our internal state. When getting, we may request an idle token as + // needed. + // + // aProofOfUnlock is the proof that our caller unlocked its mutex. + TimeStamp GetIdleDeadlineInternal(bool aIsPeek, + const MutexAutoUnlock& aProofOfUnlock); + + // Whether we should be getting an idle token (i.e. are a content process + // and are using cross process idle scheduling). + bool ShouldGetIdleToken(); + + // Set to true if we have claimed we have a ready-to-run idle task when asked. + // In that case, we will ensure that we allow at least one task to run when + // someone tries to run a task, even if we have run out of idle period at that + // point. This ensures that we never fail to produce a task to run if we + // claim we have a task ready to run. + bool mHasPendingEventsPromisedIdleEvent = false; + + // mIdlePeriod keeps track of the current idle period. Calling + // mIdlePeriod->GetIdlePeriodHint() will give an estimate of when + // the current idle period will end. + nsCOMPtr mIdlePeriod; + + // If non-null, this timestamp represents the end time of the idle period. An + // idle period starts when we get the idle token from the parent process and + // ends when either there are no more things we want to run at idle priority + // or mIdleToken < TimeStamp::Now(), so we have reached our idle deadline. + TimeStamp mIdleToken; + + // The id of the last idle request to the cross-process idle scheduler. + uint64_t mIdleRequestId = 0; + + // If we're in a content process, we use mIdleScheduler to communicate with + // the parent process for purposes of cross-process idle tracking. + RefPtr mIdleScheduler; + + // Our cached idle deadline. This is set by UpdateCachedIdleDeadline() and + // cleared by ClearCachedIdleDeadline(). Consumers should do the former while + // not holding any locks, but may do the latter while holding locks. + TimeStamp mCachedIdleDeadline; + + // mActive is true when the PrioritizedEventQueue or TaskController we are + // associated with is running tasks. + bool mActive = true; +}; + +} // namespace mozilla + +#endif // mozilla_IdlePeriodState_h diff --git a/xpcom/threads/IdleTaskRunner.cpp b/xpcom/threads/IdleTaskRunner.cpp new file mode 100644 index 0000000000..e9bb895dee --- /dev/null +++ b/xpcom/threads/IdleTaskRunner.cpp @@ -0,0 +1,281 @@ +/* -*- 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 "IdleTaskRunner.h" +#include "mozilla/TaskController.h" +#include "nsRefreshDriver.h" + +namespace mozilla { + +already_AddRefed IdleTaskRunner::Create( + const CallbackType& aCallback, const char* aRunnableName, + TimeDuration aStartDelay, TimeDuration aMaxDelay, + TimeDuration aMinimumUsefulBudget, bool aRepeating, + const MayStopProcessingCallbackType& aMayStopProcessing, + const RequestInterruptCallbackType& aRequestInterrupt) { + if (aMayStopProcessing && aMayStopProcessing()) { + return nullptr; + } + + RefPtr runner = new IdleTaskRunner( + aCallback, aRunnableName, aStartDelay, aMaxDelay, aMinimumUsefulBudget, + aRepeating, aMayStopProcessing, aRequestInterrupt); + runner->Schedule(false); // Initial scheduling shouldn't use idle dispatch. + return runner.forget(); +} + +class IdleTaskRunnerTask : public Task { + public: + explicit IdleTaskRunnerTask(IdleTaskRunner* aRunner) + : Task(Kind::MainThreadOnly, EventQueuePriority::Idle), + mRunner(aRunner), + mRequestInterrupt(aRunner->mRequestInterrupt) { + SetManager(TaskController::Get()->GetIdleTaskManager()); + } + + TaskResult Run() override { + if (mRunner) { + // IdleTaskRunner::Run can actually trigger the destruction of the + // IdleTaskRunner. Make sure it doesn't get destroyed before the method + // finished. + RefPtr runner(mRunner); + runner->Run(); + } + return TaskResult::Complete; + } + + void SetIdleDeadline(TimeStamp aDeadline) override { + if (mRunner) { + mRunner->SetIdleDeadline(aDeadline); + } + } + + void Cancel() { mRunner = nullptr; } + + bool GetName(nsACString& aName) override { + if (mRunner) { + aName.Assign(mRunner->GetName()); + } else { + aName.Assign("ExpiredIdleTaskRunner"); + } + return true; + } + + void RequestInterrupt(uint32_t aInterruptPriority) override { + if (mRequestInterrupt) { + mRequestInterrupt(aInterruptPriority); + } + } + + private: + IdleTaskRunner* mRunner; + + // Copied here and invoked even if there is no mRunner currently, to avoid + // race conditions checking mRunner when an interrupt is requested. + IdleTaskRunner::RequestInterruptCallbackType mRequestInterrupt; +}; + +IdleTaskRunner::IdleTaskRunner( + const CallbackType& aCallback, const char* aRunnableName, + TimeDuration aStartDelay, TimeDuration aMaxDelay, + TimeDuration aMinimumUsefulBudget, bool aRepeating, + const MayStopProcessingCallbackType& aMayStopProcessing, + const RequestInterruptCallbackType& aRequestInterrupt) + : mCallback(aCallback), + mStartTime(TimeStamp::Now() + aStartDelay), + mMaxDelay(aMaxDelay), + mMinimumUsefulBudget(aMinimumUsefulBudget), + mRepeating(aRepeating), + mTimerActive(false), + mMayStopProcessing(aMayStopProcessing), + mRequestInterrupt(aRequestInterrupt), + mName(aRunnableName) {} + +void IdleTaskRunner::Run() { + if (!mCallback) { + return; + } + + // Deadline is null when called from timer or RunNextCollectorTimer rather + // than during idle time. + TimeStamp now = TimeStamp::Now(); + + // Note that if called from RunNextCollectorTimer, we may not have reached + // mStartTime yet. Pretend we are overdue for idle time. + bool overdueForIdle = mDeadline.IsNull(); + bool didRun = false; + bool allowIdleDispatch = false; + + if (mTask) { + // If we find ourselves here we should usually be running from this task, + // but there are exceptions. In any case we're doing the work now and don't + // need our task going forward unless we're re-scheduled. + nsRefreshDriver::CancelIdleTask(mTask); + // Extra safety, make sure a task can never have a dangling ptr. + mTask->Cancel(); + mTask = nullptr; + } + + if (overdueForIdle || ((now + mMinimumUsefulBudget) < mDeadline)) { + CancelTimer(); + didRun = mCallback(mDeadline); + // If we didn't do meaningful work, don't schedule using immediate + // idle dispatch, since that could lead to a loop until the idle + // period ends. + allowIdleDispatch = didRun; + } else if (now >= mDeadline) { + allowIdleDispatch = true; + } + + if (mCallback && (mRepeating || !didRun)) { + Schedule(allowIdleDispatch); + } else { + mCallback = nullptr; + } +} + +static void TimedOut(nsITimer* aTimer, void* aClosure) { + RefPtr runner = static_cast(aClosure); + runner->Run(); +} + +void IdleTaskRunner::SetIdleDeadline(mozilla::TimeStamp aDeadline) { + mDeadline = aDeadline; +} + +void IdleTaskRunner::SetMinimumUsefulBudget(int64_t aMinimumUsefulBudget) { + mMinimumUsefulBudget = TimeDuration::FromMilliseconds(aMinimumUsefulBudget); +} + +void IdleTaskRunner::SetTimer(TimeDuration aDelay, nsIEventTarget* aTarget) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aTarget->IsOnCurrentThread()); + // aTarget is always the main thread event target provided from + // NS_DispatchToCurrentThreadQueue(). We ignore aTarget here to ensure that + // CollectorRunner always run specifically the main thread. + SetTimerInternal(aDelay); +} + +void IdleTaskRunner::Cancel() { + CancelTimer(); + mTimer = nullptr; + mScheduleTimer = nullptr; + mCallback = nullptr; +} + +static void ScheduleTimedOut(nsITimer* aTimer, void* aClosure) { + RefPtr runnable = static_cast(aClosure); + runnable->Schedule(true); +} + +void IdleTaskRunner::Schedule(bool aAllowIdleDispatch) { + if (!mCallback) { + return; + } + + if (mMayStopProcessing && mMayStopProcessing()) { + Cancel(); + return; + } + + mDeadline = TimeStamp(); + + TimeStamp now = TimeStamp::Now(); + + if (aAllowIdleDispatch) { + SetTimerInternal(mMaxDelay); + if (!mTask) { + mTask = new IdleTaskRunnerTask(this); + RefPtr task(mTask); + TaskController::Get()->AddTask(task.forget()); + } + return; + } + + bool useRefreshDriver = false; + if (now >= mStartTime) { + // Detect whether the refresh driver is ticking by checking if + // GetIdleDeadlineHint returns its input parameter. + useRefreshDriver = + (nsRefreshDriver::GetIdleDeadlineHint( + now, nsRefreshDriver::IdleCheck::OnlyThisProcessRefreshDriver) != + now); + } + + if (useRefreshDriver) { + if (!mTask) { + // If a task was already scheduled, no point rescheduling. + mTask = new IdleTaskRunnerTask(this); + // RefreshDriver is ticking, let it schedule the idle dispatch. + nsRefreshDriver::DispatchIdleTaskAfterTickUnlessExists(mTask); + } + // Ensure we get called at some point, even if RefreshDriver is stopped. + SetTimerInternal(mMaxDelay); + } else { + // RefreshDriver doesn't seem to be running. + if (!mScheduleTimer) { + mScheduleTimer = NS_NewTimer(); + if (!mScheduleTimer) { + return; + } + } else { + mScheduleTimer->Cancel(); + } + // We weren't allowed to do idle dispatch immediately, do it after a + // short timeout. (Or wait for our start time if we haven't started yet.) + uint32_t waitToSchedule = 16; /* ms */ + if (now < mStartTime) { + // + 1 to round milliseconds up to be sure to wait until after + // mStartTime. + waitToSchedule = (mStartTime - now).ToMilliseconds() + 1; + } + mScheduleTimer->InitWithNamedFuncCallback( + ScheduleTimedOut, this, waitToSchedule, + nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY, mName); + } +} + +IdleTaskRunner::~IdleTaskRunner() { CancelTimer(); } + +void IdleTaskRunner::CancelTimer() { + if (mTask) { + nsRefreshDriver::CancelIdleTask(mTask); + mTask->Cancel(); + mTask = nullptr; + } + if (mTimer) { + mTimer->Cancel(); + } + if (mScheduleTimer) { + mScheduleTimer->Cancel(); + } + mTimerActive = false; +} + +void IdleTaskRunner::SetTimerInternal(TimeDuration aDelay) { + if (mTimerActive) { + return; + } + ResetTimer(aDelay); +} + +void IdleTaskRunner::ResetTimer(TimeDuration aDelay) { + mTimerActive = false; + + if (!mTimer) { + mTimer = NS_NewTimer(); + } else { + mTimer->Cancel(); + } + + if (mTimer) { + mTimer->InitWithNamedFuncCallback(TimedOut, this, aDelay.ToMilliseconds(), + nsITimer::TYPE_ONE_SHOT, mName); + mTimerActive = true; + } +} + +} // end of namespace mozilla diff --git a/xpcom/threads/IdleTaskRunner.h b/xpcom/threads/IdleTaskRunner.h new file mode 100644 index 0000000000..97a8c0b09e --- /dev/null +++ b/xpcom/threads/IdleTaskRunner.h @@ -0,0 +1,125 @@ +/* -*- 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 IdleTaskRunner_h +#define IdleTaskRunner_h + +#include "mozilla/TimeStamp.h" +#include "nsIEventTarget.h" +#include "nsISupports.h" +#include "nsITimer.h" +#include + +namespace mozilla { + +class IdleTaskRunnerTask; + +// A general purpose repeating callback runner (it can be configured to a +// one-time runner, too.) If it is running repeatedly, one has to either +// explicitly Cancel() the runner or have MayStopProcessing() callback return +// true to completely remove the runner. +class IdleTaskRunner { + public: + friend class IdleTaskRunnerTask; + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(IdleTaskRunner) + + // Return true if some meaningful work was done. + using CallbackType = std::function; + + // A callback for "stop processing" decision. Return true to + // stop processing. This can be an alternative to Cancel() or + // work together in different way. + using MayStopProcessingCallbackType = std::function; + + // A callback to be invoked when an interrupt is requested + // (eg during an idle activity when the user presses a key.) + // The callback takes an "interrupt priority" value as its + // sole parameter. + using RequestInterruptCallbackType = std::function; + + public: + // An IdleTaskRunner has (up to) three phases: + // + // - (duration aStartDelay) waiting to run (aStartDelay can be zero) + // + // - (duration aMaxDelay) attempting to find a long enough amount of idle + // time, at least aMinimumUsefulBudget + // + // - overdue for idle time, run as soon as possible + // + // If aRepeating is true, then aStartDelay applies only to the first run; the + // second run will attempt to run in the first idle slice that is long + // enough. + // + // All durations are in milliseconds. + // + static already_AddRefed Create( + const CallbackType& aCallback, const char* aRunnableName, + TimeDuration aStartDelay, TimeDuration aMaxDelay, + TimeDuration aMinimumUsefulBudget, bool aRepeating, + const MayStopProcessingCallbackType& aMayStopProcessing, + const RequestInterruptCallbackType& aRequestInterrupt = nullptr); + + void Run(); + + // (Used by the task triggering code.) Record the end of the current idle + // period, or null if not running during idle time. + void SetIdleDeadline(mozilla::TimeStamp aDeadline); + + // If the timer is already active, SetTimer doesn't do anything. + void SetTimer(TimeDuration aDelay, nsIEventTarget* aTarget); + + void ResetTimer(TimeDuration aDelay); + + // Update the minimum idle time that this callback would be invoked for. + void SetMinimumUsefulBudget(int64_t aMinimumUsefulBudget); + + void Cancel(); + + void Schedule(bool aAllowIdleDispatch); + + const char* GetName() { return mName; } + + private: + explicit IdleTaskRunner( + const CallbackType& aCallback, const char* aRunnableName, + TimeDuration aStartDelay, TimeDuration aMaxDelay, + TimeDuration aMinimumUsefulBudget, bool aRepeating, + const MayStopProcessingCallbackType& aMayStopProcessing, + const RequestInterruptCallbackType& aRequestInterrupt); + ~IdleTaskRunner(); + void CancelTimer(); + void SetTimerInternal(TimeDuration aDelay); + + nsCOMPtr mTimer; + nsCOMPtr mScheduleTimer; + CallbackType mCallback; + + // Do not run until this time. + const mozilla::TimeStamp mStartTime; + + // Wait this long for idle time before giving up and running a non-idle + // callback. + TimeDuration mMaxDelay; + + // If running during idle time, the expected end of the current idle period. + // The null timestamp when the run is triggered by aMaxDelay instead of idle. + TimeStamp mDeadline; + + // The least duration worth calling the callback for during idle time. + TimeDuration mMinimumUsefulBudget; + + bool mRepeating; + bool mTimerActive; + MayStopProcessingCallbackType mMayStopProcessing; + RequestInterruptCallbackType mRequestInterrupt; + const char* mName; + RefPtr mTask; +}; + +} // end of namespace mozilla. + +#endif diff --git a/xpcom/threads/InputTaskManager.cpp b/xpcom/threads/InputTaskManager.cpp new file mode 100644 index 0000000000..42b2814290 --- /dev/null +++ b/xpcom/threads/InputTaskManager.cpp @@ -0,0 +1,156 @@ +/* -*- 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 "InputTaskManager.h" +#include "VsyncTaskManager.h" +#include "nsRefreshDriver.h" + +namespace mozilla { + +StaticRefPtr InputTaskManager::gInputTaskManager; + +void InputTaskManager::EnableInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInputQueueState == STATE_DISABLED); + mInputQueueState = STATE_ENABLED; +} + +void InputTaskManager::FlushInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInputQueueState == STATE_ENABLED || + mInputQueueState == STATE_SUSPEND); + mInputQueueState = + mInputQueueState == STATE_ENABLED ? STATE_FLUSHING : STATE_SUSPEND; +} + +void InputTaskManager::SuspendInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInputQueueState == STATE_ENABLED || + mInputQueueState == STATE_FLUSHING); + mInputQueueState = STATE_SUSPEND; +} + +void InputTaskManager::ResumeInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(mInputQueueState == STATE_SUSPEND); + mInputQueueState = STATE_ENABLED; +} + +int32_t InputTaskManager::GetPriorityModifierForEventLoopTurn( + const MutexAutoLock& aProofOfLock) { + // When the state is disabled, the input task that we have is + // very likely SuspendInputEventQueue, so here we also use + // normal priority as ResumeInputEventQueue, FlushInputEventQueue and + // SetInputEventQueueEnabled all uses normal priority, to + // ensure the ordering is correct. + if (State() == InputTaskManager::STATE_DISABLED) { + return static_cast(EventQueuePriority::Normal) - + static_cast(EventQueuePriority::InputHigh); + } + + return GetPriorityModifierForEventLoopTurnForStrictVsyncAlignment(); +} + +void InputTaskManager::WillRunTask() { + TaskManager::WillRunTask(); + mInputPriorityController.WillRunTask(); +} + +int32_t +InputTaskManager::GetPriorityModifierForEventLoopTurnForStrictVsyncAlignment() { + MOZ_ASSERT(!IsSuspended()); + + size_t inputCount = PendingTaskCount(); + if (inputCount > 0 && + mInputPriorityController.ShouldUseHighestPriority(this)) { + return static_cast(EventQueuePriority::InputHighest) - + static_cast(EventQueuePriority::InputHigh); + } + + if (State() == STATE_FLUSHING || + nsRefreshDriver::GetNextTickHint().isNothing()) { + return 0; + } + + return static_cast(EventQueuePriority::InputLow) - + static_cast(EventQueuePriority::InputHigh); +} + +InputTaskManager::InputPriorityController::InputPriorityController() + : mInputVsyncState(InputVsyncState::NoPendingVsync) {} + +bool InputTaskManager::InputPriorityController::ShouldUseHighestPriority( + InputTaskManager* aInputTaskManager) { + if (mInputVsyncState == InputVsyncState::HasPendingVsync) { + return true; + } + + if (mInputVsyncState == InputVsyncState::RunVsync) { + return false; + } + + if (mInputVsyncState == InputVsyncState::NoPendingVsync && + VsyncTaskManager::Get()->PendingTaskCount()) { + EnterPendingVsyncState(aInputTaskManager->PendingTaskCount()); + return true; + } + + return false; +} + +void InputTaskManager::InputPriorityController::EnterPendingVsyncState( + uint32_t aNumPendingTasks) { + MOZ_ASSERT(mInputVsyncState == InputVsyncState::NoPendingVsync); + + mInputVsyncState = InputVsyncState::HasPendingVsync; + mMaxInputTasksToRun = aNumPendingTasks; + mRunInputStartTime = TimeStamp::Now(); +} + +void InputTaskManager::InputPriorityController::WillRunVsync() { + if (mInputVsyncState == InputVsyncState::RunVsync || + mInputVsyncState == InputVsyncState::HasPendingVsync) { + LeavePendingVsyncState(false); + } +} + +void InputTaskManager::InputPriorityController::LeavePendingVsyncState( + bool aRunVsync) { + if (aRunVsync) { + MOZ_ASSERT(mInputVsyncState == InputVsyncState::HasPendingVsync); + mInputVsyncState = InputVsyncState::RunVsync; + } else { + mInputVsyncState = InputVsyncState::NoPendingVsync; + } + + mMaxInputTasksToRun = 0; +} + +void InputTaskManager::InputPriorityController::WillRunTask() { + switch (mInputVsyncState) { + case InputVsyncState::NoPendingVsync: + return; + case InputVsyncState::HasPendingVsync: + MOZ_ASSERT(mMaxInputTasksToRun > 0); + --mMaxInputTasksToRun; + if (!mMaxInputTasksToRun || + TimeStamp::Now() - mRunInputStartTime >= + TimeDuration::FromMilliseconds( + StaticPrefs::dom_input_event_queue_duration_max())) { + LeavePendingVsyncState(true); + } + return; + default: + MOZ_DIAGNOSTIC_ASSERT( + false, "Shouldn't run this input task when we suppose to run vsync"); + return; + } +} + +// static +void InputTaskManager::Init() { gInputTaskManager = new InputTaskManager(); } + +} // namespace mozilla diff --git a/xpcom/threads/InputTaskManager.h b/xpcom/threads/InputTaskManager.h new file mode 100644 index 0000000000..2f920a31ae --- /dev/null +++ b/xpcom/threads/InputTaskManager.h @@ -0,0 +1,141 @@ +/* -*- 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 mozilla_InputTaskManager_h +#define mozilla_InputTaskManager_h + +#include "nsTArray.h" +#include "nsXULAppAPI.h" +#include "TaskController.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/StaticPrefs_dom.h" + +namespace mozilla { + +class InputTaskManager : public TaskManager { + public: + int32_t GetPriorityModifierForEventLoopTurn( + const MutexAutoLock& aProofOfLock) final; + void WillRunTask() final; + + enum InputEventQueueState { + STATE_DISABLED, + STATE_FLUSHING, + STATE_SUSPEND, + STATE_ENABLED + }; + + void EnableInputEventPrioritization(); + void FlushInputEventPrioritization(); + void SuspendInputEventPrioritization(); + void ResumeInputEventPrioritization(); + + InputEventQueueState State() { return mInputQueueState; } + + void SetState(InputEventQueueState aState) { mInputQueueState = aState; } + + static InputTaskManager* Get() { return gInputTaskManager.get(); } + static void Cleanup() { gInputTaskManager = nullptr; } + static void Init(); + + bool IsSuspended(const MutexAutoLock& aProofOfLock) override { + MOZ_ASSERT(NS_IsMainThread()); + return mInputQueueState == STATE_SUSPEND || mSuspensionLevel > 0; + } + + bool IsSuspended() { + MOZ_ASSERT(NS_IsMainThread()); + return mSuspensionLevel > 0; + } + + void IncSuspensionLevel() { + MOZ_ASSERT(NS_IsMainThread()); + ++mSuspensionLevel; + } + + void DecSuspensionLevel() { + MOZ_ASSERT(NS_IsMainThread()); + --mSuspensionLevel; + } + + static bool CanSuspendInputEvent() { + // Ensure it's content process because InputTaskManager only + // works in e10s. + // + // Input tasks will have nullptr as their task manager when the + // event queue state is STATE_DISABLED, so we can't suspend + // input events. + return XRE_IsContentProcess() && + StaticPrefs::dom_input_events_canSuspendInBCG_enabled() && + InputTaskManager::Get()->State() != + InputEventQueueState::STATE_DISABLED; + } + + void NotifyVsync() { mInputPriorityController.WillRunVsync(); } + + private: + InputTaskManager() : mInputQueueState(STATE_DISABLED) {} + + class InputPriorityController { + public: + InputPriorityController(); + // Determines whether we should use the highest input priority for input + // tasks + bool ShouldUseHighestPriority(InputTaskManager*); + + void WillRunVsync(); + + // Gets called when a input task is going to run; If the current + // input vsync state is `HasPendingVsync`, determines whether we + // should continue running input tasks or leave the `HasPendingVsync` state + // based on + // 1. Whether we still have time to process input tasks + // 2. Whether we have processed the max number of tasks that + // we should process. + void WillRunTask(); + + private: + // Used to represents the relationship between Input and Vsync. + // + // HasPendingVsync: There are pending vsync tasks and we are using + // InputHighest priority for inputs. + // NoPendingVsync: No pending vsync tasks and no need to use InputHighest + // priority. + // RunVsync: Finished running input tasks and the vsync task + // should be run. + enum class InputVsyncState { + HasPendingVsync, + NoPendingVsync, + RunVsync, + }; + + void EnterPendingVsyncState(uint32_t aNumPendingTasks); + void LeavePendingVsyncState(bool aRunVsync); + + // Stores the number of pending input tasks when we enter the + // InputVsyncState::HasPendingVsync state. + uint32_t mMaxInputTasksToRun = 0; + + InputVsyncState mInputVsyncState; + + TimeStamp mRunInputStartTime; + }; + + int32_t GetPriorityModifierForEventLoopTurnForStrictVsyncAlignment(); + + Atomic mInputQueueState; + + static StaticRefPtr gInputTaskManager; + + // Number of BCGs have asked InputTaskManager to suspend input events + uint32_t mSuspensionLevel = 0; + + InputPriorityController mInputPriorityController; +}; + +} // namespace mozilla + +#endif // mozilla_InputTaskManager_h diff --git a/xpcom/threads/LazyIdleThread.cpp b/xpcom/threads/LazyIdleThread.cpp new file mode 100644 index 0000000000..4187fcc4ff --- /dev/null +++ b/xpcom/threads/LazyIdleThread.cpp @@ -0,0 +1,126 @@ +/* -*- 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 "LazyIdleThread.h" + +#include "nsIObserverService.h" +#include "nsServiceManagerUtils.h" +#include "nsThreadUtils.h" + +#ifdef DEBUG +# define ASSERT_OWNING_THREAD() \ + do { \ + MOZ_ASSERT(mOwningEventTarget->IsOnCurrentThread()); \ + } while (0) +#else +# define ASSERT_OWNING_THREAD() /* nothing */ +#endif + +namespace mozilla { + +LazyIdleThread::LazyIdleThread(uint32_t aIdleTimeoutMS, const char* aName, + ShutdownMethod aShutdownMethod) + : mOwningEventTarget(GetCurrentSerialEventTarget()), + mThreadPool(new nsThreadPool()), + mTaskQueue(TaskQueue::Create(do_AddRef(mThreadPool), aName)) { + // Configure the threadpool to host a single thread. It will be responsible + // for managing the thread's lifetime. + MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetThreadLimit(1)); + MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadLimit(1)); + MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetIdleThreadTimeout(aIdleTimeoutMS)); + MOZ_ALWAYS_SUCCEEDS(mThreadPool->SetName(nsDependentCString(aName))); + + if (aShutdownMethod == ShutdownMethod::AutomaticShutdown && + NS_IsMainThread()) { + if (nsCOMPtr obs = + do_GetService(NS_OBSERVERSERVICE_CONTRACTID)) { + MOZ_ALWAYS_SUCCEEDS( + obs->AddObserver(this, "xpcom-shutdown-threads", false)); + } + } +} + +static void LazyIdleThreadShutdown(nsThreadPool* aThreadPool, + TaskQueue* aTaskQueue) { + aTaskQueue->BeginShutdown(); + aTaskQueue->AwaitShutdownAndIdle(); + aThreadPool->Shutdown(); +} + +LazyIdleThread::~LazyIdleThread() { + if (!mShutdown) { + mOwningEventTarget->Dispatch(NS_NewRunnableFunction( + "LazyIdleThread::~LazyIdleThread", + [threadPool = mThreadPool, taskQueue = mTaskQueue] { + LazyIdleThreadShutdown(threadPool, taskQueue); + })); + } +} + +void LazyIdleThread::Shutdown() { + ASSERT_OWNING_THREAD(); + + if (!mShutdown) { + mShutdown = true; + LazyIdleThreadShutdown(mThreadPool, mTaskQueue); + } +} + +nsresult LazyIdleThread::SetListener(nsIThreadPoolListener* aListener) { + return mThreadPool->SetListener(aListener); +} + +NS_IMPL_ISUPPORTS(LazyIdleThread, nsIEventTarget, nsISerialEventTarget, + nsIObserver) + +NS_IMETHODIMP +LazyIdleThread::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + nsCOMPtr event(aEvent); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +LazyIdleThread::Dispatch(already_AddRefed aEvent, + uint32_t aFlags) { + return mTaskQueue->Dispatch(std::move(aEvent), aFlags); +} + +NS_IMETHODIMP +LazyIdleThread::DelayedDispatch(already_AddRefed, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LazyIdleThread::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LazyIdleThread::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +LazyIdleThread::IsOnCurrentThread(bool* aIsOnCurrentThread) { + return mTaskQueue->IsOnCurrentThread(aIsOnCurrentThread); +} + +NS_IMETHODIMP_(bool) +LazyIdleThread::IsOnCurrentThreadInfallible() { + return mTaskQueue->IsOnCurrentThreadInfallible(); +} + +NS_IMETHODIMP +LazyIdleThread::Observe(nsISupports* /* aSubject */, const char* aTopic, + const char16_t* /* aData */) { + MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); + MOZ_ASSERT(!strcmp("xpcom-shutdown-threads", aTopic), "Bad topic!"); + + Shutdown(); + return NS_OK; +} + +} // namespace mozilla diff --git a/xpcom/threads/LazyIdleThread.h b/xpcom/threads/LazyIdleThread.h new file mode 100644 index 0000000000..8a720bc69b --- /dev/null +++ b/xpcom/threads/LazyIdleThread.h @@ -0,0 +1,93 @@ +/* -*- 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 mozilla_lazyidlethread_h__ +#define mozilla_lazyidlethread_h__ + +#ifndef MOZILLA_INTERNAL_API +# error "This header is only usable from within libxul (MOZILLA_INTERNAL_API)." +#endif + +#include "mozilla/TaskQueue.h" +#include "nsIObserver.h" +#include "nsThreadPool.h" + +namespace mozilla { + +/** + * This class provides a basic event target that creates its thread lazily and + * destroys its thread after a period of inactivity. It may be created and used + * on any thread but it may only be shut down from the thread on which it is + * created. + */ +class LazyIdleThread final : public nsISerialEventTarget, public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + NS_DECL_NSIOBSERVER + + /** + * If AutomaticShutdown is specified, and the LazyIdleThread is created on the + * main thread, Shutdown() will automatically be called during + * xpcom-shutdown-threads. + */ + enum ShutdownMethod { AutomaticShutdown = 0, ManualShutdown }; + + /** + * Create a new LazyIdleThread that will destroy its thread after the given + * number of milliseconds. + */ + LazyIdleThread(uint32_t aIdleTimeoutMS, const char* aName, + ShutdownMethod aShutdownMethod = AutomaticShutdown); + + /** + * Shuts down the LazyIdleThread, waiting for any pending work to complete. + * Must be called from mOwningEventTarget. + */ + void Shutdown(); + + /** + * Register a nsIThreadPoolListener on the underlying threadpool to track the + * thread as it is created/destroyed. + */ + nsresult SetListener(nsIThreadPoolListener* aListener); + + private: + /** + * Asynchronously shuts down the LazyIdleThread on mOwningEventTarget. + */ + ~LazyIdleThread(); + + /** + * The thread which created this LazyIdleThread and is responsible for + * shutting it down. + */ + const nsCOMPtr mOwningEventTarget; + + /** + * The single-thread backing threadpool which provides the actual threads used + * by LazyIdleThread, and implements the timeout. + */ + const RefPtr mThreadPool; + + /** + * The serial event target providing a `nsISerialEventTarget` implementation + * when on the LazyIdleThread. + */ + const RefPtr mTaskQueue; + + /** + * Only safe to access on the owning thread or in the destructor (as no other + * threads have access then). If `true`, means the LazyIdleThread has already + * been shut down, so it does not need to be shut down asynchronously from the + * destructor. + */ + bool mShutdown = false; +}; + +} // namespace mozilla + +#endif // mozilla_lazyidlethread_h__ diff --git a/xpcom/threads/LeakRefPtr.h b/xpcom/threads/LeakRefPtr.h new file mode 100644 index 0000000000..ee260dad7e --- /dev/null +++ b/xpcom/threads/LeakRefPtr.h @@ -0,0 +1,48 @@ +/* -*- 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/. */ + +/* Smart pointer which leaks its owning refcounted object by default. */ + +#ifndef LeakRefPtr_h +#define LeakRefPtr_h + +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla { + +/** + * Instance of this class behaves like a raw pointer which leaks the + * resource it's owning if not explicitly released. + */ +template +class LeakRefPtr { + public: + explicit LeakRefPtr(already_AddRefed&& aPtr) : mRawPtr(aPtr.take()) {} + + explicit operator bool() const { return !!mRawPtr; } + + LeakRefPtr& operator=(already_AddRefed&& aPtr) { + mRawPtr = aPtr.take(); + return *this; + } + + T* get() const { return mRawPtr; } + + already_AddRefed take() { + T* rawPtr = mRawPtr; + mRawPtr = nullptr; + return already_AddRefed(rawPtr); + } + + void release() { NS_RELEASE(mRawPtr); } + + private: + T* MOZ_OWNING_REF mRawPtr; +}; + +} // namespace mozilla + +#endif // LeakRefPtr_h diff --git a/xpcom/threads/MainThreadIdlePeriod.cpp b/xpcom/threads/MainThreadIdlePeriod.cpp new file mode 100644 index 0000000000..805626dd67 --- /dev/null +++ b/xpcom/threads/MainThreadIdlePeriod.cpp @@ -0,0 +1,77 @@ +/* -*- 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 "MainThreadIdlePeriod.h" + +#include "mozilla/Maybe.h" +#include "mozilla/Preferences.h" +#include "mozilla/StaticPrefs_idle_period.h" +#include "mozilla/dom/Document.h" +#include "VRManagerChild.h" +#include "nsRefreshDriver.h" +#include "nsThreadUtils.h" + +// The amount of idle time (milliseconds) reserved for a long idle period. +static const double kLongIdlePeriodMS = 50.0; + +// The minimum amount of time (milliseconds) required for an idle period to be +// scheduled on the main thread. N.B. layout.idle_period.time_limit adds +// padding at the end of the idle period, which makes the point in time that we +// expect to become busy again be: +// now + idle_period.min + layout.idle_period.time_limit +// or during page load +// now + idle_period.during_page_load.min + layout.idle_period.time_limit + +static const uint32_t kMaxTimerThreadBound = 25; // Number of timers to check. + +namespace mozilla { + +NS_IMETHODIMP +MainThreadIdlePeriod::GetIdlePeriodHint(TimeStamp* aIdleDeadline) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aIdleDeadline); + + TimeStamp now = TimeStamp::Now(); + TimeStamp currentGuess = + now + TimeDuration::FromMilliseconds(kLongIdlePeriodMS); + + currentGuess = nsRefreshDriver::GetIdleDeadlineHint( + currentGuess, nsRefreshDriver::IdleCheck::AllVsyncListeners); + if (XRE_IsContentProcess()) { + currentGuess = gfx::VRManagerChild::GetIdleDeadlineHint(currentGuess); + } + currentGuess = NS_GetTimerDeadlineHintOnCurrentThread(currentGuess, + kMaxTimerThreadBound); + + // If the idle period is too small, then just return a null time + // to indicate we are busy. Otherwise return the actual deadline. + double highRateMultiplier = nsRefreshDriver::HighRateMultiplier(); + TimeDuration minIdlePeriod = TimeDuration::FromMilliseconds( + std::max(highRateMultiplier * StaticPrefs::idle_period_min(), 1.0)); + bool busySoon = currentGuess.IsNull() || + (now >= (currentGuess - minIdlePeriod)) || + currentGuess < mLastIdleDeadline; + + // During page load use higher minimum idle period. + if (!busySoon && XRE_IsContentProcess() && + mozilla::dom::Document::HasRecentlyStartedForegroundLoads()) { + TimeDuration minIdlePeriod = TimeDuration::FromMilliseconds(std::max( + highRateMultiplier * StaticPrefs::idle_period_during_page_load_min(), + 1.0)); + busySoon = (now >= (currentGuess - minIdlePeriod)); + } + + if (!busySoon) { + *aIdleDeadline = mLastIdleDeadline = currentGuess; + } + + return NS_OK; +} + +/* static */ +float MainThreadIdlePeriod::GetLongIdlePeriod() { return kLongIdlePeriodMS; } + +} // namespace mozilla diff --git a/xpcom/threads/MainThreadIdlePeriod.h b/xpcom/threads/MainThreadIdlePeriod.h new file mode 100644 index 0000000000..7e382f6771 --- /dev/null +++ b/xpcom/threads/MainThreadIdlePeriod.h @@ -0,0 +1,31 @@ +/* -*- 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 mozilla_dom_mainthreadidleperiod_h +#define mozilla_dom_mainthreadidleperiod_h + +#include "mozilla/TimeStamp.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +class MainThreadIdlePeriod final : public IdlePeriod { + public: + NS_DECL_NSIIDLEPERIOD + + MainThreadIdlePeriod() : mLastIdleDeadline(TimeStamp::Now()) {} + + static float GetLongIdlePeriod(); + + private: + virtual ~MainThreadIdlePeriod() = default; + + TimeStamp mLastIdleDeadline; +}; + +} // namespace mozilla + +#endif // mozilla_dom_mainthreadidleperiod_h diff --git a/xpcom/threads/MainThreadUtils.h b/xpcom/threads/MainThreadUtils.h new file mode 100644 index 0000000000..152805eb66 --- /dev/null +++ b/xpcom/threads/MainThreadUtils.h @@ -0,0 +1,60 @@ +/* -*- 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 MainThreadUtils_h_ +#define MainThreadUtils_h_ + +#include "mozilla/Assertions.h" +#include "mozilla/ThreadSafety.h" +#include "nscore.h" + +class nsIThread; + +/** + * Get a reference to the main thread. + * + * @param aResult + * The resulting nsIThread object. + */ +extern nsresult NS_GetMainThread(nsIThread** aResult); + +#ifdef MOZILLA_INTERNAL_API +bool NS_IsMainThreadTLSInitialized(); +extern "C" { +bool NS_IsMainThread(); +} + +namespace mozilla { + +/** + * A dummy static capability for the thread safety analysis which can be + * required by functions and members using `MOZ_REQUIRE(sMainThreadCapability)` + * and `MOZ_GUARDED_BY(sMainThreadCapability)` and asserted using + * `AssertIsOnMainThread()`. + * + * If you want a thread-safety-analysis capability for a non-main thread, + * consider using the `EventTargetCapability` type. + */ +class MOZ_CAPABILITY("main thread") MainThreadCapability final {}; +constexpr MainThreadCapability sMainThreadCapability; + +# ifdef DEBUG +void AssertIsOnMainThread() MOZ_ASSERT_CAPABILITY(sMainThreadCapability); +# else +inline void AssertIsOnMainThread() + MOZ_ASSERT_CAPABILITY(sMainThreadCapability) {} +# endif + +inline void ReleaseAssertIsOnMainThread() + MOZ_ASSERT_CAPABILITY(sMainThreadCapability) { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); +} + +} // namespace mozilla + +#endif + +#endif // MainThreadUtils_h_ diff --git a/xpcom/threads/Monitor.h b/xpcom/threads/Monitor.h new file mode 100644 index 0000000000..7bb91f9ad7 --- /dev/null +++ b/xpcom/threads/Monitor.h @@ -0,0 +1,316 @@ +/* -*- 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 mozilla_Monitor_h +#define mozilla_Monitor_h + +#include "mozilla/CondVar.h" +#include "mozilla/Mutex.h" + +namespace mozilla { + +/** + * Monitor provides a *non*-reentrant monitor: *not* a Java-style + * monitor. If your code needs support for reentrancy, use + * ReentrantMonitor instead. (Rarely should reentrancy be needed.) + * + * Instead of directly calling Monitor methods, it's safer and simpler + * to instead use the RAII wrappers MonitorAutoLock and + * MonitorAutoUnlock. + */ +class MOZ_CAPABILITY("monitor") Monitor { + public: + explicit Monitor(const char* aName) + : mMutex(aName), mCondVar(mMutex, "[Monitor.mCondVar]") {} + + ~Monitor() = default; + + void Lock() MOZ_CAPABILITY_ACQUIRE() { mMutex.Lock(); } + [[nodiscard]] bool TryLock() MOZ_TRY_ACQUIRE(true) { + return mMutex.TryLock(); + } + void Unlock() MOZ_CAPABILITY_RELEASE() { mMutex.Unlock(); } + + void Wait() MOZ_REQUIRES(this) { mCondVar.Wait(); } + CVStatus Wait(TimeDuration aDuration) MOZ_REQUIRES(this) { + return mCondVar.Wait(aDuration); + } + + void Notify() { mCondVar.Notify(); } + void NotifyAll() { mCondVar.NotifyAll(); } + + void AssertCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(this) { + mMutex.AssertCurrentThreadOwns(); + } + void AssertNotCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(!this) { + mMutex.AssertNotCurrentThreadOwns(); + } + + private: + Monitor() = delete; + Monitor(const Monitor&) = delete; + Monitor& operator=(const Monitor&) = delete; + + Mutex mMutex; + CondVar mCondVar; +}; + +/** + * MonitorSingleWriter + * + * Monitor where a single writer exists, so that reads from the same thread + * will not generate data races or consistency issues. + * + * When possible, use MonitorAutoLock/MonitorAutoUnlock to lock/unlock this + * monitor within a scope, instead of calling Lock/Unlock directly. + * + * This requires an object implementing Mutex's SingleWriterLockOwner, so + * we can do correct-thread checks. + */ + +class MonitorSingleWriter : public Monitor { + public: + // aOwner should be the object that contains the mutex, typically. We + // will use that object (which must have a lifetime the same or greater + // than this object) to verify that we're running on the correct thread, + // typically only in DEBUG builds + explicit MonitorSingleWriter(const char* aName, SingleWriterLockOwner* aOwner) + : Monitor(aName) +#ifdef DEBUG + , + mOwner(aOwner) +#endif + { + MOZ_COUNT_CTOR(MonitorSingleWriter); + MOZ_ASSERT(mOwner); + } + + MOZ_COUNTED_DTOR(MonitorSingleWriter) + + void AssertOnWritingThread() const MOZ_ASSERT_CAPABILITY(this) { + MOZ_ASSERT(mOwner->OnWritingThread()); + } + void AssertOnWritingThreadOrHeld() const MOZ_ASSERT_CAPABILITY(this) { +#ifdef DEBUG + if (!mOwner->OnWritingThread()) { + AssertCurrentThreadOwns(); + } +#endif + } + + private: +#ifdef DEBUG + SingleWriterLockOwner* mOwner MOZ_UNSAFE_REF( + "This is normally the object that contains the MonitorSingleWriter, so " + "we don't want to hold a reference to ourselves"); +#endif + + MonitorSingleWriter() = delete; + MonitorSingleWriter(const MonitorSingleWriter&) = delete; + MonitorSingleWriter& operator=(const MonitorSingleWriter&) = delete; +}; + +/** + * Lock the monitor for the lexical scope instances of this class are + * bound to (except for MonitorAutoUnlock in nested scopes). + * + * The monitor must be unlocked when instances of this class are + * created. + */ +namespace detail { +template +class MOZ_SCOPED_CAPABILITY MOZ_STACK_CLASS BaseMonitorAutoLock { + public: + explicit BaseMonitorAutoLock(T& aMonitor) MOZ_CAPABILITY_ACQUIRE(aMonitor) + : mMonitor(&aMonitor) { + mMonitor->Lock(); + } + + ~BaseMonitorAutoLock() MOZ_CAPABILITY_RELEASE() { mMonitor->Unlock(); } + // It's very hard to mess up MonitorAutoLock lock(mMonitor); ... lock.Wait(). + // The only way you can fail to hold the lock when you call lock.Wait() is to + // use MonitorAutoUnlock. For now we'll ignore that case. + void Wait() { + mMonitor->AssertCurrentThreadOwns(); + mMonitor->Wait(); + } + CVStatus Wait(TimeDuration aDuration) { + mMonitor->AssertCurrentThreadOwns(); + return mMonitor->Wait(aDuration); + } + + void Notify() { mMonitor->Notify(); } + void NotifyAll() { mMonitor->NotifyAll(); } + + // Assert that aLock is the monitor passed to the constructor and that the + // current thread owns the monitor. In coding patterns such as: + // + // void LockedMethod(const BaseAutoLock& aProofOfLock) + // { + // aProofOfLock.AssertOwns(mMonitor); + // ... + // } + // + // Without this assertion, it could be that mMonitor is not actually + // locked. It's possible to have code like: + // + // BaseAutoLock lock(someMonitor); + // ... + // BaseAutoUnlock unlock(someMonitor); + // ... + // LockedMethod(lock); + // + // and in such a case, simply asserting that the monitor pointers match is not + // sufficient; monitor ownership must be asserted as well. + // + // Note that if you are going to use the coding pattern presented above, you + // should use this method in preference to using AssertCurrentThreadOwns on + // the mutex you expected to be held, since this method provides stronger + // guarantees. + void AssertOwns(const T& aMonitor) const MOZ_ASSERT_CAPABILITY(aMonitor) { + MOZ_ASSERT(&aMonitor == mMonitor); + mMonitor->AssertCurrentThreadOwns(); + } + + private: + BaseMonitorAutoLock() = delete; + BaseMonitorAutoLock(const BaseMonitorAutoLock&) = delete; + BaseMonitorAutoLock& operator=(const BaseMonitorAutoLock&) = delete; + static void* operator new(size_t) noexcept(true); + + friend class MonitorAutoUnlock; + + protected: + T* mMonitor; +}; +} // namespace detail +typedef detail::BaseMonitorAutoLock MonitorAutoLock; +typedef detail::BaseMonitorAutoLock + MonitorSingleWriterAutoLock; + +// clang-format off +// Use if we've done AssertOnWritingThread(), and then later need to take the +// lock to write to a protected member. Instead of +// MutexSingleWriterAutoLock lock(mutex) +// use +// MutexSingleWriterAutoLockOnThread(lock, mutex) +// clang-format on +#define MonitorSingleWriterAutoLockOnThread(lock, monitor) \ + MOZ_PUSH_IGNORE_THREAD_SAFETY \ + MonitorSingleWriterAutoLock lock(monitor); \ + MOZ_POP_THREAD_SAFETY + +/** + * Unlock the monitor for the lexical scope instances of this class + * are bound to (except for MonitorAutoLock in nested scopes). + * + * The monitor must be locked by the current thread when instances of + * this class are created. + */ +namespace detail { +template +class MOZ_STACK_CLASS MOZ_SCOPED_CAPABILITY BaseMonitorAutoUnlock { + public: + explicit BaseMonitorAutoUnlock(T& aMonitor) + MOZ_SCOPED_UNLOCK_RELEASE(aMonitor) + : mMonitor(&aMonitor) { + mMonitor->Unlock(); + } + + ~BaseMonitorAutoUnlock() MOZ_SCOPED_UNLOCK_REACQUIRE() { mMonitor->Lock(); } + + private: + BaseMonitorAutoUnlock() = delete; + BaseMonitorAutoUnlock(const BaseMonitorAutoUnlock&) = delete; + BaseMonitorAutoUnlock& operator=(const BaseMonitorAutoUnlock&) = delete; + static void* operator new(size_t) noexcept(true); + + T* mMonitor; +}; +} // namespace detail +typedef detail::BaseMonitorAutoUnlock MonitorAutoUnlock; +typedef detail::BaseMonitorAutoUnlock + MonitorSingleWriterAutoUnlock; + +/** + * Lock the monitor for the lexical scope instances of this class are + * bound to (except for MonitorAutoUnlock in nested scopes). + * + * The monitor must be unlocked when instances of this class are + * created. + */ +class MOZ_SCOPED_CAPABILITY MOZ_STACK_CLASS ReleasableMonitorAutoLock { + public: + explicit ReleasableMonitorAutoLock(Monitor& aMonitor) + MOZ_CAPABILITY_ACQUIRE(aMonitor) + : mMonitor(&aMonitor) { + mMonitor->Lock(); + mLocked = true; + } + + ~ReleasableMonitorAutoLock() MOZ_CAPABILITY_RELEASE() { + if (mLocked) { + mMonitor->Unlock(); + } + } + + // See BaseMonitorAutoLock::Wait + void Wait() { + mMonitor->AssertCurrentThreadOwns(); // someone could have called Unlock() + mMonitor->Wait(); + } + CVStatus Wait(TimeDuration aDuration) { + mMonitor->AssertCurrentThreadOwns(); + return mMonitor->Wait(aDuration); + } + + void Notify() { + MOZ_ASSERT(mLocked); + mMonitor->Notify(); + } + void NotifyAll() { + MOZ_ASSERT(mLocked); + mMonitor->NotifyAll(); + } + + // Allow dropping the lock prematurely; for example to support something like: + // clang-format off + // MonitorAutoLock lock(mMonitor); + // ... + // if (foo) { + // lock.Unlock(); + // MethodThatCantBeCalledWithLock() + // return; + // } + // clang-format on + void Unlock() MOZ_CAPABILITY_RELEASE() { + MOZ_ASSERT(mLocked); + mMonitor->Unlock(); + mLocked = false; + } + void Lock() MOZ_CAPABILITY_ACQUIRE() { + MOZ_ASSERT(!mLocked); + mMonitor->Lock(); + mLocked = true; + } + void AssertCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY() { + mMonitor->AssertCurrentThreadOwns(); + } + + private: + bool mLocked; + Monitor* mMonitor; + + ReleasableMonitorAutoLock() = delete; + ReleasableMonitorAutoLock(const ReleasableMonitorAutoLock&) = delete; + ReleasableMonitorAutoLock& operator=(const ReleasableMonitorAutoLock&) = + delete; + static void* operator new(size_t) noexcept(true); +}; + +} // namespace mozilla + +#endif // mozilla_Monitor_h diff --git a/xpcom/threads/MozPromise.h b/xpcom/threads/MozPromise.h new file mode 100644 index 0000000000..f17e085c2f --- /dev/null +++ b/xpcom/threads/MozPromise.h @@ -0,0 +1,1757 @@ +/* -*- 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/. */ + +#if !defined(MozPromise_h_) +# define MozPromise_h_ + +# include +# include + +# include "mozilla/ErrorNames.h" +# include "mozilla/Logging.h" +# include "mozilla/Maybe.h" +# include "mozilla/Monitor.h" +# include "mozilla/Mutex.h" +# include "mozilla/RefPtr.h" +# include "mozilla/UniquePtr.h" +# include "mozilla/Variant.h" +# include "nsIDirectTaskDispatcher.h" +# include "nsISerialEventTarget.h" +# include "nsTArray.h" +# include "nsThreadUtils.h" + +# ifdef MOZ_WIDGET_ANDROID +# include "mozilla/jni/GeckoResultUtils.h" +# endif + +# if MOZ_DIAGNOSTIC_ASSERT_ENABLED +# define PROMISE_DEBUG +# endif + +# ifdef PROMISE_DEBUG +# define PROMISE_ASSERT MOZ_RELEASE_ASSERT +# else +# define PROMISE_ASSERT(...) \ + do { \ + } while (0) +# endif + +# if DEBUG +# include "nsPrintfCString.h" +# endif + +namespace mozilla { + +namespace dom { +class Promise; +} + +extern LazyLogModule gMozPromiseLog; + +# define PROMISE_LOG(x, ...) \ + MOZ_LOG(gMozPromiseLog, mozilla::LogLevel::Debug, (x, ##__VA_ARGS__)) + +namespace detail { +template +struct MethodTraitsHelper : MethodTraitsHelper {}; +template +struct MethodTraitsHelper { + using ReturnType = Ret; + static const size_t ArgSize = sizeof...(ArgTypes); +}; +template +struct MethodTraitsHelper { + using ReturnType = Ret; + static const size_t ArgSize = sizeof...(ArgTypes); +}; +template +struct MethodTraitsHelper { + using ReturnType = Ret; + static const size_t ArgSize = sizeof...(ArgTypes); +}; +template +struct MethodTraitsHelper { + using ReturnType = Ret; + static const size_t ArgSize = sizeof...(ArgTypes); +}; +template +struct MethodTrait : MethodTraitsHelper> {}; + +} // namespace detail + +template +using TakesArgument = + std::integral_constant::ArgSize != 0>; + +template +using ReturnTypeIs = + std::is_convertible::ReturnType, + TargetType>; + +template +class MozPromise; + +template +struct IsMozPromise : std::false_type {}; + +template +struct IsMozPromise> + : std::true_type {}; + +/* + * A promise manages an asynchronous request that may or may not be able to be + * fulfilled immediately. When an API returns a promise, the consumer may attach + * callbacks to be invoked (asynchronously, on a specified thread) when the + * request is either completed (resolved) or cannot be completed (rejected). + * Whereas JS promise callbacks are dispatched from Microtask checkpoints, + * MozPromises resolution/rejection make a normal round-trip through the event + * loop, which simplifies their ordering semantics relative to other native + * code. + * + * MozPromises attempt to mirror the spirit of JS Promises to the extent that + * is possible (and desirable) in C++. While the intent is that MozPromises + * feel familiar to programmers who are accustomed to their JS-implemented + * cousin, we don't shy away from imposing restrictions and adding features that + * make sense for the use cases we encounter. + * + * A MozPromise is ThreadSafe, and may be ->Then()ed on any thread. The Then() + * call accepts resolve and reject callbacks, and returns a magic object which + * will be implicitly converted to a MozPromise::Request or a MozPromise object + * depending on how the return value is used. The magic object serves several + * purposes for the consumer. + * + * (1) When converting to a MozPromise::Request, it allows the caller to + * cancel the delivery of the resolve/reject value if it has not already + * occurred, via Disconnect() (this must be done on the target thread to + * avoid racing). + * + * (2) When converting to a MozPromise (which is called a completion promise), + * it allows promise chaining so ->Then() can be called again to attach + * more resolve and reject callbacks. If the resolve/reject callback + * returns a new MozPromise, that promise is chained to the completion + * promise, such that its resolve/reject value will be forwarded along + * when it arrives. If the resolve/reject callback returns void, the + * completion promise is resolved/rejected with the same value that was + * passed to the callback. + * + * The MozPromise APIs skirt traditional XPCOM convention by returning nsRefPtrs + * (rather than already_AddRefed) from various methods. This is done to allow + * elegant chaining of calls without cluttering up the code with intermediate + * variables, and without introducing separate API variants for callers that + * want a return value (from, say, ->Then()) from those that don't. + * + * When IsExclusive is true, the MozPromise does a release-mode assertion that + * there is at most one call to either Then(...) or ChainTo(...). + */ + +class MozPromiseRefcountable { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MozPromiseRefcountable) + protected: + virtual ~MozPromiseRefcountable() = default; +}; + +class MozPromiseBase : public MozPromiseRefcountable { + public: + virtual void AssertIsDead() = 0; +}; + +template +class MozPromiseHolder; +template +class MozPromiseRequestHolder; +template +class MozPromise : public MozPromiseBase { + static const uint32_t sMagic = 0xcecace11; + + // Return a |T&&| to enable move when IsExclusive is true or + // a |const T&| to enforce copy otherwise. + template > + static R MaybeMove(T& aX) { + return static_cast(aX); + } + + public: + typedef ResolveValueT ResolveValueType; + typedef RejectValueT RejectValueType; + class ResolveOrRejectValue { + public: + template + void SetResolve(ResolveValueType_&& aResolveValue) { + MOZ_ASSERT(IsNothing()); + mValue = Storage(VariantIndex{}, + std::forward(aResolveValue)); + } + + template + void SetReject(RejectValueType_&& aRejectValue) { + MOZ_ASSERT(IsNothing()); + mValue = Storage(VariantIndex{}, + std::forward(aRejectValue)); + } + + template + static ResolveOrRejectValue MakeResolve(ResolveValueType_&& aResolveValue) { + ResolveOrRejectValue val; + val.SetResolve(std::forward(aResolveValue)); + return val; + } + + template + static ResolveOrRejectValue MakeReject(RejectValueType_&& aRejectValue) { + ResolveOrRejectValue val; + val.SetReject(std::forward(aRejectValue)); + return val; + } + + bool IsResolve() const { return mValue.template is(); } + bool IsReject() const { return mValue.template is(); } + bool IsNothing() const { return mValue.template is(); } + + const ResolveValueType& ResolveValue() const { + return mValue.template as(); + } + ResolveValueType& ResolveValue() { + return mValue.template as(); + } + const RejectValueType& RejectValue() const { + return mValue.template as(); + } + RejectValueType& RejectValue() { return mValue.template as(); } + + private: + enum { NothingIndex, ResolveIndex, RejectIndex }; + using Storage = Variant; + Storage mValue = Storage(VariantIndex{}); + }; + + protected: + // MozPromise is the public type, and never constructed directly. Construct + // a MozPromise::Private, defined below. + MozPromise(const char* aCreationSite, bool aIsCompletionPromise) + : mCreationSite(aCreationSite), + mMutex("MozPromise Mutex"), + mHaveRequest(false), + mIsCompletionPromise(aIsCompletionPromise) +# ifdef PROMISE_DEBUG + , + mMagic4(&mMutex) +# endif + { + PROMISE_LOG("%s creating MozPromise (%p)", mCreationSite, this); + } + + public: + // MozPromise::Private allows us to separate the public interface (upon which + // consumers of the promise may invoke methods like Then()) from the private + // interface (upon which the creator of the promise may invoke Resolve() or + // Reject()). APIs should create and store a MozPromise::Private (usually + // via a MozPromiseHolder), and return a MozPromise to consumers. + // + // NB: We can include the definition of this class inline once B2G ICS is + // gone. + class Private; + + template + [[nodiscard]] static RefPtr CreateAndResolve( + ResolveValueType_&& aResolveValue, const char* aResolveSite) { + static_assert(std::is_convertible_v, + "Resolve() argument must be implicitly convertible to " + "MozPromise's ResolveValueT"); + RefPtr p = + new MozPromise::Private(aResolveSite); + p->Resolve(std::forward(aResolveValue), aResolveSite); + return p; + } + + template + [[nodiscard]] static RefPtr CreateAndReject( + RejectValueType_&& aRejectValue, const char* aRejectSite) { + static_assert(std::is_convertible_v, + "Reject() argument must be implicitly convertible to " + "MozPromise's RejectValueT"); + RefPtr p = + new MozPromise::Private(aRejectSite); + p->Reject(std::forward(aRejectValue), aRejectSite); + return p; + } + + template + [[nodiscard]] static RefPtr CreateAndResolveOrReject( + ResolveOrRejectValueType_&& aValue, const char* aSite) { + RefPtr p = new MozPromise::Private(aSite); + p->ResolveOrReject(std::forward(aValue), aSite); + return p; + } + + typedef MozPromise, RejectValueType, + IsExclusive> + AllPromiseType; + + typedef MozPromise, bool, IsExclusive> + AllSettledPromiseType; + + private: + class AllPromiseHolder : public MozPromiseRefcountable { + public: + explicit AllPromiseHolder(size_t aDependentPromises) + : mPromise(new typename AllPromiseType::Private(__func__)), + mOutstandingPromises(aDependentPromises) { + MOZ_ASSERT(aDependentPromises > 0); + mResolveValues.SetLength(aDependentPromises); + } + + template + void Resolve(size_t aIndex, ResolveValueType_&& aResolveValue) { + if (!mPromise) { + // Already rejected. + return; + } + + mResolveValues[aIndex].emplace( + std::forward(aResolveValue)); + if (--mOutstandingPromises == 0) { + nsTArray resolveValues; + resolveValues.SetCapacity(mResolveValues.Length()); + for (auto&& resolveValue : mResolveValues) { + resolveValues.AppendElement(std::move(resolveValue.ref())); + } + + mPromise->Resolve(std::move(resolveValues), __func__); + mPromise = nullptr; + mResolveValues.Clear(); + } + } + + template + void Reject(RejectValueType_&& aRejectValue) { + if (!mPromise) { + // Already rejected. + return; + } + + mPromise->Reject(std::forward(aRejectValue), __func__); + mPromise = nullptr; + mResolveValues.Clear(); + } + + AllPromiseType* Promise() { return mPromise; } + + private: + nsTArray> mResolveValues; + RefPtr mPromise; + size_t mOutstandingPromises; + }; + + // Trying to pass ResolveOrRejectValue by value fails static analysis checks, + // so we need to use either a const& or an rvalue reference, depending on + // whether IsExclusive is true or not. + typedef std::conditional_t + ResolveOrRejectValueParam; + + typedef std::conditional_t + ResolveValueTypeParam; + + typedef std::conditional_t + RejectValueTypeParam; + + class AllSettledPromiseHolder : public MozPromiseRefcountable { + public: + explicit AllSettledPromiseHolder(size_t aDependentPromises) + : mPromise(new typename AllSettledPromiseType::Private(__func__)), + mOutstandingPromises(aDependentPromises) { + MOZ_ASSERT(aDependentPromises > 0); + mValues.SetLength(aDependentPromises); + } + + void Settle(size_t aIndex, ResolveOrRejectValueParam aValue) { + if (!mPromise) { + // Already rejected. + return; + } + + mValues[aIndex].emplace(MaybeMove(aValue)); + if (--mOutstandingPromises == 0) { + nsTArray values; + values.SetCapacity(mValues.Length()); + for (auto&& value : mValues) { + values.AppendElement(std::move(value.ref())); + } + + mPromise->Resolve(std::move(values), __func__); + mPromise = nullptr; + mValues.Clear(); + } + } + + AllSettledPromiseType* Promise() { return mPromise; } + + private: + nsTArray> mValues; + RefPtr mPromise; + size_t mOutstandingPromises; + }; + + public: + [[nodiscard]] static RefPtr All( + nsISerialEventTarget* aProcessingTarget, + nsTArray>& aPromises) { + if (aPromises.Length() == 0) { + return AllPromiseType::CreateAndResolve( + CopyableTArray(), __func__); + } + + RefPtr holder = new AllPromiseHolder(aPromises.Length()); + RefPtr promise = holder->Promise(); + for (size_t i = 0; i < aPromises.Length(); ++i) { + aPromises[i]->Then( + aProcessingTarget, __func__, + [holder, i](ResolveValueTypeParam aResolveValue) -> void { + holder->Resolve(i, MaybeMove(aResolveValue)); + }, + [holder](RejectValueTypeParam aRejectValue) -> void { + holder->Reject(MaybeMove(aRejectValue)); + }); + } + return promise; + } + + [[nodiscard]] static RefPtr AllSettled( + nsISerialEventTarget* aProcessingTarget, + nsTArray>& aPromises) { + if (aPromises.Length() == 0) { + return AllSettledPromiseType::CreateAndResolve( + CopyableTArray(), __func__); + } + + RefPtr holder = + new AllSettledPromiseHolder(aPromises.Length()); + RefPtr promise = holder->Promise(); + for (size_t i = 0; i < aPromises.Length(); ++i) { + aPromises[i]->Then(aProcessingTarget, __func__, + [holder, i](ResolveOrRejectValueParam aValue) -> void { + holder->Settle(i, MaybeMove(aValue)); + }); + } + return promise; + } + + class Request : public MozPromiseRefcountable { + public: + virtual void Disconnect() = 0; + + protected: + Request() : mComplete(false), mDisconnected(false) {} + virtual ~Request() = default; + + bool mComplete; + bool mDisconnected; + }; + + protected: + /* + * A ThenValue tracks a single consumer waiting on the promise. When a + * consumer invokes promise->Then(...), a ThenValue is created. Once the + * Promise is resolved or rejected, a {Resolve,Reject}Runnable is dispatched, + * which invokes the resolve/reject method and then deletes the ThenValue. + */ + class ThenValueBase : public Request { + friend class MozPromise; + static const uint32_t sMagic = 0xfadece11; + + public: + class ResolveOrRejectRunnable final + : public PrioritizableCancelableRunnable { + public: + ResolveOrRejectRunnable(ThenValueBase* aThenValue, MozPromise* aPromise) + : PrioritizableCancelableRunnable( + aPromise->mPriority, + "MozPromise::ThenValueBase::ResolveOrRejectRunnable"), + mThenValue(aThenValue), + mPromise(aPromise) { + MOZ_DIAGNOSTIC_ASSERT(!mPromise->IsPending()); + } + + ~ResolveOrRejectRunnable() { + if (mThenValue) { + mThenValue->AssertIsDead(); + } + } + + NS_IMETHOD Run() override { + PROMISE_LOG("ResolveOrRejectRunnable::Run() [this=%p]", this); + mThenValue->DoResolveOrReject(mPromise->Value()); + mThenValue = nullptr; + mPromise = nullptr; + return NS_OK; + } + + nsresult Cancel() override { return Run(); } + + private: + RefPtr mThenValue; + RefPtr mPromise; + }; + + ThenValueBase(nsISerialEventTarget* aResponseTarget, const char* aCallSite) + : mResponseTarget(aResponseTarget), mCallSite(aCallSite) { + MOZ_ASSERT(aResponseTarget); + } + +# ifdef PROMISE_DEBUG + ~ThenValueBase() { + mMagic1 = 0; + mMagic2 = 0; + } +# endif + + void AssertIsDead() { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic); + // We want to assert that this ThenValues is dead - that is to say, that + // there are no consumers waiting for the result. In the case of a normal + // ThenValue, we check that it has been disconnected, which is the way + // that the consumer signals that it no longer wishes to hear about the + // result. If this ThenValue has a completion promise (which is mutually + // exclusive with being disconnectable), we recursively assert that every + // ThenValue associated with the completion promise is dead. + if (MozPromiseBase* p = CompletionPromise()) { + p->AssertIsDead(); + } else { +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + if (MOZ_UNLIKELY(!Request::mDisconnected)) { + MOZ_CRASH_UNSAFE_PRINTF( + "MozPromise::ThenValue created from '%s' destroyed without being " + "either disconnected, resolved, or rejected (dispatchRv: %s)", + mCallSite, + mDispatchRv ? GetStaticErrorName(*mDispatchRv) + : "not dispatched"); + } +# endif + } + } + + void Dispatch(MozPromise* aPromise) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic); + aPromise->mMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(!aPromise->IsPending()); + + nsCOMPtr r = new ResolveOrRejectRunnable(this, aPromise); + PROMISE_LOG( + "%s Then() call made from %s [Runnable=%p, Promise=%p, ThenValue=%p] " + "%s dispatch", + aPromise->mValue.IsResolve() ? "Resolving" : "Rejecting", mCallSite, + r.get(), aPromise, this, + aPromise->mUseSynchronousTaskDispatch ? "synchronous" + : aPromise->mUseDirectTaskDispatch ? "directtask" + : "normal"); + + if (aPromise->mUseSynchronousTaskDispatch && + mResponseTarget->IsOnCurrentThread()) { + PROMISE_LOG("ThenValue::Dispatch running task synchronously [this=%p]", + this); + r->Run(); + return; + } + + if (aPromise->mUseDirectTaskDispatch && + mResponseTarget->IsOnCurrentThread()) { + PROMISE_LOG( + "ThenValue::Dispatch dispatch task via direct task queue [this=%p]", + this); + nsCOMPtr dispatcher = + do_QueryInterface(mResponseTarget); + if (dispatcher) { + SetDispatchRv(dispatcher->DispatchDirectTask(r.forget())); + return; + } + NS_WARNING( + nsPrintfCString( + "Direct Task dispatching not available for thread \"%s\"", + PR_GetThreadName(PR_GetCurrentThread())) + .get()); + MOZ_DIAGNOSTIC_ASSERT( + false, + "mResponseTarget must implement nsIDirectTaskDispatcher for direct " + "task dispatching"); + } + + // Promise consumers are allowed to disconnect the Request object and + // then shut down the thread or task queue that the promise result would + // be dispatched on. So we unfortunately can't assert that promise + // dispatch succeeds. :-( + // We do record whether or not it succeeds so that if the ThenValueBase is + // then destroyed and it was not disconnected, we can include that + // information in the assertion message. + SetDispatchRv(mResponseTarget->Dispatch(r.forget())); + } + + void Disconnect() override { + MOZ_DIAGNOSTIC_ASSERT(mResponseTarget->IsOnCurrentThread()); + MOZ_DIAGNOSTIC_ASSERT(!Request::mComplete); + Request::mDisconnected = true; + + // We could support rejecting the completion promise on disconnection, but + // then we'd need to have some sort of default reject value. The use cases + // of disconnection and completion promise chaining seem pretty + // orthogonal, so let's use assert against it. + MOZ_DIAGNOSTIC_ASSERT(!CompletionPromise()); + } + + protected: + virtual MozPromiseBase* CompletionPromise() const = 0; + virtual void DoResolveOrRejectInternal(ResolveOrRejectValue& aValue) = 0; + + void DoResolveOrReject(ResolveOrRejectValue& aValue) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic); + MOZ_DIAGNOSTIC_ASSERT(mResponseTarget->IsOnCurrentThread()); + Request::mComplete = true; + if (Request::mDisconnected) { + PROMISE_LOG( + "ThenValue::DoResolveOrReject disconnected - bailing out [this=%p]", + this); + return; + } + + // Invoke the resolve or reject method. + DoResolveOrRejectInternal(aValue); + } + + void SetDispatchRv(nsresult aRv) { +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + mDispatchRv = Some(aRv); +# endif + } + + nsCOMPtr + mResponseTarget; // May be released on any thread. +# ifdef PROMISE_DEBUG + uint32_t mMagic1 = sMagic; +# endif + const char* mCallSite; +# ifdef PROMISE_DEBUG + uint32_t mMagic2 = sMagic; +# endif +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + Maybe mDispatchRv; +# endif + }; + + /* + * We create two overloads for invoking Resolve/Reject Methods so as to + * make the resolve/reject value argument "optional". + */ + template + static std::enable_if_t::value, + typename detail::MethodTrait::ReturnType> + InvokeMethod(ThisType* aThisVal, MethodType aMethod, ValueType&& aValue) { + return (aThisVal->*aMethod)(std::forward(aValue)); + } + + template + static std::enable_if_t::value, + typename detail::MethodTrait::ReturnType> + InvokeMethod(ThisType* aThisVal, MethodType aMethod, ValueType&& aValue) { + return (aThisVal->*aMethod)(); + } + + // Called when promise chaining is supported. + template + static std::enable_if_t InvokeCallbackMethod( + ThisType* aThisVal, MethodType aMethod, ValueType&& aValue, + CompletionPromiseType&& aCompletionPromise) { + auto p = InvokeMethod(aThisVal, aMethod, std::forward(aValue)); + if (aCompletionPromise) { + p->ChainTo(aCompletionPromise.forget(), ""); + } + } + + // Called when promise chaining is not supported. + template + static std::enable_if_t InvokeCallbackMethod( + ThisType* aThisVal, MethodType aMethod, ValueType&& aValue, + CompletionPromiseType&& aCompletionPromise) { + MOZ_DIAGNOSTIC_ASSERT( + !aCompletionPromise, + "Can't do promise chaining for a non-promise-returning method."); + InvokeMethod(aThisVal, aMethod, std::forward(aValue)); + } + + template + class ThenCommand; + + template + class ThenValue; + + template + class ThenValue + : public ThenValueBase { + friend class ThenCommand; + + using R1 = typename RemoveSmartPointer< + typename detail::MethodTrait::ReturnType>::Type; + using R2 = typename RemoveSmartPointer< + typename detail::MethodTrait::ReturnType>::Type; + using SupportChaining = + std::integral_constant::value && + std::is_same_v>; + + // Fall back to MozPromise when promise chaining is not supported to make + // code compile. + using PromiseType = + std::conditional_t; + + public: + ThenValue(nsISerialEventTarget* aResponseTarget, ThisType* aThisVal, + ResolveMethodType aResolveMethod, RejectMethodType aRejectMethod, + const char* aCallSite) + : ThenValueBase(aResponseTarget, aCallSite), + mThisVal(aThisVal), + mResolveMethod(aResolveMethod), + mRejectMethod(aRejectMethod) {} + + void Disconnect() override { + ThenValueBase::Disconnect(); + + // If a Request has been disconnected, we don't guarantee that the + // resolve/reject runnable will be dispatched. Null out our refcounted + // this-value now so that it's released predictably on the dispatch + // thread. + mThisVal = nullptr; + } + + protected: + MozPromiseBase* CompletionPromise() const override { + return mCompletionPromise; + } + + void DoResolveOrRejectInternal(ResolveOrRejectValue& aValue) override { + if (aValue.IsResolve()) { + InvokeCallbackMethod( + mThisVal.get(), mResolveMethod, MaybeMove(aValue.ResolveValue()), + std::move(mCompletionPromise)); + } else { + InvokeCallbackMethod( + mThisVal.get(), mRejectMethod, MaybeMove(aValue.RejectValue()), + std::move(mCompletionPromise)); + } + + // Null out mThisVal after invoking the callback so that any references + // are released predictably on the dispatch thread. Otherwise, it would be + // released on whatever thread last drops its reference to the ThenValue, + // which may or may not be ok. + mThisVal = nullptr; + } + + private: + RefPtr + mThisVal; // Only accessed and refcounted on dispatch thread. + ResolveMethodType mResolveMethod; + RejectMethodType mRejectMethod; + RefPtr mCompletionPromise; + }; + + template + class ThenValue : public ThenValueBase { + friend class ThenCommand; + + using R1 = typename RemoveSmartPointer::ReturnType>::Type; + using SupportChaining = + std::integral_constant::value>; + + // Fall back to MozPromise when promise chaining is not supported to make + // code compile. + using PromiseType = + std::conditional_t; + + public: + ThenValue(nsISerialEventTarget* aResponseTarget, ThisType* aThisVal, + ResolveRejectMethodType aResolveRejectMethod, + const char* aCallSite) + : ThenValueBase(aResponseTarget, aCallSite), + mThisVal(aThisVal), + mResolveRejectMethod(aResolveRejectMethod) {} + + void Disconnect() override { + ThenValueBase::Disconnect(); + + // If a Request has been disconnected, we don't guarantee that the + // resolve/reject runnable will be dispatched. Null out our refcounted + // this-value now so that it's released predictably on the dispatch + // thread. + mThisVal = nullptr; + } + + protected: + MozPromiseBase* CompletionPromise() const override { + return mCompletionPromise; + } + + void DoResolveOrRejectInternal(ResolveOrRejectValue& aValue) override { + InvokeCallbackMethod( + mThisVal.get(), mResolveRejectMethod, MaybeMove(aValue), + std::move(mCompletionPromise)); + + // Null out mThisVal after invoking the callback so that any references + // are released predictably on the dispatch thread. Otherwise, it would be + // released on whatever thread last drops its reference to the ThenValue, + // which may or may not be ok. + mThisVal = nullptr; + } + + private: + RefPtr + mThisVal; // Only accessed and refcounted on dispatch thread. + ResolveRejectMethodType mResolveRejectMethod; + RefPtr mCompletionPromise; + }; + + // NB: We could use std::function here instead of a template if it were + // supported. :-( + template + class ThenValue : public ThenValueBase { + friend class ThenCommand; + + using R1 = typename RemoveSmartPointer< + typename detail::MethodTrait::ReturnType>::Type; + using R2 = typename RemoveSmartPointer< + typename detail::MethodTrait::ReturnType>::Type; + using SupportChaining = + std::integral_constant::value && + std::is_same_v>; + + // Fall back to MozPromise when promise chaining is not supported to make + // code compile. + using PromiseType = + std::conditional_t; + + public: + ThenValue(nsISerialEventTarget* aResponseTarget, + ResolveFunction&& aResolveFunction, + RejectFunction&& aRejectFunction, const char* aCallSite) + : ThenValueBase(aResponseTarget, aCallSite) { + mResolveFunction.emplace(std::move(aResolveFunction)); + mRejectFunction.emplace(std::move(aRejectFunction)); + } + + void Disconnect() override { + ThenValueBase::Disconnect(); + + // If a Request has been disconnected, we don't guarantee that the + // resolve/reject runnable will be dispatched. Destroy our callbacks + // now so that any references in closures are released predictable on + // the dispatch thread. + mResolveFunction.reset(); + mRejectFunction.reset(); + } + + protected: + MozPromiseBase* CompletionPromise() const override { + return mCompletionPromise; + } + + void DoResolveOrRejectInternal(ResolveOrRejectValue& aValue) override { + // Note: The usage of InvokeCallbackMethod here requires that + // ResolveFunction/RejectFunction are capture-lambdas (i.e. anonymous + // classes with ::operator()), since it allows us to share code more + // easily. We could fix this if need be, though it's quite easy to work + // around by just capturing something. + if (aValue.IsResolve()) { + InvokeCallbackMethod( + mResolveFunction.ptr(), &ResolveFunction::operator(), + MaybeMove(aValue.ResolveValue()), std::move(mCompletionPromise)); + } else { + InvokeCallbackMethod( + mRejectFunction.ptr(), &RejectFunction::operator(), + MaybeMove(aValue.RejectValue()), std::move(mCompletionPromise)); + } + + // Destroy callbacks after invocation so that any references in closures + // are released predictably on the dispatch thread. Otherwise, they would + // be released on whatever thread last drops its reference to the + // ThenValue, which may or may not be ok. + mResolveFunction.reset(); + mRejectFunction.reset(); + } + + private: + Maybe + mResolveFunction; // Only accessed and deleted on dispatch thread. + Maybe + mRejectFunction; // Only accessed and deleted on dispatch thread. + RefPtr mCompletionPromise; + }; + + template + class ThenValue : public ThenValueBase { + friend class ThenCommand; + + using R1 = typename RemoveSmartPointer< + typename detail::MethodTrait::ReturnType>::Type; + using SupportChaining = + std::integral_constant::value>; + + // Fall back to MozPromise when promise chaining is not supported to make + // code compile. + using PromiseType = + std::conditional_t; + + public: + ThenValue(nsISerialEventTarget* aResponseTarget, + ResolveRejectFunction&& aResolveRejectFunction, + const char* aCallSite) + : ThenValueBase(aResponseTarget, aCallSite) { + mResolveRejectFunction.emplace(std::move(aResolveRejectFunction)); + } + + void Disconnect() override { + ThenValueBase::Disconnect(); + + // If a Request has been disconnected, we don't guarantee that the + // resolve/reject runnable will be dispatched. Destroy our callbacks + // now so that any references in closures are released predictable on + // the dispatch thread. + mResolveRejectFunction.reset(); + } + + protected: + MozPromiseBase* CompletionPromise() const override { + return mCompletionPromise; + } + + void DoResolveOrRejectInternal(ResolveOrRejectValue& aValue) override { + // Note: The usage of InvokeCallbackMethod here requires that + // ResolveRejectFunction is capture-lambdas (i.e. anonymous + // classes with ::operator()), since it allows us to share code more + // easily. We could fix this if need be, though it's quite easy to work + // around by just capturing something. + InvokeCallbackMethod( + mResolveRejectFunction.ptr(), &ResolveRejectFunction::operator(), + MaybeMove(aValue), std::move(mCompletionPromise)); + + // Destroy callbacks after invocation so that any references in closures + // are released predictably on the dispatch thread. Otherwise, they would + // be released on whatever thread last drops its reference to the + // ThenValue, which may or may not be ok. + mResolveRejectFunction.reset(); + } + + private: + Maybe + mResolveRejectFunction; // Only accessed and deleted on dispatch + // thread. + RefPtr mCompletionPromise; + }; + + public: + void ThenInternal(already_AddRefed aThenValue, + const char* aCallSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + RefPtr thenValue = aThenValue; + MutexAutoLock lock(mMutex); + MOZ_DIAGNOSTIC_ASSERT( + !IsExclusive || !mHaveRequest, + "Using an exclusive promise in a non-exclusive fashion"); + mHaveRequest = true; + PROMISE_LOG("%s invoking Then() [this=%p, aThenValue=%p, isPending=%d]", + aCallSite, this, thenValue.get(), (int)IsPending()); + if (!IsPending()) { + thenValue->Dispatch(this); + } else { + mThenValues.AppendElement(thenValue.forget()); + } + } + + protected: + /* + * A command object to store all information needed to make a request to + * the promise. This allows us to delay the request until further use is + * known (whether it is ->Then() again for more promise chaining or ->Track() + * to terminate chaining and issue the request). + * + * This allows a unified syntax for promise chaining and disconnection + * and feels more like its JS counterpart. + */ + template + class ThenCommand { + // Allow Promise1::ThenCommand to access the private constructor, + // Promise2::ThenCommand(ThenCommand&&). + template + friend class MozPromise; + + using PromiseType = typename ThenValueType::PromiseType; + using Private = typename PromiseType::Private; + + ThenCommand(const char* aCallSite, + already_AddRefed aThenValue, + MozPromise* aReceiver) + : mCallSite(aCallSite), mThenValue(aThenValue), mReceiver(aReceiver) {} + + ThenCommand(ThenCommand&& aOther) = default; + + public: + ~ThenCommand() { + // Issue the request now if the return value of Then() is not used. + if (mThenValue) { + mReceiver->ThenInternal(mThenValue.forget(), mCallSite); + } + } + + // Allow RefPtr p = somePromise->Then(); + // p->Then(thread1, ...); + // p->Then(thread2, ...); + operator RefPtr() { + static_assert( + ThenValueType::SupportChaining::value, + "The resolve/reject callback needs to return a RefPtr " + "in order to do promise chaining."); + + // mCompletionPromise must be created before ThenInternal() to avoid race. + RefPtr p = + new Private("", true /* aIsCompletionPromise */); + mThenValue->mCompletionPromise = p; + // Note ThenInternal() might nullify mCompletionPromise before return. + // So we need to return p instead of mCompletionPromise. + mReceiver->ThenInternal(mThenValue.forget(), mCallSite); + return p; + } + + template + auto Then(Ts&&... aArgs) -> decltype(std::declval().Then( + std::forward(aArgs)...)) { + return static_cast>(*this)->Then( + std::forward(aArgs)...); + } + + void Track(MozPromiseRequestHolder& aRequestHolder) { + aRequestHolder.Track(do_AddRef(mThenValue)); + mReceiver->ThenInternal(mThenValue.forget(), mCallSite); + } + + // Allow calling ->Then() again for more promise chaining or ->Track() to + // end chaining and track the request for future disconnection. + ThenCommand* operator->() { return this; } + + private: + const char* mCallSite; + RefPtr mThenValue; + RefPtr mReceiver; + }; + + public: + template , + typename ReturnType = ThenCommand> + ReturnType Then(nsISerialEventTarget* aResponseTarget, const char* aCallSite, + ThisType* aThisVal, Methods... aMethods) { + RefPtr thenValue = + new ThenValueType(aResponseTarget, aThisVal, aMethods..., aCallSite); + return ReturnType(aCallSite, thenValue.forget(), this); + } + + template , + typename ReturnType = ThenCommand> + ReturnType Then(nsISerialEventTarget* aResponseTarget, const char* aCallSite, + Functions&&... aFunctions) { + RefPtr thenValue = + new ThenValueType(aResponseTarget, std::move(aFunctions)..., aCallSite); + return ReturnType(aCallSite, thenValue.forget(), this); + } + + void ChainTo(already_AddRefed aChainedPromise, + const char* aCallSite) { + MutexAutoLock lock(mMutex); + MOZ_DIAGNOSTIC_ASSERT( + !IsExclusive || !mHaveRequest, + "Using an exclusive promise in a non-exclusive fashion"); + mHaveRequest = true; + RefPtr chainedPromise = aChainedPromise; + PROMISE_LOG( + "%s invoking Chain() [this=%p, chainedPromise=%p, isPending=%d]", + aCallSite, this, chainedPromise.get(), (int)IsPending()); + + // We want to use the same type of dispatching method with the chained + // promises. + + // We need to ensure that the UseSynchronousTaskDispatch branch isn't taken + // at compilation time to ensure we're not triggering the static_assert in + // UseSynchronousTaskDispatch method. if constexpr (IsExclusive) ensures + // that. + if (mUseDirectTaskDispatch) { + chainedPromise->UseDirectTaskDispatch(aCallSite); + } else if constexpr (IsExclusive) { + if (mUseSynchronousTaskDispatch) { + chainedPromise->UseSynchronousTaskDispatch(aCallSite); + } + } else { + chainedPromise->SetTaskPriority(mPriority, aCallSite); + } + + if (!IsPending()) { + ForwardTo(chainedPromise); + } else { + mChainedPromises.AppendElement(chainedPromise); + } + } + +# ifdef MOZ_WIDGET_ANDROID + // Creates a C++ MozPromise from its Java counterpart, GeckoResult. + [[nodiscard]] static RefPtr FromGeckoResult( + java::GeckoResult::Param aGeckoResult) { + using jni::GeckoResultCallback; + RefPtr p = new Private("GeckoResult Glue", false); + auto resolve = GeckoResultCallback::CreateAndAttach( + [p](ResolveValueType&& aArg) { + p->Resolve(MaybeMove(aArg), __func__); + }); + auto reject = GeckoResultCallback::CreateAndAttach( + [p](RejectValueType&& aArg) { p->Reject(MaybeMove(aArg), __func__); }); + aGeckoResult->NativeThen(resolve, reject); + return p; + } +# endif + + // Note we expose the function AssertIsDead() instead of IsDead() since + // checking IsDead() is a data race in the situation where the request is not + // dead. Therefore we enforce the form |Assert(IsDead())| by exposing + // AssertIsDead() only. + void AssertIsDead() override { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + for (auto&& then : mThenValues) { + then->AssertIsDead(); + } + for (auto&& chained : mChainedPromises) { + chained->AssertIsDead(); + } + } + + bool IsResolved() const { return mValue.IsResolve(); } + + protected: + bool IsPending() const { return mValue.IsNothing(); } + + ResolveOrRejectValue& Value() { + // This method should only be called once the value has stabilized. As + // such, we don't need to acquire the lock here. + MOZ_DIAGNOSTIC_ASSERT(!IsPending()); + return mValue; + } + + void DispatchAll() { + mMutex.AssertCurrentThreadOwns(); + for (auto&& thenValue : mThenValues) { + thenValue->Dispatch(this); + } + mThenValues.Clear(); + + for (auto&& chainedPromise : mChainedPromises) { + ForwardTo(chainedPromise); + } + mChainedPromises.Clear(); + } + + void ForwardTo(Private* aOther) { + MOZ_ASSERT(!IsPending()); + if (mValue.IsResolve()) { + aOther->Resolve(MaybeMove(mValue.ResolveValue()), ""); + } else { + aOther->Reject(MaybeMove(mValue.RejectValue()), ""); + } + } + + virtual ~MozPromise() { + PROMISE_LOG("MozPromise::~MozPromise [this=%p]", this); + AssertIsDead(); + // We can't guarantee a completion promise will always be revolved or + // rejected since ResolveOrRejectRunnable might not run when dispatch fails. + if (!mIsCompletionPromise) { + MOZ_ASSERT(!IsPending()); + MOZ_ASSERT(mThenValues.IsEmpty()); + MOZ_ASSERT(mChainedPromises.IsEmpty()); + } +# ifdef PROMISE_DEBUG + mMagic1 = 0; + mMagic2 = 0; + mMagic3 = 0; + mMagic4 = nullptr; +# endif + }; + + const char* mCreationSite; // For logging + Mutex mMutex MOZ_UNANNOTATED; + ResolveOrRejectValue mValue; + bool mUseSynchronousTaskDispatch = false; + bool mUseDirectTaskDispatch = false; + uint32_t mPriority = nsIRunnablePriority::PRIORITY_NORMAL; +# ifdef PROMISE_DEBUG + uint32_t mMagic1 = sMagic; +# endif + // Try shows we never have more than 3 elements when IsExclusive is false. + // So '3' is a good value to avoid heap allocation in most cases. + AutoTArray, IsExclusive ? 1 : 3> mThenValues; +# ifdef PROMISE_DEBUG + uint32_t mMagic2 = sMagic; +# endif + nsTArray> mChainedPromises; +# ifdef PROMISE_DEBUG + uint32_t mMagic3 = sMagic; +# endif + bool mHaveRequest; + const bool mIsCompletionPromise; +# ifdef PROMISE_DEBUG + void* mMagic4; +# endif +}; + +template +class MozPromise::Private + : public MozPromise { + public: + explicit Private(const char* aCreationSite, bool aIsCompletionPromise = false) + : MozPromise(aCreationSite, aIsCompletionPromise) {} + + template + void Resolve(ResolveValueT_&& aResolveValue, const char* aResolveSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s resolving MozPromise (%p created at %s)", aResolveSite, + this, mCreationSite); + if (!IsPending()) { + PROMISE_LOG( + "%s ignored already resolved or rejected MozPromise (%p created at " + "%s)", + aResolveSite, this, mCreationSite); + return; + } + mValue.SetResolve(std::forward(aResolveValue)); + DispatchAll(); + } + + template + void Reject(RejectValueT_&& aRejectValue, const char* aRejectSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s rejecting MozPromise (%p created at %s)", aRejectSite, this, + mCreationSite); + if (!IsPending()) { + PROMISE_LOG( + "%s ignored already resolved or rejected MozPromise (%p created at " + "%s)", + aRejectSite, this, mCreationSite); + return; + } + mValue.SetReject(std::forward(aRejectValue)); + DispatchAll(); + } + + template + void ResolveOrReject(ResolveOrRejectValue_&& aValue, const char* aSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s resolveOrRejecting MozPromise (%p created at %s)", aSite, + this, mCreationSite); + if (!IsPending()) { + PROMISE_LOG( + "%s ignored already resolved or rejected MozPromise (%p created at " + "%s)", + aSite, this, mCreationSite); + return; + } + mValue = std::forward(aValue); + DispatchAll(); + } + + // If the caller and target are both on the same thread, run the the resolve + // or reject callback synchronously. Otherwise, the task will be dispatched + // via the target Dispatch method. + void UseSynchronousTaskDispatch(const char* aSite) { + static_assert( + IsExclusive, + "Synchronous dispatch can only be used with exclusive promises"); + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s UseSynchronousTaskDispatch MozPromise (%p created at %s)", + aSite, this, mCreationSite); + MOZ_ASSERT(IsPending(), + "A Promise must not have been already resolved or rejected to " + "set dispatch state"); + mUseSynchronousTaskDispatch = true; + } + + // If the caller and target are both on the same thread, run the + // resolve/reject callback off the direct task queue instead. This avoids a + // full trip to the back of the event queue for each additional asynchronous + // step when using MozPromise, and is similar (but not identical to) the + // microtask semantics of JS promises. + void UseDirectTaskDispatch(const char* aSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s UseDirectTaskDispatch MozPromise (%p created at %s)", aSite, + this, mCreationSite); + MOZ_ASSERT(IsPending(), + "A Promise must not have been already resolved or rejected to " + "set dispatch state"); + MOZ_ASSERT(!mUseSynchronousTaskDispatch, + "Promise already set for synchronous dispatch"); + mUseDirectTaskDispatch = true; + } + + // If the resolve/reject will be handled on a thread supporting priorities, + // one may want to tweak the priority of the task by passing a + // nsIRunnablePriority::PRIORITY_* to SetTaskPriority. + void SetTaskPriority(uint32_t aPriority, const char* aSite) { + PROMISE_ASSERT(mMagic1 == sMagic && mMagic2 == sMagic && + mMagic3 == sMagic && mMagic4 == &mMutex); + MutexAutoLock lock(mMutex); + PROMISE_LOG("%s TaskPriority MozPromise (%p created at %s)", aSite, this, + mCreationSite); + MOZ_ASSERT(IsPending(), + "A Promise must not have been already resolved or rejected to " + "set dispatch state"); + MOZ_ASSERT(!mUseSynchronousTaskDispatch, + "Promise already set for synchronous dispatch"); + MOZ_ASSERT(!mUseDirectTaskDispatch, + "Promise already set for direct dispatch"); + mPriority = aPriority; + } +}; + +// A generic promise type that does the trick for simple use cases. +typedef MozPromise GenericPromise; + +// A generic, non-exclusive promise type that does the trick for simple use +// cases. +typedef MozPromise + GenericNonExclusivePromise; + +/* + * Class to encapsulate a promise for a particular role. Use this as the member + * variable for a class whose method returns a promise. + */ +template +class MozPromiseHolderBase { + public: + MozPromiseHolderBase() = default; + + MozPromiseHolderBase(MozPromiseHolderBase&& aOther) = default; + MozPromiseHolderBase& operator=(MozPromiseHolderBase&& aOther) = default; + + ~MozPromiseHolderBase() { MOZ_ASSERT(!mPromise); } + + already_AddRefed Ensure(const char* aMethodName) { + static_cast(this)->Check(); + if (!mPromise) { + mPromise = new (typename PromiseType::Private)(aMethodName); + } + RefPtr p = mPromise.get(); + return p.forget(); + } + + bool IsEmpty() const { + static_cast(this)->Check(); + return !mPromise; + } + + already_AddRefed Steal() { + static_cast(this)->Check(); + return mPromise.forget(); + } + + template + void Resolve(ResolveValueType_&& aResolveValue, const char* aMethodName) { + static_assert(std::is_convertible_v, + "Resolve() argument must be implicitly convertible to " + "MozPromise's ResolveValueT"); + + static_cast(this)->Check(); + MOZ_ASSERT(mPromise); + mPromise->Resolve(std::forward(aResolveValue), + aMethodName); + mPromise = nullptr; + } + + template + void ResolveIfExists(ResolveValueType_&& aResolveValue, + const char* aMethodName) { + if (!IsEmpty()) { + Resolve(std::forward(aResolveValue), aMethodName); + } + } + + template + void Reject(RejectValueType_&& aRejectValue, const char* aMethodName) { + static_assert(std::is_convertible_v, + "Reject() argument must be implicitly convertible to " + "MozPromise's RejectValueT"); + + static_cast(this)->Check(); + MOZ_ASSERT(mPromise); + mPromise->Reject(std::forward(aRejectValue), aMethodName); + mPromise = nullptr; + } + + template + void RejectIfExists(RejectValueType_&& aRejectValue, + const char* aMethodName) { + if (!IsEmpty()) { + Reject(std::forward(aRejectValue), aMethodName); + } + } + + template + void ResolveOrReject(ResolveOrRejectValueType_&& aValue, + const char* aMethodName) { + static_cast(this)->Check(); + MOZ_ASSERT(mPromise); + mPromise->ResolveOrReject(std::forward(aValue), + aMethodName); + mPromise = nullptr; + } + + template + void ResolveOrRejectIfExists(ResolveOrRejectValueType_&& aValue, + const char* aMethodName) { + if (!IsEmpty()) { + ResolveOrReject(std::forward(aValue), + aMethodName); + } + } + + void UseSynchronousTaskDispatch(const char* aSite) { + MOZ_ASSERT(mPromise); + mPromise->UseSynchronousTaskDispatch(aSite); + } + + void UseDirectTaskDispatch(const char* aSite) { + MOZ_ASSERT(mPromise); + mPromise->UseDirectTaskDispatch(aSite); + } + + void SetTaskPriority(uint32_t aPriority, const char* aSite) { + MOZ_ASSERT(mPromise); + mPromise->SetTaskPriority(aPriority, aSite); + } + + private: + RefPtr mPromise; +}; + +template +class MozPromiseHolder + : public MozPromiseHolderBase> { + public: + using MozPromiseHolderBase< + PromiseType, MozPromiseHolder>::MozPromiseHolderBase; + static constexpr void Check(){}; +}; + +template +class MozMonitoredPromiseHolder + : public MozPromiseHolderBase> { + public: + // Provide a Monitor that should always be held when accessing this instance. + explicit MozMonitoredPromiseHolder(Monitor* const aMonitor) + : mMonitor(aMonitor) { + MOZ_ASSERT(aMonitor); + } + + MozMonitoredPromiseHolder(MozMonitoredPromiseHolder&& aOther) = delete; + MozMonitoredPromiseHolder& operator=(MozMonitoredPromiseHolder&& aOther) = + delete; + + void Check() const { mMonitor->AssertCurrentThreadOwns(); } + + private: + Monitor* const mMonitor; +}; + +/* + * Class to encapsulate a MozPromise::Request reference. Use this as the member + * variable for a class waiting on a MozPromise. + */ +template +class MozPromiseRequestHolder { + public: + MozPromiseRequestHolder() = default; + ~MozPromiseRequestHolder() { MOZ_ASSERT(!mRequest); } + + void Track(already_AddRefed aRequest) { + MOZ_DIAGNOSTIC_ASSERT(!Exists()); + mRequest = aRequest; + } + + void Complete() { + MOZ_DIAGNOSTIC_ASSERT(Exists()); + mRequest = nullptr; + } + + // Disconnects and forgets an outstanding promise. The resolve/reject methods + // will never be called. + void Disconnect() { + MOZ_ASSERT(Exists()); + mRequest->Disconnect(); + mRequest = nullptr; + } + + void DisconnectIfExists() { + if (Exists()) { + Disconnect(); + } + } + + bool Exists() const { return !!mRequest; } + + private: + RefPtr mRequest; +}; + +// Asynchronous Potentially-Cross-Thread Method Calls. +// +// This machinery allows callers to schedule a promise-returning function +// (a method and object, or a function object like a lambda) to be invoked +// asynchronously on a given thread, while at the same time receiving a +// promise upon which to invoke Then() immediately. InvokeAsync dispatches a +// task to invoke the function on the proper thread and also chain the +// resulting promise to the one that the caller received, so that resolve/ +// reject values are forwarded through. + +namespace detail { + +// Non-templated base class to allow us to use MOZ_COUNT_{C,D}TOR, which cause +// assertions when used on templated types. +class MethodCallBase { + public: + MOZ_COUNTED_DEFAULT_CTOR(MethodCallBase) + MOZ_COUNTED_DTOR_VIRTUAL(MethodCallBase) +}; + +template +class MethodCall : public MethodCallBase { + public: + template + MethodCall(MethodType aMethod, ThisType* aThisVal, Args&&... aArgs) + : mMethod(aMethod), + mThisVal(aThisVal), + mArgs(std::forward(aArgs)...) { + static_assert(sizeof...(Storages) == sizeof...(Args), + "Storages and Args should have equal sizes"); + } + + RefPtr Invoke() { return mArgs.apply(mThisVal.get(), mMethod); } + + private: + MethodType mMethod; + RefPtr mThisVal; + RunnableMethodArguments mArgs; +}; + +template +class ProxyRunnable : public CancelableRunnable { + public: + ProxyRunnable( + typename PromiseType::Private* aProxyPromise, + MethodCall* aMethodCall) + : CancelableRunnable("detail::ProxyRunnable"), + mProxyPromise(aProxyPromise), + mMethodCall(aMethodCall) {} + + NS_IMETHOD Run() override { + RefPtr p = mMethodCall->Invoke(); + mMethodCall = nullptr; + p->ChainTo(mProxyPromise.forget(), ""); + return NS_OK; + } + + nsresult Cancel() override { return Run(); } + + private: + RefPtr mProxyPromise; + UniquePtr> + mMethodCall; +}; + +template +static RefPtr InvokeAsyncImpl( + nsISerialEventTarget* aTarget, ThisType* aThisVal, const char* aCallerName, + RefPtr (ThisType::*aMethod)(ArgTypes...), + ActualArgTypes&&... aArgs) { + MOZ_ASSERT(aTarget); + + typedef RefPtr (ThisType::*MethodType)(ArgTypes...); + typedef detail::MethodCall + MethodCallType; + typedef detail::ProxyRunnable + ProxyRunnableType; + + MethodCallType* methodCall = new MethodCallType( + aMethod, aThisVal, std::forward(aArgs)...); + RefPtr p = + new (typename PromiseType::Private)(aCallerName); + RefPtr r = new ProxyRunnableType(p, methodCall); + aTarget->Dispatch(r.forget()); + return p; +} + +constexpr bool Any() { return false; } + +template +constexpr bool Any(T1 a) { + return static_cast(a); +} + +template +constexpr bool Any(T1 a, Ts... aOthers) { + return a || Any(aOthers...); +} + +} // namespace detail + +// InvokeAsync with explicitly-specified storages. +// See ParameterStorage in nsThreadUtils.h for help. +template = 0> +static RefPtr InvokeAsync( + nsISerialEventTarget* aTarget, ThisType* aThisVal, const char* aCallerName, + RefPtr (ThisType::*aMethod)(ArgTypes...), + ActualArgTypes&&... aArgs) { + static_assert( + sizeof...(Storages) == sizeof...(ArgTypes), + "Provided Storages and method's ArgTypes should have equal sizes"); + static_assert(sizeof...(Storages) == sizeof...(ActualArgTypes), + "Provided Storages and ActualArgTypes should have equal sizes"); + return detail::InvokeAsyncImpl( + aTarget, aThisVal, aCallerName, aMethod, + std::forward(aArgs)...); +} + +// InvokeAsync with no explicitly-specified storages, will copy arguments and +// then move them out of the runnable into the target method parameters. +template = 0> +static RefPtr InvokeAsync( + nsISerialEventTarget* aTarget, ThisType* aThisVal, const char* aCallerName, + RefPtr (ThisType::*aMethod)(ArgTypes...), + ActualArgTypes&&... aArgs) { + static_assert( + !detail::Any( + std::is_pointer_v>...), + "Cannot pass pointer types through InvokeAsync, Storages must be " + "provided"); + static_assert(sizeof...(ArgTypes) == sizeof...(ActualArgTypes), + "Method's ArgTypes and ActualArgTypes should have equal sizes"); + return detail::InvokeAsyncImpl< + StoreCopyPassByRRef>...>( + aTarget, aThisVal, aCallerName, aMethod, + std::forward(aArgs)...); +} + +namespace detail { + +template +class ProxyFunctionRunnable : public CancelableRunnable { + using FunctionStorage = std::decay_t; + + public: + template + ProxyFunctionRunnable(typename PromiseType::Private* aProxyPromise, + F&& aFunction) + : CancelableRunnable("detail::ProxyFunctionRunnable"), + mProxyPromise(aProxyPromise), + mFunction(new FunctionStorage(std::forward(aFunction))) {} + + NS_IMETHOD Run() override { + RefPtr p = (*mFunction)(); + mFunction = nullptr; + p->ChainTo(mProxyPromise.forget(), ""); + return NS_OK; + } + + nsresult Cancel() override { return Run(); } + + private: + RefPtr mProxyPromise; + UniquePtr mFunction; +}; + +// Note: The following struct and function are not for public consumption (yet?) +// as we would prefer all calls to pass on-the-spot lambdas (or at least moved +// function objects). They could be moved outside of detail if really needed. + +// We prefer getting function objects by non-lvalue-ref (to avoid copying them +// and their captures). This struct is a tag that allows the use of objects +// through lvalue-refs where necessary. +struct AllowInvokeAsyncFunctionLVRef {}; + +// Invoke a function object (e.g., lambda or std/mozilla::function) +// asynchronously; note that the object will be copied if provided by +// lvalue-ref. Return a promise that the function should eventually resolve or +// reject. +template +static auto InvokeAsync(nsISerialEventTarget* aTarget, const char* aCallerName, + AllowInvokeAsyncFunctionLVRef, Function&& aFunction) + -> decltype(aFunction()) { + static_assert( + IsRefcountedSmartPointer::value && + IsMozPromise< + typename RemoveSmartPointer::Type>::value, + "Function object must return RefPtr"); + MOZ_ASSERT(aTarget); + typedef typename RemoveSmartPointer::Type PromiseType; + typedef detail::ProxyFunctionRunnable + ProxyRunnableType; + + auto p = MakeRefPtr(aCallerName); + auto r = MakeRefPtr(p, std::forward(aFunction)); + aTarget->Dispatch(r.forget()); + return p; +} + +} // namespace detail + +// Invoke a function object (e.g., lambda) asynchronously. +// Return a promise that the function should eventually resolve or reject. +template +static auto InvokeAsync(nsISerialEventTarget* aTarget, const char* aCallerName, + Function&& aFunction) -> decltype(aFunction()) { + static_assert(!std::is_lvalue_reference_v, + "Function object must not be passed by lvalue-ref (to avoid " + "unplanned copies); Consider move()ing the object."); + return detail::InvokeAsync(aTarget, aCallerName, + detail::AllowInvokeAsyncFunctionLVRef(), + std::forward(aFunction)); +} + +# undef PROMISE_LOG +# undef PROMISE_ASSERT +# undef PROMISE_DEBUG + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/Mutex.h b/xpcom/threads/Mutex.h new file mode 100644 index 0000000000..5116f1d7c3 --- /dev/null +++ b/xpcom/threads/Mutex.h @@ -0,0 +1,485 @@ +/* -*- 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 mozilla_Mutex_h +#define mozilla_Mutex_h + +#include "mozilla/BlockingResourceBase.h" +#include "mozilla/ThreadSafety.h" +#include "mozilla/PlatformMutex.h" +#include "mozilla/Maybe.h" +#include "nsISupports.h" + +// +// Provides: +// +// - Mutex, a non-recursive mutex +// - MutexAutoLock, an RAII class for ensuring that Mutexes are properly +// locked and unlocked +// - MutexAutoUnlock, complementary sibling to MutexAutoLock +// +// - OffTheBooksMutex, a non-recursive mutex that doesn't do leak checking +// - OffTheBooksMutexAuto{Lock,Unlock} - Like MutexAuto{Lock,Unlock}, but for +// an OffTheBooksMutex. +// +// Using MutexAutoLock/MutexAutoUnlock etc. is MUCH preferred to making bare +// calls to Lock and Unlock. +// +namespace mozilla { + +/** + * OffTheBooksMutex is identical to Mutex, except that OffTheBooksMutex doesn't + * include leak checking. Sometimes you want to intentionally "leak" a mutex + * until shutdown; in these cases, OffTheBooksMutex is for you. + */ +class MOZ_CAPABILITY("mutex") OffTheBooksMutex : public detail::MutexImpl, + BlockingResourceBase { + public: + /** + * @param aName A name which can reference this lock + * @returns If failure, nullptr + * If success, a valid Mutex* which must be destroyed + * by Mutex::DestroyMutex() + **/ + explicit OffTheBooksMutex(const char* aName) + : BlockingResourceBase(aName, eMutex) +#ifdef DEBUG + , + mOwningThread(nullptr) +#endif + { + } + + ~OffTheBooksMutex() { +#ifdef DEBUG + MOZ_ASSERT(!mOwningThread, "destroying a still-owned lock!"); +#endif + } + +#ifndef DEBUG + /** + * Lock this mutex. + **/ + void Lock() MOZ_CAPABILITY_ACQUIRE() { this->lock(); } + + /** + * Try to lock this mutex, returning true if we were successful. + **/ + [[nodiscard]] bool TryLock() MOZ_TRY_ACQUIRE(true) { return this->tryLock(); } + + /** + * Unlock this mutex. + **/ + void Unlock() MOZ_CAPABILITY_RELEASE() { this->unlock(); } + + /** + * Assert that the current thread owns this mutex in debug builds. + * + * Does nothing in non-debug builds. + **/ + void AssertCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(this) {} + + /** + * Assert that the current thread does not own this mutex. + * + * Note that this function is not implemented for debug builds *and* + * non-debug builds due to difficulties in dealing with memory ordering. + * + * It is therefore mostly useful as documentation. + **/ + void AssertNotCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(!this) {} + +#else + void Lock() MOZ_CAPABILITY_ACQUIRE(); + + [[nodiscard]] bool TryLock() MOZ_TRY_ACQUIRE(true); + void Unlock() MOZ_CAPABILITY_RELEASE(); + + void AssertCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(this); + void AssertNotCurrentThreadOwns() const MOZ_ASSERT_CAPABILITY(!this) { + // FIXME bug 476536 + } +#endif // ifndef DEBUG + + private: + OffTheBooksMutex() = delete; + OffTheBooksMutex(const OffTheBooksMutex&) = delete; + OffTheBooksMutex& operator=(const OffTheBooksMutex&) = delete; + + friend class OffTheBooksCondVar; + +#ifdef DEBUG + PRThread* mOwningThread; +#endif +}; + +/** + * Mutex + * When possible, use MutexAutoLock/MutexAutoUnlock to lock/unlock this + * mutex within a scope, instead of calling Lock/Unlock directly. + */ +class Mutex : public OffTheBooksMutex { + public: + explicit Mutex(const char* aName) : OffTheBooksMutex(aName) { + MOZ_COUNT_CTOR(Mutex); + } + + MOZ_COUNTED_DTOR(Mutex) + + private: + Mutex() = delete; + Mutex(const Mutex&) = delete; + Mutex& operator=(const Mutex&) = delete; +}; + +/** + * MutexSingleWriter + * + * Mutex where a single writer exists, so that reads from the same thread + * will not generate data races or consistency issues. + * + * When possible, use MutexAutoLock/MutexAutoUnlock to lock/unlock this + * mutex within a scope, instead of calling Lock/Unlock directly. + * + * This requires an object implementing Mutex's SingleWriterLockOwner, so + * we can do correct-thread checks. + */ +// Subclass this in the object owning the mutex +class SingleWriterLockOwner { + public: + SingleWriterLockOwner() = default; + ~SingleWriterLockOwner() = default; + + virtual bool OnWritingThread() const = 0; +}; + +class MutexSingleWriter : public OffTheBooksMutex { + public: + // aOwner should be the object that contains the mutex, typically. We + // will use that object (which must have a lifetime the same or greater + // than this object) to verify that we're running on the correct thread, + // typically only in DEBUG builds + explicit MutexSingleWriter(const char* aName, SingleWriterLockOwner* aOwner) + : OffTheBooksMutex(aName) +#ifdef DEBUG + , + mOwner(aOwner) +#endif + { + MOZ_COUNT_CTOR(MutexSingleWriter); + MOZ_ASSERT(mOwner); + } + + MOZ_COUNTED_DTOR(MutexSingleWriter) + + /** + * Statically assert that we're on the only thread that modifies data + * guarded by this Mutex. This allows static checking for the pattern of + * having a single thread modify a set of data, and read it (under lock) + * on other threads, and reads on the thread that modifies it doesn't + * require a lock. This doesn't solve the issue of some data under the + * Mutex following this pattern, and other data under the mutex being + * written from multiple threads. + * + * We could set the writing thread and dynamically check it in debug + * builds, but this doesn't. We could also use thread-safety/capability + * system to provide direct thread assertions. + **/ + void AssertOnWritingThread() const MOZ_ASSERT_CAPABILITY(this) { + MOZ_ASSERT(mOwner->OnWritingThread()); + } + void AssertOnWritingThreadOrHeld() const MOZ_ASSERT_CAPABILITY(this) { +#ifdef DEBUG + if (!mOwner->OnWritingThread()) { + AssertCurrentThreadOwns(); + } +#endif + } + + private: +#ifdef DEBUG + SingleWriterLockOwner* mOwner MOZ_UNSAFE_REF( + "This is normally the object that contains the MonitorSingleWriter, so " + "we don't want to hold a reference to ourselves"); +#endif + + MutexSingleWriter() = delete; + MutexSingleWriter(const MutexSingleWriter&) = delete; + MutexSingleWriter& operator=(const MutexSingleWriter&) = delete; +}; + +namespace detail { +template +class MOZ_RAII BaseAutoUnlock; + +/** + * MutexAutoLock + * Acquires the Mutex when it enters scope, and releases it when it leaves + * scope. + * + * MUCH PREFERRED to bare calls to Mutex.Lock and Unlock. + */ +template +class MOZ_RAII MOZ_SCOPED_CAPABILITY BaseAutoLock { + public: + /** + * Constructor + * The constructor aquires the given lock. The destructor + * releases the lock. + * + * @param aLock A valid mozilla::Mutex* returned by + * mozilla::Mutex::NewMutex. + **/ + explicit BaseAutoLock(T aLock) MOZ_CAPABILITY_ACQUIRE(aLock) : mLock(aLock) { + mLock.Lock(); + } + + ~BaseAutoLock(void) MOZ_CAPABILITY_RELEASE() { mLock.Unlock(); } + + // Assert that aLock is the mutex passed to the constructor and that the + // current thread owns the mutex. In coding patterns such as: + // + // void LockedMethod(const BaseAutoLock& aProofOfLock) + // { + // aProofOfLock.AssertOwns(mMutex); + // ... + // } + // + // Without this assertion, it could be that mMutex is not actually + // locked. It's possible to have code like: + // + // BaseAutoLock lock(someMutex); + // ... + // BaseAutoUnlock unlock(someMutex); + // ... + // LockedMethod(lock); + // + // and in such a case, simply asserting that the mutex pointers match is not + // sufficient; mutex ownership must be asserted as well. + // + // Note that if you are going to use the coding pattern presented above, you + // should use this method in preference to using AssertCurrentThreadOwns on + // the mutex you expected to be held, since this method provides stronger + // guarantees. + void AssertOwns(const T& aMutex) const MOZ_ASSERT_CAPABILITY(aMutex) { + MOZ_ASSERT(&aMutex == &mLock); + mLock.AssertCurrentThreadOwns(); + } + + private: + BaseAutoLock() = delete; + BaseAutoLock(BaseAutoLock&) = delete; + BaseAutoLock& operator=(BaseAutoLock&) = delete; + static void* operator new(size_t) noexcept(true); + + friend class BaseAutoUnlock; + + T mLock; +}; + +template +BaseAutoLock(MutexType&) -> BaseAutoLock; +} // namespace detail + +typedef detail::BaseAutoLock MutexAutoLock; +typedef detail::BaseAutoLock MutexSingleWriterAutoLock; +typedef detail::BaseAutoLock OffTheBooksMutexAutoLock; + +// Specialization of Maybe<*AutoLock> for space efficiency and to silence +// thread-safety analysis, which cannot track what's going on. +template +class Maybe> { + public: + Maybe() : mLock(nullptr) {} + ~Maybe() MOZ_NO_THREAD_SAFETY_ANALYSIS { + if (mLock) { + mLock->Unlock(); + } + } + + constexpr bool isSome() const { return mLock; } + constexpr bool isNothing() const { return !mLock; } + + void emplace(MutexType& aMutex) MOZ_NO_THREAD_SAFETY_ANALYSIS { + MOZ_RELEASE_ASSERT(!mLock); + mLock = &aMutex; + mLock->Lock(); + } + + void reset() MOZ_NO_THREAD_SAFETY_ANALYSIS { + if (mLock) { + mLock->Unlock(); + mLock = nullptr; + } + } + + private: + MutexType* mLock; +}; + +// Use if we've done AssertOnWritingThread(), and then later need to take the +// lock to write to a protected member. Instead of +// MutexSingleWriterAutoLock lock(mutex) +// use +// MutexSingleWriterAutoLockOnThread(lock, mutex) +#define MutexSingleWriterAutoLockOnThread(lock, mutex) \ + MOZ_PUSH_IGNORE_THREAD_SAFETY \ + MutexSingleWriterAutoLock lock(mutex); \ + MOZ_POP_THREAD_SAFETY + +namespace detail { +/** + * ReleasableMutexAutoLock + * Acquires the Mutex when it enters scope, and releases it when it leaves + * scope. Allows calling Unlock (and Lock) as an alternative to + * MutexAutoUnlock; this can avoid an extra lock/unlock pair. + * + */ +template +class MOZ_RAII MOZ_SCOPED_CAPABILITY ReleasableBaseAutoLock { + public: + /** + * Constructor + * The constructor aquires the given lock. The destructor + * releases the lock. + * + * @param aLock A valid mozilla::Mutex& returned by + * mozilla::Mutex::NewMutex. + **/ + explicit ReleasableBaseAutoLock(T aLock) MOZ_CAPABILITY_ACQUIRE(aLock) + : mLock(aLock) { + mLock.Lock(); + mLocked = true; + } + + ~ReleasableBaseAutoLock(void) MOZ_CAPABILITY_RELEASE() { + if (mLocked) { + Unlock(); + } + } + + void AssertOwns(const T& aMutex) const MOZ_ASSERT_CAPABILITY(aMutex) { + MOZ_ASSERT(&aMutex == &mLock); + mLock.AssertCurrentThreadOwns(); + } + + // Allow dropping the lock prematurely; for example to support something like: + // clang-format off + // MutexAutoLock lock(mMutex); + // ... + // if (foo) { + // lock.Unlock(); + // MethodThatCantBeCalledWithLock() + // return; + // } + // clang-format on + void Unlock() MOZ_CAPABILITY_RELEASE() { + MOZ_ASSERT(mLocked); + mLock.Unlock(); + mLocked = false; + } + void Lock() MOZ_CAPABILITY_ACQUIRE() { + MOZ_ASSERT(!mLocked); + mLock.Lock(); + mLocked = true; + } + + private: + ReleasableBaseAutoLock() = delete; + ReleasableBaseAutoLock(ReleasableBaseAutoLock&) = delete; + ReleasableBaseAutoLock& operator=(ReleasableBaseAutoLock&) = delete; + static void* operator new(size_t) noexcept(true); + + bool mLocked; + T mLock; +}; + +template +ReleasableBaseAutoLock(MutexType&) -> ReleasableBaseAutoLock; +} // namespace detail + +typedef detail::ReleasableBaseAutoLock ReleasableMutexAutoLock; + +namespace detail { +/** + * BaseAutoUnlock + * Releases the Mutex when it enters scope, and re-acquires it when it leaves + * scope. + * + * MUCH PREFERRED to bare calls to Mutex.Unlock and Lock. + */ +template +class MOZ_RAII MOZ_SCOPED_CAPABILITY BaseAutoUnlock { + public: + explicit BaseAutoUnlock(T aLock) MOZ_SCOPED_UNLOCK_RELEASE(aLock) + : mLock(aLock) { + mLock.Unlock(); + } + + explicit BaseAutoUnlock(BaseAutoLock& aAutoLock) + /* MOZ_CAPABILITY_RELEASE(aAutoLock.mLock) */ + : mLock(aAutoLock.mLock) { + NS_ASSERTION(mLock, "null lock"); + mLock->Unlock(); + } + + ~BaseAutoUnlock() MOZ_SCOPED_UNLOCK_REACQUIRE() { mLock.Lock(); } + + private: + BaseAutoUnlock() = delete; + BaseAutoUnlock(BaseAutoUnlock&) = delete; + BaseAutoUnlock& operator=(BaseAutoUnlock&) = delete; + static void* operator new(size_t) noexcept(true); + + T mLock; +}; + +template +BaseAutoUnlock(MutexType&) -> BaseAutoUnlock; +} // namespace detail + +typedef detail::BaseAutoUnlock MutexAutoUnlock; +typedef detail::BaseAutoUnlock MutexSingleWriterAutoUnlock; +typedef detail::BaseAutoUnlock OffTheBooksMutexAutoUnlock; + +namespace detail { +/** + * BaseAutoTryLock + * Tries to acquire the Mutex when it enters scope, and releases it when it + * leaves scope. + * + * MUCH PREFERRED to bare calls to Mutex.TryLock and Unlock. + */ +template +class MOZ_RAII MOZ_SCOPED_CAPABILITY BaseAutoTryLock { + public: + explicit BaseAutoTryLock(T& aLock) MOZ_CAPABILITY_ACQUIRE(aLock) + : mLock(aLock.TryLock() ? &aLock : nullptr) {} + + ~BaseAutoTryLock() MOZ_CAPABILITY_RELEASE() { + if (mLock) { + mLock->Unlock(); + mLock = nullptr; + } + } + + explicit operator bool() const { return mLock; } + + private: + BaseAutoTryLock(BaseAutoTryLock&) = delete; + BaseAutoTryLock& operator=(BaseAutoTryLock&) = delete; + static void* operator new(size_t) noexcept(true); + + T* mLock; +}; +} // namespace detail + +typedef detail::BaseAutoTryLock MutexAutoTryLock; +typedef detail::BaseAutoTryLock OffTheBooksMutexAutoTryLock; + +} // namespace mozilla + +#endif // ifndef mozilla_Mutex_h diff --git a/xpcom/threads/Queue.h b/xpcom/threads/Queue.h new file mode 100644 index 0000000000..fa36433fdf --- /dev/null +++ b/xpcom/threads/Queue.h @@ -0,0 +1,265 @@ +/* -*- 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 mozilla_Queue_h +#define mozilla_Queue_h + +#include +#include +#include "mozilla/MemoryReporting.h" +#include "mozilla/Assertions.h" +#include "mozalloc.h" + +namespace mozilla { + +// define to turn on additional (DEBUG) asserts +// #define EXTRA_ASSERTS 1 + +// A queue implements a singly linked list of pages, each of which contains some +// number of elements. Since the queue needs to store a "next" pointer, the +// actual number of elements per page won't be quite as many as were requested. +// +// Each page consists of N entries. We use the head buffer as a circular buffer +// if it's the only buffer; if we have more than one buffer when the head is +// empty we release it. This avoids occasional freeing and reallocating buffers +// every N entries. We'll still allocate and free every N if the normal queue +// depth is greated than N. A fancier solution would be to move an empty Head +// buffer to be an empty tail buffer, freeing if we have multiple empty tails, +// but that probably isn't worth it. +// +// Cases: +// a) single buffer, circular +// Push: if not full: +// Add to tail, bump tail and reset to 0 if at end +// full: +// Add new page, insert there and set tail to 1 +// Pop: +// take entry and bump head, reset to 0 if at end +// b) multiple buffers: +// Push: if not full: +// Add to tail, bump tail +// full: +// Add new page, insert there and set tail to 1 +// Pop: +// take entry and bump head, reset to 0 if at end +// if buffer is empty, free head buffer and promote next to head +// +template +class Queue { + public: + Queue() = default; + + Queue(Queue&& aOther) noexcept + : mHead(std::exchange(aOther.mHead, nullptr)), + mTail(std::exchange(aOther.mTail, nullptr)), + mOffsetHead(std::exchange(aOther.mOffsetHead, 0)), + mHeadLength(std::exchange(aOther.mHeadLength, 0)), + mTailLength(std::exchange(aOther.mTailLength, 0)) {} + + Queue& operator=(Queue&& aOther) noexcept { + Clear(); + + mHead = std::exchange(aOther.mHead, nullptr); + mTail = std::exchange(aOther.mTail, nullptr); + mOffsetHead = std::exchange(aOther.mOffsetHead, 0); + mHeadLength = std::exchange(aOther.mHeadLength, 0); + mTailLength = std::exchange(aOther.mTailLength, 0); + return *this; + } + + ~Queue() { Clear(); } + + // Discard all elements form the queue, clearing it to be empty. + void Clear() { + while (!IsEmpty()) { + Pop(); + } + if (mHead) { + free(mHead); + mHead = nullptr; + } + } + + T& Push(T&& aElement) { +#if defined(EXTRA_ASSERTS) && DEBUG + size_t original_length = Count(); +#endif + if (!mHead) { + mHead = NewPage(); + MOZ_ASSERT(mHead); + + mTail = mHead; + T* eltPtr = &mTail->mEvents[0]; + new (eltPtr) T(std::move(aElement)); + mOffsetHead = 0; + mHeadLength = 1; +#ifdef EXTRA_ASSERTS + MOZ_ASSERT(Count() == original_length + 1); +#endif + return *eltPtr; + } + if ((mHead == mTail && mHeadLength == ItemsPerPage) || + (mHead != mTail && mTailLength == ItemsPerPage)) { + // either we have one (circular) buffer and it's full, or + // we have multiple buffers and the last buffer is full + Page* page = NewPage(); + MOZ_ASSERT(page); + + mTail->mNext = page; + mTail = page; + T* eltPtr = &page->mEvents[0]; + new (eltPtr) T(std::move(aElement)); + mTailLength = 1; +#ifdef EXTRA_ASSERTS + MOZ_ASSERT(Count() == original_length + 1); +#endif + return *eltPtr; + } + if (mHead == mTail) { + // we have space in the (single) head buffer + uint16_t offset = (mOffsetHead + mHeadLength++) % ItemsPerPage; + T* eltPtr = &mTail->mEvents[offset]; + new (eltPtr) T(std::move(aElement)); +#ifdef EXTRA_ASSERTS + MOZ_ASSERT(Count() == original_length + 1); +#endif + return *eltPtr; + } + // else we have space to insert into last buffer + T* eltPtr = &mTail->mEvents[mTailLength++]; + new (eltPtr) T(std::move(aElement)); +#ifdef EXTRA_ASSERTS + MOZ_ASSERT(Count() == original_length + 1); +#endif + return *eltPtr; + } + + bool IsEmpty() const { + return !mHead || (mHead == mTail && mHeadLength == 0); + } + + T Pop() { +#if defined(EXTRA_ASSERTS) && DEBUG + size_t original_length = Count(); +#endif + MOZ_ASSERT(!IsEmpty()); + + T result = std::move(mHead->mEvents[mOffsetHead]); + mHead->mEvents[mOffsetHead].~T(); + mOffsetHead = (mOffsetHead + 1) % ItemsPerPage; + mHeadLength -= 1; + + // Check if mHead points to empty (circular) Page and we have more + // pages + if (mHead != mTail && mHeadLength == 0) { + Page* dead = mHead; + mHead = mHead->mNext; + free(dead); + mOffsetHead = 0; + // if there are still >1 pages, the new head is full. + if (mHead != mTail) { + mHeadLength = ItemsPerPage; + } else { + mHeadLength = mTailLength; + mTailLength = 0; + } + } + +#ifdef EXTRA_ASSERTS + MOZ_ASSERT(Count() == original_length - 1); +#endif + return result; + } + + T& FirstElement() { + MOZ_ASSERT(!IsEmpty()); + return mHead->mEvents[mOffsetHead]; + } + + const T& FirstElement() const { + MOZ_ASSERT(!IsEmpty()); + return mHead->mEvents[mOffsetHead]; + } + + T& LastElement() { + MOZ_ASSERT(!IsEmpty()); + uint16_t offset = + mHead == mTail ? mOffsetHead + mHeadLength - 1 : mTailLength - 1; + return mTail->mEvents[offset]; + } + + const T& LastElement() const { + MOZ_ASSERT(!IsEmpty()); + uint16_t offset = + mHead == mTail ? mOffsetHead + mHeadLength - 1 : mTailLength - 1; + return mTail->mEvents[offset]; + } + + size_t Count() const { + // It is obvious count is 0 when the queue is empty. + if (!mHead) { + return 0; + } + + // Compute full (intermediate) pages; Doesn't count first or last page + int count = 0; + // 1 buffer will have mHead == mTail; 2 will have mHead->mNext == mTail + for (Page* page = mHead; page != mTail && page->mNext != mTail; + page = page->mNext) { + count += ItemsPerPage; + } + // add first and last page + count += mHeadLength + mTailLength; + MOZ_ASSERT(count >= 0); + + return count; + } + + size_t ShallowSizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + if (mHead) { + for (Page* page = mHead; page != mTail; page = page->mNext) { + n += aMallocSizeOf(page); + } + } + return n; + } + + size_t ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const { + return aMallocSizeOf(this) + ShallowSizeOfExcludingThis(aMallocSizeOf); + } + + private: + static_assert( + (RequestedItemsPerPage & (RequestedItemsPerPage - 1)) == 0, + "RequestedItemsPerPage should be a power of two to avoid heap slop."); + + // Since a Page must also contain a "next" pointer, we use one of the items to + // store this pointer. If sizeof(T) > sizeof(Page*), then some space will be + // wasted. So be it. + static const size_t ItemsPerPage = RequestedItemsPerPage - 1; + + // Page objects are linked together to form a simple deque. + struct Page { + struct Page* mNext; + T mEvents[ItemsPerPage]; + }; + + static Page* NewPage() { + return static_cast(moz_xcalloc(1, sizeof(Page))); + } + + Page* mHead = nullptr; + Page* mTail = nullptr; + + uint16_t mOffsetHead = 0; // Read position in head page + uint16_t mHeadLength = 0; // Number of items in the head page + uint16_t mTailLength = 0; // Number of items in the tail page +}; + +} // namespace mozilla + +#endif // mozilla_Queue_h diff --git a/xpcom/threads/RWLock.cpp b/xpcom/threads/RWLock.cpp new file mode 100644 index 0000000000..949934c8cc --- /dev/null +++ b/xpcom/threads/RWLock.cpp @@ -0,0 +1,28 @@ +/* -*- 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/RWLock.h" + +namespace mozilla { + +RWLock::RWLock(const char* aName) + : BlockingResourceBase(aName, eMutex) +#ifdef DEBUG + , + mOwningThread(nullptr) +#endif +{ +} + +#ifdef DEBUG +bool RWLock::LockedForWritingByCurrentThread() { + return mOwningThread == PR_GetCurrentThread(); +} +#endif + +} // namespace mozilla + +#undef NativeHandle diff --git a/xpcom/threads/RWLock.h b/xpcom/threads/RWLock.h new file mode 100644 index 0000000000..e03d008631 --- /dev/null +++ b/xpcom/threads/RWLock.h @@ -0,0 +1,243 @@ +/* -*- 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/. */ + +// An interface for read-write locks. + +#ifndef mozilla_RWLock_h +#define mozilla_RWLock_h + +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/BlockingResourceBase.h" +#include "mozilla/PlatformRWLock.h" +#include "mozilla/ThreadSafety.h" + +namespace mozilla { + +// A RWLock is similar to a Mutex, but whereas a Mutex permits only a single +// reader thread or a single writer thread to access a piece of data, a +// RWLock distinguishes between readers and writers: you may have multiple +// reader threads concurrently accessing a piece of data or a single writer +// thread. This difference should guide your usage of RWLock: if you are not +// reading the data from multiple threads simultaneously or you are writing +// to the data roughly as often as read from it, then Mutex will suit your +// purposes just fine. +// +// You should be using the AutoReadLock and AutoWriteLock classes, below, +// for RAII read locking and write locking, respectively. If you really must +// take a read lock manually, call the ReadLock method; to relinquish that +// read lock, call the ReadUnlock method. Similarly, WriteLock and WriteUnlock +// perform the same operations, but for write locks. +// +// It is unspecified what happens when a given thread attempts to acquire the +// same lock in multiple ways; some underlying implementations of RWLock do +// support acquiring a read lock multiple times on a given thread, but you +// should not rely on this behavior. +// +// It is unspecified whether RWLock gives priority to waiting readers or +// a waiting writer when unlocking. +class MOZ_CAPABILITY("rwlock") RWLock : public detail::RWLockImpl, + public BlockingResourceBase { + public: + explicit RWLock(const char* aName); + +#ifdef DEBUG + bool LockedForWritingByCurrentThread(); + [[nodiscard]] bool TryReadLock() MOZ_SHARED_TRYLOCK_FUNCTION(true); + void ReadLock() MOZ_ACQUIRE_SHARED(); + void ReadUnlock() MOZ_RELEASE_SHARED(); + [[nodiscard]] bool TryWriteLock() MOZ_TRY_ACQUIRE(true); + void WriteLock() MOZ_CAPABILITY_ACQUIRE(); + void WriteUnlock() MOZ_EXCLUSIVE_RELEASE(); +#else + [[nodiscard]] bool TryReadLock() MOZ_SHARED_TRYLOCK_FUNCTION(true) { + return detail::RWLockImpl::tryReadLock(); + } + void ReadLock() MOZ_ACQUIRE_SHARED() { detail::RWLockImpl::readLock(); } + void ReadUnlock() MOZ_RELEASE_SHARED() { detail::RWLockImpl::readUnlock(); } + [[nodiscard]] bool TryWriteLock() MOZ_TRY_ACQUIRE(true) { + return detail::RWLockImpl::tryWriteLock(); + } + void WriteLock() MOZ_CAPABILITY_ACQUIRE() { detail::RWLockImpl::writeLock(); } + void WriteUnlock() MOZ_EXCLUSIVE_RELEASE() { + detail::RWLockImpl::writeUnlock(); + } +#endif + + private: + RWLock() = delete; + RWLock(const RWLock&) = delete; + RWLock& operator=(const RWLock&) = delete; + +#ifdef DEBUG + // We record the owning thread for write locks only. + PRThread* mOwningThread; +#endif +}; + +// We only use this once; not sure we can add thread safety attributions here +template +class MOZ_RAII BaseAutoTryReadLock { + public: + explicit BaseAutoTryReadLock(T& aLock) + : mLock(aLock.TryReadLock() ? &aLock : nullptr) {} + + ~BaseAutoTryReadLock() { + if (mLock) { + mLock->ReadUnlock(); + } + } + + explicit operator bool() const { return mLock; } + + private: + BaseAutoTryReadLock() = delete; + BaseAutoTryReadLock(const BaseAutoTryReadLock&) = delete; + BaseAutoTryReadLock& operator=(const BaseAutoTryReadLock&) = delete; + + T* mLock; +}; + +template +class MOZ_SCOPED_CAPABILITY MOZ_RAII BaseAutoReadLock { + public: + explicit BaseAutoReadLock(T& aLock) MOZ_ACQUIRE_SHARED(aLock) + : mLock(&aLock) { + MOZ_ASSERT(mLock, "null lock"); + mLock->ReadLock(); + } + + // Not MOZ_RELEASE_SHARED(), which would make sense - apparently this trips + // over a bug in clang's static analyzer and it says it expected an + // exclusive unlock. + ~BaseAutoReadLock() MOZ_RELEASE_GENERIC() { mLock->ReadUnlock(); } + + private: + BaseAutoReadLock() = delete; + BaseAutoReadLock(const BaseAutoReadLock&) = delete; + BaseAutoReadLock& operator=(const BaseAutoReadLock&) = delete; + + T* mLock; +}; + +// XXX Mutex attributions? +template +class MOZ_RAII BaseAutoTryWriteLock { + public: + explicit BaseAutoTryWriteLock(T& aLock) + : mLock(aLock.TryWriteLock() ? &aLock : nullptr) {} + + ~BaseAutoTryWriteLock() { + if (mLock) { + mLock->WriteUnlock(); + } + } + + explicit operator bool() const { return mLock; } + + private: + BaseAutoTryWriteLock() = delete; + BaseAutoTryWriteLock(const BaseAutoTryWriteLock&) = delete; + BaseAutoTryWriteLock& operator=(const BaseAutoTryWriteLock&) = delete; + + T* mLock; +}; + +template +class MOZ_SCOPED_CAPABILITY MOZ_RAII BaseAutoWriteLock final { + public: + explicit BaseAutoWriteLock(T& aLock) MOZ_CAPABILITY_ACQUIRE(aLock) + : mLock(&aLock) { + MOZ_ASSERT(mLock, "null lock"); + mLock->WriteLock(); + } + + ~BaseAutoWriteLock() MOZ_CAPABILITY_RELEASE() { mLock->WriteUnlock(); } + + private: + BaseAutoWriteLock() = delete; + BaseAutoWriteLock(const BaseAutoWriteLock&) = delete; + BaseAutoWriteLock& operator=(const BaseAutoWriteLock&) = delete; + + T* mLock; +}; + +// Read try-lock and unlock a RWLock with RAII semantics. Much preferred to +// bare calls to TryReadLock() and ReadUnlock(). +typedef BaseAutoTryReadLock AutoTryReadLock; + +// Read lock and unlock a RWLock with RAII semantics. Much preferred to bare +// calls to ReadLock() and ReadUnlock(). +typedef BaseAutoReadLock AutoReadLock; + +// Write try-lock and unlock a RWLock with RAII semantics. Much preferred to +// bare calls to TryWriteLock() and WriteUnlock(). +typedef BaseAutoTryWriteLock AutoTryWriteLock; + +// Write lock and unlock a RWLock with RAII semantics. Much preferred to bare +// calls to WriteLock() and WriteUnlock(). +typedef BaseAutoWriteLock AutoWriteLock; + +class MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS MOZ_CAPABILITY("rwlock") + StaticRWLock { + public: + // In debug builds, check that mLock is initialized for us as we expect by + // the compiler. In non-debug builds, don't declare a constructor so that + // the compiler can see that the constructor is trivial. +#ifdef DEBUG + StaticRWLock() { MOZ_ASSERT(!mLock); } +#endif + + [[nodiscard]] bool TryReadLock() MOZ_SHARED_TRYLOCK_FUNCTION(true) { + return Lock()->TryReadLock(); + } + void ReadLock() MOZ_ACQUIRE_SHARED() { Lock()->ReadLock(); } + void ReadUnlock() MOZ_RELEASE_SHARED() { Lock()->ReadUnlock(); } + [[nodiscard]] bool TryWriteLock() MOZ_TRY_ACQUIRE(true) { + return Lock()->TryWriteLock(); + } + void WriteLock() MOZ_CAPABILITY_ACQUIRE() { Lock()->WriteLock(); } + void WriteUnlock() MOZ_EXCLUSIVE_RELEASE() { Lock()->WriteUnlock(); } + + private: + [[nodiscard]] RWLock* Lock() MOZ_RETURN_CAPABILITY(*mLock) { + if (mLock) { + return mLock; + } + + RWLock* lock = new RWLock("StaticRWLock"); + if (!mLock.compareExchange(nullptr, lock)) { + delete lock; + } + + return mLock; + } + + Atomic mLock; + + // Disallow copy constructor, but only in debug mode. We only define + // a default constructor in debug mode (see above); if we declared + // this constructor always, the compiler wouldn't generate a trivial + // default constructor for us in non-debug mode. +#ifdef DEBUG + StaticRWLock(const StaticRWLock& aOther); +#endif + + // Disallow these operators. + StaticRWLock& operator=(StaticRWLock* aRhs) = delete; + static void* operator new(size_t) noexcept(true) = delete; + static void operator delete(void*) = delete; +}; + +typedef BaseAutoTryReadLock StaticAutoTryReadLock; +typedef BaseAutoReadLock StaticAutoReadLock; +typedef BaseAutoTryWriteLock StaticAutoTryWriteLock; +typedef BaseAutoWriteLock StaticAutoWriteLock; + +} // namespace mozilla + +#endif // mozilla_RWLock_h diff --git a/xpcom/threads/RecursiveMutex.cpp b/xpcom/threads/RecursiveMutex.cpp new file mode 100644 index 0000000000..7c45052c07 --- /dev/null +++ b/xpcom/threads/RecursiveMutex.cpp @@ -0,0 +1,85 @@ +/* -*- 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/RecursiveMutex.h" + +#ifdef XP_WIN +# include + +# define NativeHandle(m) (reinterpret_cast(&m)) +#endif + +namespace mozilla { + +RecursiveMutex::RecursiveMutex(const char* aName) + : BlockingResourceBase(aName, eRecursiveMutex) +#ifdef DEBUG + , + mOwningThread(nullptr), + mEntryCount(0) +#endif +{ +#ifdef XP_WIN + // This number was adapted from NSPR. + static const DWORD sLockSpinCount = 100; + +# if defined(RELEASE_OR_BETA) + // Vista and later automatically allocate and subsequently leak a debug info + // object for each critical section that we allocate unless we tell the + // system not to do that. + DWORD flags = CRITICAL_SECTION_NO_DEBUG_INFO; +# else + DWORD flags = 0; +# endif + BOOL r = + InitializeCriticalSectionEx(NativeHandle(mMutex), sLockSpinCount, flags); + MOZ_RELEASE_ASSERT(r); +#else + pthread_mutexattr_t attr; + + MOZ_RELEASE_ASSERT(pthread_mutexattr_init(&attr) == 0, + "pthread_mutexattr_init failed"); + + MOZ_RELEASE_ASSERT( + pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE) == 0, + "pthread_mutexattr_settype failed"); + + MOZ_RELEASE_ASSERT(pthread_mutex_init(&mMutex, &attr) == 0, + "pthread_mutex_init failed"); + + MOZ_RELEASE_ASSERT(pthread_mutexattr_destroy(&attr) == 0, + "pthread_mutexattr_destroy failed"); +#endif +} + +RecursiveMutex::~RecursiveMutex() { +#ifdef XP_WIN + DeleteCriticalSection(NativeHandle(mMutex)); +#else + MOZ_RELEASE_ASSERT(pthread_mutex_destroy(&mMutex) == 0, + "pthread_mutex_destroy failed"); +#endif +} + +void RecursiveMutex::LockInternal() { +#ifdef XP_WIN + EnterCriticalSection(NativeHandle(mMutex)); +#else + MOZ_RELEASE_ASSERT(pthread_mutex_lock(&mMutex) == 0, + "pthread_mutex_lock failed"); +#endif +} + +void RecursiveMutex::UnlockInternal() { +#ifdef XP_WIN + LeaveCriticalSection(NativeHandle(mMutex)); +#else + MOZ_RELEASE_ASSERT(pthread_mutex_unlock(&mMutex) == 0, + "pthread_mutex_unlock failed"); +#endif +} + +} // namespace mozilla diff --git a/xpcom/threads/RecursiveMutex.h b/xpcom/threads/RecursiveMutex.h new file mode 100644 index 0000000000..dde21c9a35 --- /dev/null +++ b/xpcom/threads/RecursiveMutex.h @@ -0,0 +1,120 @@ +/* -*- 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/. */ + +// A lock that can be acquired multiple times on the same thread. + +#ifndef mozilla_RecursiveMutex_h +#define mozilla_RecursiveMutex_h + +#include "mozilla/ThreadSafety.h" +#include "mozilla/BlockingResourceBase.h" + +#ifndef XP_WIN +# include +#endif + +namespace mozilla { + +class MOZ_CAPABILITY("recursive mutex") RecursiveMutex + : public BlockingResourceBase { + public: + explicit RecursiveMutex(const char* aName); + ~RecursiveMutex(); + +#ifdef DEBUG + void Lock() MOZ_CAPABILITY_ACQUIRE(); + void Unlock() MOZ_CAPABILITY_RELEASE(); +#else + void Lock() MOZ_CAPABILITY_ACQUIRE() { LockInternal(); } + void Unlock() MOZ_CAPABILITY_RELEASE() { UnlockInternal(); } +#endif + +#ifdef DEBUG + /** + * AssertCurrentThreadIn + **/ + void AssertCurrentThreadIn() const MOZ_ASSERT_CAPABILITY(this); + /** + * AssertNotCurrentThreadIn + **/ + void AssertNotCurrentThreadIn() const MOZ_EXCLUDES(this) { + // Not currently implemented. See bug 476536 for discussion. + } +#else + void AssertCurrentThreadIn() const MOZ_ASSERT_CAPABILITY(this) {} + void AssertNotCurrentThreadIn() const MOZ_EXCLUDES(this) {} +#endif + + private: + RecursiveMutex() = delete; + RecursiveMutex(const RecursiveMutex&) = delete; + RecursiveMutex& operator=(const RecursiveMutex&) = delete; + + void LockInternal(); + void UnlockInternal(); + +#ifdef DEBUG + PRThread* mOwningThread; + size_t mEntryCount; +#endif + +#if !defined(XP_WIN) + pthread_mutex_t mMutex; +#else + // We eschew including windows.h and using CRITICAL_SECTION here so that files + // including us don't also pull in windows.h. Just use a type that's big + // enough for CRITICAL_SECTION, and we'll fix it up later. + void* mMutex[6]; +#endif +}; + +class MOZ_RAII MOZ_SCOPED_CAPABILITY RecursiveMutexAutoLock { + public: + explicit RecursiveMutexAutoLock(RecursiveMutex& aRecursiveMutex) + MOZ_CAPABILITY_ACQUIRE(aRecursiveMutex) + : mRecursiveMutex(&aRecursiveMutex) { + NS_ASSERTION(mRecursiveMutex, "null mutex"); + mRecursiveMutex->Lock(); + } + + ~RecursiveMutexAutoLock(void) MOZ_CAPABILITY_RELEASE() { + mRecursiveMutex->Unlock(); + } + + private: + RecursiveMutexAutoLock() = delete; + RecursiveMutexAutoLock(const RecursiveMutexAutoLock&) = delete; + RecursiveMutexAutoLock& operator=(const RecursiveMutexAutoLock&) = delete; + static void* operator new(size_t) noexcept(true); + + mozilla::RecursiveMutex* mRecursiveMutex; +}; + +class MOZ_RAII MOZ_SCOPED_CAPABILITY RecursiveMutexAutoUnlock { + public: + explicit RecursiveMutexAutoUnlock(RecursiveMutex& aRecursiveMutex) + MOZ_SCOPED_UNLOCK_RELEASE(aRecursiveMutex) + : mRecursiveMutex(&aRecursiveMutex) { + NS_ASSERTION(mRecursiveMutex, "null mutex"); + mRecursiveMutex->Unlock(); + } + + ~RecursiveMutexAutoUnlock(void) MOZ_SCOPED_UNLOCK_REACQUIRE() { + mRecursiveMutex->Lock(); + } + + private: + RecursiveMutexAutoUnlock() = delete; + RecursiveMutexAutoUnlock(const RecursiveMutexAutoUnlock&) = delete; + RecursiveMutexAutoUnlock& operator=(const RecursiveMutexAutoUnlock&) = delete; + static void* operator new(size_t) noexcept(true); + + mozilla::RecursiveMutex* mRecursiveMutex; +}; + +} // namespace mozilla + +#endif // mozilla_RecursiveMutex_h diff --git a/xpcom/threads/ReentrantMonitor.h b/xpcom/threads/ReentrantMonitor.h new file mode 100644 index 0000000000..09debad577 --- /dev/null +++ b/xpcom/threads/ReentrantMonitor.h @@ -0,0 +1,251 @@ +/* -*- 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 mozilla_ReentrantMonitor_h +#define mozilla_ReentrantMonitor_h + +#include "prmon.h" + +#if defined(MOZILLA_INTERNAL_API) && !defined(DEBUG) +# include "mozilla/ProfilerThreadSleep.h" +#endif // defined( MOZILLA_INTERNAL_API) && !defined(DEBUG) + +#include "mozilla/BlockingResourceBase.h" +#include "mozilla/ThreadSafety.h" +#include "nsISupports.h" +// +// Provides: +// +// - ReentrantMonitor, a Java-like monitor +// - ReentrantMonitorAutoEnter, an RAII class for ensuring that +// ReentrantMonitors are properly entered and exited +// +// Using ReentrantMonitorAutoEnter is MUCH preferred to making bare calls to +// ReentrantMonitor.Enter and Exit. +// +namespace mozilla { + +/** + * ReentrantMonitor + * Java-like monitor. + * When possible, use ReentrantMonitorAutoEnter to hold this monitor within a + * scope, instead of calling Enter/Exit directly. + **/ +class MOZ_CAPABILITY("reentrant monitor") ReentrantMonitor + : BlockingResourceBase { + public: + /** + * ReentrantMonitor + * @param aName A name which can reference this monitor + */ + explicit ReentrantMonitor(const char* aName) + : BlockingResourceBase(aName, eReentrantMonitor) +#ifdef DEBUG + , + mEntryCount(0) +#endif + { + MOZ_COUNT_CTOR(ReentrantMonitor); + mReentrantMonitor = PR_NewMonitor(); + if (!mReentrantMonitor) { + MOZ_CRASH("Can't allocate mozilla::ReentrantMonitor"); + } + } + + /** + * ~ReentrantMonitor + **/ + ~ReentrantMonitor() { + NS_ASSERTION(mReentrantMonitor, + "improperly constructed ReentrantMonitor or double free"); + PR_DestroyMonitor(mReentrantMonitor); + mReentrantMonitor = 0; + MOZ_COUNT_DTOR(ReentrantMonitor); + } + +#ifndef DEBUG + /** + * Enter + * @see prmon.h + **/ + void Enter() MOZ_CAPABILITY_ACQUIRE() { PR_EnterMonitor(mReentrantMonitor); } + + /** + * Exit + * @see prmon.h + **/ + void Exit() MOZ_CAPABILITY_RELEASE() { PR_ExitMonitor(mReentrantMonitor); } + + /** + * Wait + * @see prmon.h + **/ + nsresult Wait(PRIntervalTime aInterval = PR_INTERVAL_NO_TIMEOUT) { + PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mReentrantMonitor); +# ifdef MOZILLA_INTERNAL_API + AUTO_PROFILER_THREAD_SLEEP; +# endif // MOZILLA_INTERNAL_API + return PR_Wait(mReentrantMonitor, aInterval) == PR_SUCCESS + ? NS_OK + : NS_ERROR_FAILURE; + } + +#else // ifndef DEBUG + void Enter() MOZ_CAPABILITY_ACQUIRE(); + void Exit() MOZ_CAPABILITY_RELEASE(); + nsresult Wait(PRIntervalTime aInterval = PR_INTERVAL_NO_TIMEOUT); + +#endif // ifndef DEBUG + + /** + * Notify + * @see prmon.h + **/ + nsresult Notify() { + return PR_Notify(mReentrantMonitor) == PR_SUCCESS ? NS_OK + : NS_ERROR_FAILURE; + } + + /** + * NotifyAll + * @see prmon.h + **/ + nsresult NotifyAll() { + return PR_NotifyAll(mReentrantMonitor) == PR_SUCCESS ? NS_OK + : NS_ERROR_FAILURE; + } + +#ifdef DEBUG + /** + * AssertCurrentThreadIn + * @see prmon.h + **/ + void AssertCurrentThreadIn() MOZ_ASSERT_CAPABILITY(this) { + PR_ASSERT_CURRENT_THREAD_IN_MONITOR(mReentrantMonitor); + } + + /** + * AssertNotCurrentThreadIn + * @see prmon.h + **/ + void AssertNotCurrentThreadIn() MOZ_ASSERT_CAPABILITY(!this) { + // FIXME bug 476536 + } + +#else + void AssertCurrentThreadIn() MOZ_ASSERT_CAPABILITY(this) {} + void AssertNotCurrentThreadIn() MOZ_ASSERT_CAPABILITY(!this) {} + +#endif // ifdef DEBUG + + private: + ReentrantMonitor(); + ReentrantMonitor(const ReentrantMonitor&); + ReentrantMonitor& operator=(const ReentrantMonitor&); + + PRMonitor* mReentrantMonitor; +#ifdef DEBUG + int32_t mEntryCount; +#endif +}; + +/** + * ReentrantMonitorAutoEnter + * Enters the ReentrantMonitor when it enters scope, and exits it when + * it leaves scope. + * + * MUCH PREFERRED to bare calls to ReentrantMonitor.Enter and Exit. + */ +class MOZ_SCOPED_CAPABILITY MOZ_STACK_CLASS ReentrantMonitorAutoEnter { + public: + /** + * Constructor + * The constructor aquires the given lock. The destructor + * releases the lock. + * + * @param aReentrantMonitor A valid mozilla::ReentrantMonitor*. + **/ + explicit ReentrantMonitorAutoEnter( + mozilla::ReentrantMonitor& aReentrantMonitor) + MOZ_CAPABILITY_ACQUIRE(aReentrantMonitor) + : mReentrantMonitor(&aReentrantMonitor) { + NS_ASSERTION(mReentrantMonitor, "null monitor"); + mReentrantMonitor->Enter(); + } + + ~ReentrantMonitorAutoEnter(void) MOZ_CAPABILITY_RELEASE() { + mReentrantMonitor->Exit(); + } + + nsresult Wait(PRIntervalTime aInterval = PR_INTERVAL_NO_TIMEOUT) { + return mReentrantMonitor->Wait(aInterval); + } + + nsresult Notify() { return mReentrantMonitor->Notify(); } + nsresult NotifyAll() { return mReentrantMonitor->NotifyAll(); } + + private: + ReentrantMonitorAutoEnter(); + ReentrantMonitorAutoEnter(const ReentrantMonitorAutoEnter&); + ReentrantMonitorAutoEnter& operator=(const ReentrantMonitorAutoEnter&); + static void* operator new(size_t) noexcept(true); + + friend class ReentrantMonitorAutoExit; + + mozilla::ReentrantMonitor* mReentrantMonitor; +}; + +/** + * ReentrantMonitorAutoExit + * Exit the ReentrantMonitor when it enters scope, and enters it when it leaves + * scope. + * + * MUCH PREFERRED to bare calls to ReentrantMonitor.Exit and Enter. + */ +class MOZ_SCOPED_CAPABILITY MOZ_STACK_CLASS ReentrantMonitorAutoExit { + public: + /** + * Constructor + * The constructor releases the given lock. The destructor + * acquires the lock. The lock must be held before constructing + * this object! + * + * @param aReentrantMonitor A valid mozilla::ReentrantMonitor*. It + * must be already locked. + **/ + explicit ReentrantMonitorAutoExit(ReentrantMonitor& aReentrantMonitor) + MOZ_EXCLUSIVE_RELEASE(aReentrantMonitor) + : mReentrantMonitor(&aReentrantMonitor) { + NS_ASSERTION(mReentrantMonitor, "null monitor"); + mReentrantMonitor->AssertCurrentThreadIn(); + mReentrantMonitor->Exit(); + } + + explicit ReentrantMonitorAutoExit( + ReentrantMonitorAutoEnter& aReentrantMonitorAutoEnter) + MOZ_EXCLUSIVE_RELEASE(aReentrantMonitorAutoEnter.mReentrantMonitor) + : mReentrantMonitor(aReentrantMonitorAutoEnter.mReentrantMonitor) { + NS_ASSERTION(mReentrantMonitor, "null monitor"); + mReentrantMonitor->AssertCurrentThreadIn(); + mReentrantMonitor->Exit(); + } + + ~ReentrantMonitorAutoExit(void) MOZ_EXCLUSIVE_RELEASE() { + mReentrantMonitor->Enter(); + } + + private: + ReentrantMonitorAutoExit(); + ReentrantMonitorAutoExit(const ReentrantMonitorAutoExit&); + ReentrantMonitorAutoExit& operator=(const ReentrantMonitorAutoExit&); + static void* operator new(size_t) noexcept(true); + + ReentrantMonitor* mReentrantMonitor; +}; + +} // namespace mozilla + +#endif // ifndef mozilla_ReentrantMonitor_h diff --git a/xpcom/threads/SchedulerGroup.cpp b/xpcom/threads/SchedulerGroup.cpp new file mode 100644 index 0000000000..f69ce07003 --- /dev/null +++ b/xpcom/threads/SchedulerGroup.cpp @@ -0,0 +1,20 @@ +/* -*- 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/SchedulerGroup.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +/* static */ +nsresult SchedulerGroup::Dispatch(already_AddRefed&& aRunnable) { + if (NS_IsMainThread()) { + return NS_DispatchToCurrentThread(std::move(aRunnable)); + } + return NS_DispatchToMainThread(std::move(aRunnable)); +} + +} // namespace mozilla diff --git a/xpcom/threads/SchedulerGroup.h b/xpcom/threads/SchedulerGroup.h new file mode 100644 index 0000000000..09f0aac26a --- /dev/null +++ b/xpcom/threads/SchedulerGroup.h @@ -0,0 +1,26 @@ +/* -*- 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 mozilla_SchedulerGroup_h +#define mozilla_SchedulerGroup_h + +#include "nscore.h" +#include "mozilla/AlreadyAddRefed.h" + +class nsIEventTarget; +class nsIRunnable; +class nsISerialEventTarget; + +namespace mozilla { + +class SchedulerGroup { + public: + static nsresult Dispatch(already_AddRefed&& aRunnable); +}; + +} // namespace mozilla + +#endif // mozilla_SchedulerGroup_h diff --git a/xpcom/threads/SharedThreadPool.cpp b/xpcom/threads/SharedThreadPool.cpp new file mode 100644 index 0000000000..a2de1c4495 --- /dev/null +++ b/xpcom/threads/SharedThreadPool.cpp @@ -0,0 +1,221 @@ +/* -*- 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/SharedThreadPool.h" +#include "mozilla/Monitor.h" +#include "mozilla/ReentrantMonitor.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPtr.h" +#include "nsTHashMap.h" +#include "nsXPCOMCIDInternal.h" +#include "nsComponentManagerUtils.h" +#include "nsIObserver.h" +#include "nsIObserverService.h" +#include "nsIThreadManager.h" +#include "nsThreadPool.h" + +namespace mozilla { + +// Created and destroyed on the main thread. +static StaticAutoPtr sMonitor; + +// Hashtable, maps thread pool name to SharedThreadPool instance. +// Modified only on the main thread. +static StaticAutoPtr> sPools; + +static already_AddRefed CreateThreadPool(const nsCString& aName); + +class SharedThreadPoolShutdownObserver : public nsIObserver { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSIOBSERVER + protected: + virtual ~SharedThreadPoolShutdownObserver() = default; +}; + +NS_IMPL_ISUPPORTS(SharedThreadPoolShutdownObserver, nsIObserver, nsISupports) + +NS_IMETHODIMP +SharedThreadPoolShutdownObserver::Observe(nsISupports* aSubject, + const char* aTopic, + const char16_t* aData) { + MOZ_RELEASE_ASSERT(!strcmp(aTopic, "xpcom-shutdown-threads")); +#ifdef EARLY_BETA_OR_EARLIER + { + ReentrantMonitorAutoEnter mon(*sMonitor); + if (!sPools->IsEmpty()) { + nsAutoCString str; + for (const auto& key : sPools->Keys()) { + str.AppendPrintf("\"%s\" ", nsAutoCString(key).get()); + } + printf_stderr( + "SharedThreadPool in xpcom-shutdown-threads. Waiting for " + "pools %s\n", + str.get()); + } + } +#endif + SharedThreadPool::SpinUntilEmpty(); + sMonitor = nullptr; + sPools = nullptr; + return NS_OK; +} + +void SharedThreadPool::InitStatics() { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(!sMonitor && !sPools); + sMonitor = new ReentrantMonitor("SharedThreadPool"); + sPools = new nsTHashMap(); + nsCOMPtr obsService = + mozilla::services::GetObserverService(); + nsCOMPtr obs = new SharedThreadPoolShutdownObserver(); + obsService->AddObserver(obs, "xpcom-shutdown-threads", false); +} + +/* static */ +bool SharedThreadPool::IsEmpty() { + ReentrantMonitorAutoEnter mon(*sMonitor); + return !sPools->Count(); +} + +/* static */ +void SharedThreadPool::SpinUntilEmpty() { + MOZ_ASSERT(NS_IsMainThread()); + SpinEventLoopUntil("SharedThreadPool::SpinUntilEmpty"_ns, []() -> bool { + sMonitor->AssertNotCurrentThreadIn(); + return IsEmpty(); + }); +} + +already_AddRefed SharedThreadPool::Get( + const nsCString& aName, uint32_t aThreadLimit) { + MOZ_ASSERT(sMonitor && sPools); + ReentrantMonitorAutoEnter mon(*sMonitor); + RefPtr pool; + + return sPools->WithEntryHandle( + aName, [&](auto&& entry) -> already_AddRefed { + if (entry) { + pool = entry.Data(); + if (NS_FAILED(pool->EnsureThreadLimitIsAtLeast(aThreadLimit))) { + NS_WARNING("Failed to set limits on thread pool"); + } + } else { + nsCOMPtr threadPool(CreateThreadPool(aName)); + if (NS_WARN_IF(!threadPool)) { + sPools->Remove(aName); // XXX entry.Remove() + return nullptr; + } + pool = new SharedThreadPool(aName, threadPool); + + // Set the thread and idle limits. Note that we don't rely on the + // EnsureThreadLimitIsAtLeast() call below, as the default thread + // limit is 4, and if aThreadLimit is less than 4 we'll end up with a + // pool with 4 threads rather than what we expected; so we'll have + // unexpected behaviour. + nsresult rv = pool->SetThreadLimit(aThreadLimit); + if (NS_WARN_IF(NS_FAILED(rv))) { + sPools->Remove(aName); // XXX entry.Remove() + return nullptr; + } + + rv = pool->SetIdleThreadLimit(aThreadLimit); + if (NS_WARN_IF(NS_FAILED(rv))) { + sPools->Remove(aName); // XXX entry.Remove() + return nullptr; + } + + entry.Insert(pool.get()); + } + + return pool.forget(); + }); +} + +NS_IMETHODIMP_(MozExternalRefCountType) SharedThreadPool::AddRef(void) { + MOZ_ASSERT(sMonitor); + ReentrantMonitorAutoEnter mon(*sMonitor); + MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); + nsrefcnt count = ++mRefCnt; + NS_LOG_ADDREF(this, count, "SharedThreadPool", sizeof(*this)); + return count; +} + +NS_IMETHODIMP_(MozExternalRefCountType) SharedThreadPool::Release(void) { + MOZ_ASSERT(sMonitor); + ReentrantMonitorAutoEnter mon(*sMonitor); + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "SharedThreadPool"); + if (count) { + return count; + } + + // Remove SharedThreadPool from table of pools. + sPools->Remove(mName); + MOZ_ASSERT(!sPools->Get(mName)); + + // Dispatch an event to the main thread to call Shutdown() on + // the nsIThreadPool. The Runnable here will add a refcount to the pool, + // and when the Runnable releases the nsIThreadPool it will be deleted. + NS_DispatchToMainThread(NewRunnableMethod("nsIThreadPool::Shutdown", mPool, + &nsIThreadPool::Shutdown)); + + // Stabilize refcount, so that if something in the dtor QIs, it won't explode. + mRefCnt = 1; + delete this; + return 0; +} + +NS_IMPL_QUERY_INTERFACE(SharedThreadPool, nsIThreadPool, nsIEventTarget) + +SharedThreadPool::SharedThreadPool(const nsCString& aName, nsIThreadPool* aPool) + : mName(aName), mPool(aPool), mRefCnt(0) {} + +SharedThreadPool::~SharedThreadPool() = default; + +nsresult SharedThreadPool::EnsureThreadLimitIsAtLeast(uint32_t aLimit) { + // We limit the number of threads that we use. Note that we + // set the thread limit to the same as the idle limit so that we're not + // constantly creating and destroying threads (see Bug 881954). When the + // thread pool threads shutdown they dispatch an event to the main thread + // to call nsIThread::Shutdown(), and if we're very busy that can take a + // while to run, and we end up with dozens of extra threads. Note that + // threads that are idle for 60 seconds are shutdown naturally. + uint32_t existingLimit = 0; + nsresult rv; + + rv = mPool->GetThreadLimit(&existingLimit); + NS_ENSURE_SUCCESS(rv, rv); + if (aLimit > existingLimit) { + rv = mPool->SetThreadLimit(aLimit); + NS_ENSURE_SUCCESS(rv, rv); + } + + rv = mPool->GetIdleThreadLimit(&existingLimit); + NS_ENSURE_SUCCESS(rv, rv); + if (aLimit > existingLimit) { + rv = mPool->SetIdleThreadLimit(aLimit); + NS_ENSURE_SUCCESS(rv, rv); + } + + return NS_OK; +} + +static already_AddRefed CreateThreadPool( + const nsCString& aName) { + nsCOMPtr pool = new nsThreadPool(); + + nsresult rv = pool->SetName(aName); + NS_ENSURE_SUCCESS(rv, nullptr); + + rv = pool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize); + NS_ENSURE_SUCCESS(rv, nullptr); + + return pool.forget(); +} + +} // namespace mozilla diff --git a/xpcom/threads/SharedThreadPool.h b/xpcom/threads/SharedThreadPool.h new file mode 100644 index 0000000000..f2c7068051 --- /dev/null +++ b/xpcom/threads/SharedThreadPool.h @@ -0,0 +1,132 @@ +/* -*- 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 SharedThreadPool_h_ +#define SharedThreadPool_h_ + +#include +#include +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/RefCountType.h" +#include "nsCOMPtr.h" +#include "nsID.h" +#include "nsIThreadPool.h" +#include "nsString.h" +#include "nscore.h" + +class nsIRunnable; + +namespace mozilla { + +// Wrapper that makes an nsIThreadPool a singleton, and provides a +// consistent threadsafe interface to get instances. Callers simply get a +// SharedThreadPool by the name of its nsIThreadPool. All get requests of +// the same name get the same SharedThreadPool. Users must store a reference +// to the pool, and when the last reference to a SharedThreadPool is dropped +// the pool is shutdown and deleted. Users aren't required to manually +// shutdown the pool, and can release references on any thread. This can make +// it significantly easier to use thread pools, because the caller doesn't need +// to worry about joining and tearing it down. +// +// On Windows all threads in the pool have MSCOM initialized with +// COINIT_MULTITHREADED. Note that not all users of MSCOM use this mode see [1], +// and mixing MSCOM objects between the two is terrible for performance, and can +// cause some functions to fail. So be careful when using Win32 APIs on a +// SharedThreadPool, and avoid sharing objects if at all possible. +// +// [1] +// https://searchfox.org/mozilla-central/search?q=coinitialize&redirect=false +class SharedThreadPool : public nsIThreadPool { + public: + // Gets (possibly creating) the shared thread pool singleton instance with + // thread pool named aName. + static already_AddRefed Get(const nsCString& aName, + uint32_t aThreadLimit = 4); + + // We implement custom threadsafe AddRef/Release pair, that destroys the + // the shared pool singleton when the refcount drops to 0. The addref/release + // are implemented using locking, so it's not recommended that you use them + // in a tight loop. + NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override; + NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override; + NS_IMETHOD_(MozExternalRefCountType) Release(void) override; + using HasThreadSafeRefCnt = std::true_type; + + // Forward behaviour to wrapped thread pool implementation. + NS_FORWARD_SAFE_NSITHREADPOOL(mPool); + + // Call this when dispatching from an event on the same + // threadpool that is about to complete. We should not create a new thread + // in that case since a thread is about to become idle. + nsresult DispatchFromEndOfTaskInThisPool(nsIRunnable* event) { + return Dispatch(event, NS_DISPATCH_AT_END); + } + + NS_IMETHOD DispatchFromScript(nsIRunnable* event, uint32_t flags) override { + return Dispatch(event, flags); + } + + NS_IMETHOD Dispatch(already_AddRefed event, + uint32_t flags = NS_DISPATCH_NORMAL) override { + return !mPool ? NS_ERROR_NULL_POINTER + : mPool->Dispatch(std::move(event), flags); + } + + NS_IMETHOD DelayedDispatch(already_AddRefed, uint32_t) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + using nsIEventTarget::Dispatch; + + NS_IMETHOD RegisterShutdownTask(nsITargetShutdownTask* task) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD UnregisterShutdownTask(nsITargetShutdownTask* task) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD IsOnCurrentThread(bool* _retval) override { + return !mPool ? NS_ERROR_NULL_POINTER : mPool->IsOnCurrentThread(_retval); + } + + NS_IMETHOD_(bool) IsOnCurrentThreadInfallible() override { + return mPool && mPool->IsOnCurrentThread(); + } + + // Creates necessary statics. Called once at startup. + static void InitStatics(); + + // Spins the event loop until all thread pools are shutdown. + // *Must* be called on the main thread. + static void SpinUntilEmpty(); + + private: + // Returns whether there are no pools in existence at the moment. + static bool IsEmpty(); + + // Creates a singleton SharedThreadPool wrapper around aPool. + // aName is the name of the aPool, and is used to lookup the + // SharedThreadPool in the hash table of all created pools. + SharedThreadPool(const nsCString& aName, nsIThreadPool* aPool); + virtual ~SharedThreadPool(); + + nsresult EnsureThreadLimitIsAtLeast(uint32_t aThreadLimit); + + // Name of mPool. + const nsCString mName; + + // Thread pool being wrapped. + nsCOMPtr mPool; + + // Refcount. We implement custom ref counting so that the thread pool is + // shutdown in a threadsafe manner and singletonness is preserved. + nsrefcnt mRefCnt; +}; + +} // namespace mozilla + +#endif // SharedThreadPool_h_ diff --git a/xpcom/threads/SpinEventLoopUntil.h b/xpcom/threads/SpinEventLoopUntil.h new file mode 100644 index 0000000000..a281177268 --- /dev/null +++ b/xpcom/threads/SpinEventLoopUntil.h @@ -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/. */ + +#ifndef xpcom_threads_SpinEventLoopUntil_h__ +#define xpcom_threads_SpinEventLoopUntil_h__ + +#include "MainThreadUtils.h" +#include "mozilla/Maybe.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/StaticMutex.h" +#include "nsString.h" +#include "nsThreadUtils.h" +#include "xpcpublic.h" + +class nsIThread; + +// A wrapper for nested event loops. +// +// This function is intended to make code more obvious (do you remember +// what NS_ProcessNextEvent(nullptr, true) means?) and slightly more +// efficient, as people often pass nullptr or NS_GetCurrentThread to +// NS_ProcessNextEvent, which results in needless querying of the current +// thread every time through the loop. +// +// You should use this function in preference to NS_ProcessNextEvent inside +// a loop unless one of the following is true: +// +// * You need to pass `false` to NS_ProcessNextEvent; or +// * You need to do unusual things around the call to NS_ProcessNextEvent, +// such as unlocking mutexes that you are holding. +// +// If you *do* need to call NS_ProcessNextEvent manually, please do call +// NS_GetCurrentThread() outside of your loop and pass the returned pointer +// into NS_ProcessNextEvent for a tiny efficiency win. +namespace mozilla { + +// You should normally not need to deal with this template parameter. If +// you enjoy esoteric event loop details, read on. +// +// If you specify that NS_ProcessNextEvent wait for an event, it is possible +// for NS_ProcessNextEvent to return false, i.e. to indicate that an event +// was not processed. This can only happen when the thread has been shut +// down by another thread, but is still attempting to process events outside +// of a nested event loop. +// +// This behavior is admittedly strange. The scenario it deals with is the +// following: +// +// * The current thread has been shut down by some owner thread. +// * The current thread is spinning an event loop waiting for some condition +// to become true. +// * Said condition is actually being fulfilled by another thread, so there +// are timing issues in play. +// +// Thus, there is a small window where the current thread's event loop +// spinning can check the condition, find it false, and call +// NS_ProcessNextEvent to wait for another event. But we don't actually +// want it to wait indefinitely, because there might not be any other events +// in the event loop, and the current thread can't accept dispatched events +// because it's being shut down. Thus, actually blocking would hang the +// thread, which is bad. The solution, then, is to detect such a scenario +// and not actually block inside NS_ProcessNextEvent. +// +// But this is a problem, because we want to return the status of +// NS_ProcessNextEvent to the caller of SpinEventLoopUntil if possible. In +// the above scenario, however, we'd stop spinning prematurely and cause +// all sorts of havoc. We therefore have this template parameter to +// control whether errors are ignored or passed out to the caller of +// SpinEventLoopUntil. The latter is the default; if you find yourself +// wanting to use the former, you should think long and hard before doing +// so, and write a comment like this defending your choice. + +enum class ProcessFailureBehavior { + IgnoreAndContinue, + ReportToCaller, +}; + +// SpinEventLoopUntil is a dangerous operation that can result in hangs. +// In particular during shutdown we want to know if we are hanging +// inside a nested event loop on the main thread. +// This is a helper annotation class to keep track of this. +struct MOZ_STACK_CLASS AutoNestedEventLoopAnnotation { + explicit AutoNestedEventLoopAnnotation(const nsACString& aEntry) + : mPrev(nullptr) { + if (NS_IsMainThread()) { + StaticMutexAutoLock lock(sStackMutex); + mPrev = sCurrent; + sCurrent = this; + if (mPrev) { + mStack = mPrev->mStack + "|"_ns + aEntry; + } else { + mStack = aEntry; + } + AnnotateXPCOMSpinEventLoopStack(mStack); + } + } + + ~AutoNestedEventLoopAnnotation() { + if (NS_IsMainThread()) { + StaticMutexAutoLock lock(sStackMutex); + MOZ_ASSERT(sCurrent == this); + sCurrent = mPrev; + if (mPrev) { + AnnotateXPCOMSpinEventLoopStack(mPrev->mStack); + } else { + AnnotateXPCOMSpinEventLoopStack(""_ns); + } + } + } + + static void CopyCurrentStack(nsCString& aNestedSpinStack) { + // We need to copy this behind a mutex as the + // memory for our instances is stack-bound and + // can go away at any time. + StaticMutexAutoLock lock(sStackMutex); + if (sCurrent) { + aNestedSpinStack = sCurrent->mStack; + } else { + aNestedSpinStack = "(no nested event loop active)"_ns; + } + } + + private: + AutoNestedEventLoopAnnotation(const AutoNestedEventLoopAnnotation&) = delete; + AutoNestedEventLoopAnnotation& operator=( + const AutoNestedEventLoopAnnotation&) = delete; + + // The declarations of these statics live in nsThreadManager.cpp. + static AutoNestedEventLoopAnnotation* sCurrent MOZ_GUARDED_BY(sStackMutex); + static StaticMutex sStackMutex; + + // We need this to avoid the inclusion of nsExceptionHandler.h here + // which can include windows.h which disturbs some dom/media/gtest. + // The implementation lives in nsThreadManager.cpp. + static void AnnotateXPCOMSpinEventLoopStack(const nsACString& aStack); + + AutoNestedEventLoopAnnotation* mPrev MOZ_GUARDED_BY(sStackMutex); + nsCString mStack MOZ_GUARDED_BY(sStackMutex); +}; + +// Please see the above notes for the Behavior template parameter. +// +// aVeryGoodReasonToDoThis is usually a literal string unique to each +// caller that can be recognized in the XPCOMSpinEventLoopStack +// annotation. +// aPredicate is the condition we wait for. +// aThread can be used to specify a thread, see the above introduction. +// It defaults to the current thread. +template < + ProcessFailureBehavior Behavior = ProcessFailureBehavior::ReportToCaller, + typename Pred> +bool SpinEventLoopUntil(const nsACString& aVeryGoodReasonToDoThis, + Pred&& aPredicate, nsIThread* aThread = nullptr) { + // Prepare the annotations + AutoNestedEventLoopAnnotation annotation(aVeryGoodReasonToDoThis); + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE( + "SpinEventLoopUntil", OTHER, aVeryGoodReasonToDoThis); + AUTO_PROFILER_MARKER_TEXT("SpinEventLoop", OTHER, MarkerStack::Capture(), + aVeryGoodReasonToDoThis); + + nsIThread* thread = aThread ? aThread : NS_GetCurrentThread(); + + // From a latency perspective, spinning the event loop is like leaving script + // and returning to the event loop. Tell the watchdog we stopped running + // script (until we return). + mozilla::Maybe asa; + if (NS_IsMainThread()) { + asa.emplace(false); + } + + while (!aPredicate()) { + bool didSomething = NS_ProcessNextEvent(thread, true); + + if (Behavior == ProcessFailureBehavior::IgnoreAndContinue) { + // Don't care what happened, continue on. + continue; + } else if (!didSomething) { + return false; + } + } + + return true; +} + +} // namespace mozilla + +#endif // xpcom_threads_SpinEventLoopUntil_h__ diff --git a/xpcom/threads/StateMirroring.h b/xpcom/threads/StateMirroring.h new file mode 100644 index 0000000000..9f8ded70f4 --- /dev/null +++ b/xpcom/threads/StateMirroring.h @@ -0,0 +1,453 @@ +/* -*- 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/. */ + +#if !defined(StateMirroring_h_) +# define StateMirroring_h_ + +# include +# include "mozilla/AbstractThread.h" +# include "mozilla/AlreadyAddRefed.h" +# include "mozilla/Assertions.h" +# include "mozilla/Logging.h" +# include "mozilla/Maybe.h" +# include "mozilla/RefPtr.h" +# include "mozilla/StateWatching.h" +# include "nsCOMPtr.h" +# include "nsIRunnable.h" +# include "nsISupports.h" +# include "nsTArray.h" +# include "nsThreadUtils.h" + +/* + * The state-mirroring machinery allows pieces of interesting state to be + * observed on multiple thread without locking. The basic strategy is to track + * changes in a canonical value and post updates to other threads that hold + * mirrors for that value. + * + * One problem with the naive implementation of such a system is that some + * pieces of state need to be updated atomically, and certain other operations + * need to wait for these atomic updates to complete before executing. The + * state-mirroring machinery solves this problem by requiring that its owner + * thread uses tail dispatch, and posting state update events (which should + * always be run first by TaskDispatcher implementations) to that tail + * dispatcher. This ensures that state changes are always atomic from the + * perspective of observing threads. + * + * Given that state-mirroring is an automatic background process, we try to + * avoid burdening the caller with worrying too much about teardown. To that + * end, we don't assert dispatch success for any of the notifications, and + * assume that any canonical or mirror owned by a thread for whom dispatch fails + * will soon be disconnected by its holder anyway. + * + * Given that semantics may change and comments tend to go out of date, we + * deliberately don't provide usage examples here. Grep around to find them. + */ + +namespace mozilla { + +// Mirror and Canonical inherit WatchTarget, so we piggy-back on the +// logging that WatchTarget already does. Given that, it makes sense to share +// the same log module. +# define MIRROR_LOG(x, ...) \ + MOZ_ASSERT(gStateWatchingLog); \ + MOZ_LOG(gStateWatchingLog, LogLevel::Debug, (x, ##__VA_ARGS__)) + +template +class AbstractMirror; + +/* + * AbstractCanonical is a superclass from which all Canonical values must + * inherit. It serves as the interface of operations which may be performed (via + * asynchronous dispatch) by other threads, in particular by the corresponding + * Mirror value. + */ +template +class AbstractCanonical { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AbstractCanonical) + AbstractCanonical(AbstractThread* aThread) : mOwnerThread(aThread) {} + virtual void AddMirror(AbstractMirror* aMirror) = 0; + virtual void RemoveMirror(AbstractMirror* aMirror) = 0; + + AbstractThread* OwnerThread() const { return mOwnerThread; } + + protected: + virtual ~AbstractCanonical() {} + RefPtr mOwnerThread; +}; + +/* + * AbstractMirror is a superclass from which all Mirror values must + * inherit. It serves as the interface of operations which may be performed (via + * asynchronous dispatch) by other threads, in particular by the corresponding + * Canonical value. + */ +template +class AbstractMirror { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AbstractMirror) + AbstractMirror(AbstractThread* aThread) : mOwnerThread(aThread) {} + virtual void ConnectedOnCanonicalThread(AbstractCanonical* aCanonical) = 0; + virtual void UpdateValue(const T& aNewValue) = 0; + virtual void NotifyDisconnected() = 0; + + AbstractThread* OwnerThread() const { return mOwnerThread; } + + protected: + virtual ~AbstractMirror() {} + RefPtr mOwnerThread; +}; + +/* + * Canonical is a wrapper class that allows a given value to be mirrored by + * other threads. It maintains a list of active mirrors, and queues updates for + * them when the internal value changes. When changing the value, the caller + * needs to pass a TaskDispatcher object, which fires the updates at the + * appropriate time. Canonical is also a WatchTarget, and may be set up to + * trigger other routines (on the same thread) when the canonical value changes. + * + * Canonical is intended to be used as a member variable, so it doesn't + * actually inherit AbstractCanonical (a refcounted type). Rather, it + * contains an inner class called |Impl| that implements most of the interesting + * logic. + */ +template +class Canonical { + public: + Canonical(AbstractThread* aThread, const T& aInitialValue, + const char* aName) { + mImpl = new Impl(aThread, aInitialValue, aName); + } + + ~Canonical() {} + + private: + class Impl : public AbstractCanonical, public WatchTarget { + public: + using AbstractCanonical::OwnerThread; + + Impl(AbstractThread* aThread, const T& aInitialValue, const char* aName) + : AbstractCanonical(aThread), + WatchTarget(aName), + mValue(aInitialValue) { + MIRROR_LOG("%s [%p] initialized", mName, this); + MOZ_ASSERT(aThread->SupportsTailDispatch(), + "Can't get coherency without tail dispatch"); + } + + void ConnectMirror(AbstractMirror* aMirror) { + MIRROR_LOG("%s [%p] canonical-init connecting mirror %p", mName, this, + aMirror); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(OwnerThread()->RequiresTailDispatch(aMirror->OwnerThread()), + "Can't get coherency without tail dispatch"); + aMirror->ConnectedOnCanonicalThread(this); + AddMirror(aMirror); + } + + void AddMirror(AbstractMirror* aMirror) override { + MIRROR_LOG("%s [%p] adding mirror %p", mName, this, aMirror); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(!mMirrors.Contains(aMirror)); + mMirrors.AppendElement(aMirror); + aMirror->OwnerThread()->DispatchStateChange(MakeNotifier(aMirror)); + } + + void RemoveMirror(AbstractMirror* aMirror) override { + MIRROR_LOG("%s [%p] removing mirror %p", mName, this, aMirror); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(mMirrors.Contains(aMirror)); + mMirrors.RemoveElement(aMirror); + } + + void DisconnectAll() { + MIRROR_LOG("%s [%p] Disconnecting all mirrors", mName, this); + for (size_t i = 0; i < mMirrors.Length(); ++i) { + mMirrors[i]->OwnerThread()->Dispatch( + NewRunnableMethod("AbstractMirror::NotifyDisconnected", mMirrors[i], + &AbstractMirror::NotifyDisconnected)); + } + mMirrors.Clear(); + } + + operator const T&() { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + return mValue; + } + + void Set(const T& aNewValue) { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + + if (aNewValue == mValue) { + return; + } + + // Notify same-thread watchers. The state watching machinery will make + // sure that notifications run at the right time. + NotifyWatchers(); + + // Check if we've already got a pending update. If so we won't schedule + // another one. + bool alreadyNotifying = mInitialValue.isSome(); + + // Stash the initial value if needed, then update to the new value. + if (mInitialValue.isNothing()) { + mInitialValue.emplace(mValue); + } + mValue = aNewValue; + + // We wait until things have stablized before sending state updates so + // that we can avoid sending multiple updates, and possibly avoid sending + // any updates at all if the value ends up where it started. + if (!alreadyNotifying) { + AbstractThread::DispatchDirectTask(NewRunnableMethod( + "Canonical::Impl::DoNotify", this, &Impl::DoNotify)); + } + } + + Impl& operator=(const T& aNewValue) { + Set(aNewValue); + return *this; + } + Impl& operator=(const Impl& aOther) { + Set(aOther); + return *this; + } + Impl(const Impl& aOther) = delete; + + protected: + ~Impl() { MOZ_DIAGNOSTIC_ASSERT(mMirrors.IsEmpty()); } + + private: + void DoNotify() { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(mInitialValue.isSome()); + bool same = mInitialValue.ref() == mValue; + mInitialValue.reset(); + + if (same) { + MIRROR_LOG("%s [%p] unchanged - not sending update", mName, this); + return; + } + + for (size_t i = 0; i < mMirrors.Length(); ++i) { + mMirrors[i]->OwnerThread()->DispatchStateChange( + MakeNotifier(mMirrors[i])); + } + } + + already_AddRefed MakeNotifier(AbstractMirror* aMirror) { + return NewRunnableMethod("AbstractMirror::UpdateValue", aMirror, + &AbstractMirror::UpdateValue, mValue); + } + + T mValue; + Maybe mInitialValue; + nsTArray>> mMirrors; + }; + + public: + /* + * Connect this Canonical to aMirror. Note that the canonical value starts + * being mirrored to aMirror immediately, and requires one thread hop to + * aMirror's owning thread before the connection is established. + * + * Note that this could race with Mirror::DisconnectIfConnected(). It is up to + * the caller to provide the guarantee that disconnection happens after the + * connection has been established. There is no race between this and + * DisconnectAll(). + */ + void ConnectMirror(AbstractMirror* aMirror) { + return mImpl->ConnectMirror(aMirror); + } + void DisconnectAll() { return mImpl->DisconnectAll(); } + + // Access to the Impl. + operator Impl&() { return *mImpl; } + Impl* operator&() { return mImpl; } + + // Access to the T. + const T& Ref() const { return *mImpl; } + operator const T&() const { return Ref(); } + void Set(const T& aNewValue) { mImpl->Set(aNewValue); } + Canonical& operator=(const T& aNewValue) { + Set(aNewValue); + return *this; + } + Canonical& operator=(const Canonical& aOther) { + Set(aOther); + return *this; + } + Canonical(const Canonical& aOther) = delete; + + private: + RefPtr mImpl; +}; + +/* + * Mirror is a wrapper class that allows a given value to mirror that of a + * Canonical owned by another thread. It registers itself with a + * Canonical, and is periodically updated with new values. Mirror is also + * a WatchTarget, and may be set up to trigger other routines (on the same + * thread) when the mirrored value changes. + * + * Mirror is intended to be used as a member variable, so it doesn't actually + * inherit AbstractMirror (a refcounted type). Rather, it contains an inner + * class called |Impl| that implements most of the interesting logic. + */ +template +class Mirror { + public: + Mirror(AbstractThread* aThread, const T& aInitialValue, const char* aName) { + mImpl = new Impl(aThread, aInitialValue, aName); + } + + ~Mirror() { + // As a member of complex objects, a Mirror may be destroyed on a + // different thread than its owner, or late in shutdown during CC. Given + // that, we require manual disconnection so that callers can put things in + // the right place. + MOZ_DIAGNOSTIC_ASSERT(!mImpl->IsConnected()); + mImpl->AssertNoIncomingConnects(); + } + + private: + class Impl : public AbstractMirror, public WatchTarget { + public: + using AbstractMirror::OwnerThread; + + Impl(AbstractThread* aThread, const T& aInitialValue, const char* aName) + : AbstractMirror(aThread), + WatchTarget(aName), + mValue(aInitialValue) { + MIRROR_LOG("%s [%p] initialized", mName, this); + MOZ_ASSERT(aThread->SupportsTailDispatch(), + "Can't get coherency without tail dispatch"); + } + + operator const T&() { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + return mValue; + } + + void ConnectedOnCanonicalThread(AbstractCanonical* aCanonical) override { + MOZ_ASSERT(aCanonical->OwnerThread()->IsCurrentThreadIn()); +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + ++mIncomingConnects; +# endif + OwnerThread()->DispatchStateChange( + NewRunnableMethod>>( + "Mirror::Impl::SetCanonical", this, &Impl::SetCanonical, + aCanonical)); + } + + void SetCanonical(AbstractCanonical* aCanonical) { + MIRROR_LOG("%s [%p] Canonical-init setting canonical %p", mName, this, + aCanonical); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(!IsConnected()); +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + --mIncomingConnects; +# endif + mCanonical = aCanonical; + } + + void UpdateValue(const T& aNewValue) override { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + if (mValue != aNewValue) { + mValue = aNewValue; + WatchTarget::NotifyWatchers(); + } + } + + void NotifyDisconnected() override { + MIRROR_LOG("%s [%p] Notifed of disconnection from %p", mName, this, + mCanonical.get()); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + mCanonical = nullptr; + } + + bool IsConnected() const { return !!mCanonical; } + + void Connect(AbstractCanonical* aCanonical) { + MIRROR_LOG("%s [%p] Connecting to %p", mName, this, aCanonical); + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + MOZ_ASSERT(!IsConnected()); + MOZ_ASSERT(OwnerThread()->RequiresTailDispatch(aCanonical->OwnerThread()), + "Can't get coherency without tail dispatch"); + + nsCOMPtr r = + NewRunnableMethod>>( + "AbstractCanonical::AddMirror", aCanonical, + &AbstractCanonical::AddMirror, this); + aCanonical->OwnerThread()->Dispatch(r.forget()); + mCanonical = aCanonical; + } + + void DisconnectIfConnected() { + MOZ_ASSERT(OwnerThread()->IsCurrentThreadIn()); + if (!IsConnected()) { + return; + } + + MIRROR_LOG("%s [%p] Disconnecting from %p", mName, this, + mCanonical.get()); + nsCOMPtr r = + NewRunnableMethod>>( + "AbstractCanonical::RemoveMirror", mCanonical, + &AbstractCanonical::RemoveMirror, this); + mCanonical->OwnerThread()->Dispatch(r.forget()); + mCanonical = nullptr; + } + + void AssertNoIncomingConnects() { + MOZ_DIAGNOSTIC_ASSERT(mIncomingConnects == 0); + } + + protected: + ~Impl() { MOZ_DIAGNOSTIC_ASSERT(!IsConnected()); } + + private: + T mValue; + RefPtr> mCanonical; +# ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + std::atomic mIncomingConnects = 0; +# endif + }; + + public: + // Forward control operations to the Impl. + /* + * Connect aCanonical to this Mirror. Note that this requires one thread hop + * back to aCanonical's owning thread before the canonical value starts + * being mirrored, and another to our owning thread before the connection is + * established. + * + * Note that this mirror-initialized connection could race with + * Canonical::DisconnectAll(). It is up to the caller to provide the guarantee + * that disconnection happens after the connection has been established. There + * is no race between this and DisconnectIfConnected(). + */ + void Connect(AbstractCanonical* aCanonical) { mImpl->Connect(aCanonical); } + void DisconnectIfConnected() { mImpl->DisconnectIfConnected(); } + + // Access to the Impl. + operator Impl&() { return *mImpl; } + Impl* operator&() { return mImpl; } + + // Access to the T. + const T& Ref() const { return *mImpl; } + operator const T&() const { return Ref(); } + + private: + RefPtr mImpl; +}; + +# undef MIRROR_LOG + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/StateWatching.h b/xpcom/threads/StateWatching.h new file mode 100644 index 0000000000..3da0c63bfe --- /dev/null +++ b/xpcom/threads/StateWatching.h @@ -0,0 +1,302 @@ +/* -*- 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/. */ + +#if !defined(StateWatching_h_) +# define StateWatching_h_ + +# include +# include +# include +# include "mozilla/AbstractThread.h" +# include "mozilla/Assertions.h" +# include "mozilla/Logging.h" +# include "mozilla/RefPtr.h" +# include "nsISupports.h" +# include "nsTArray.h" +# include "nsThreadUtils.h" + +/* + * The state-watching machinery automates the process of responding to changes + * in various pieces of state. + * + * A standard programming pattern is as follows: + * + * mFoo = ...; + * NotifyStuffChanged(); + * ... + * mBar = ...; + * NotifyStuffChanged(); + * + * This pattern is error-prone and difficult to audit because it requires the + * programmer to manually trigger the update routine. This can be especially + * problematic when the update routine depends on numerous pieces of state, and + * when that state is modified across a variety of helper methods. In these + * cases the responsibility for invoking the routine is often unclear, causing + * developers to scatter calls to it like pixie dust. This can result in + * duplicate invocations (which is wasteful) and missing invocations in corner- + * cases (which is a source of bugs). + * + * This file provides a set of primitives that automatically handle updates and + * allow the programmers to explicitly construct a graph of state dependencies. + * When used correctly, it eliminates the guess-work and wasted cycles described + * above. + * + * There are two basic pieces: + * (1) Objects that can be watched for updates. These inherit WatchTarget. + * (2) Objects that receive objects and trigger processing. These inherit + * AbstractWatcher. In the current machinery, these exist only internally + * within the WatchManager, though that could change. + * + * Note that none of this machinery is thread-safe - it must all happen on the + * same owning thread. To solve multi-threaded use-cases, use state mirroring + * and watch the mirrored value. + * + * Given that semantics may change and comments tend to go out of date, we + * deliberately don't provide usage examples here. Grep around to find them. + */ + +namespace mozilla { + +extern LazyLogModule gStateWatchingLog; + +# define WATCH_LOG(x, ...) \ + MOZ_LOG(gStateWatchingLog, LogLevel::Debug, (x, ##__VA_ARGS__)) + +/* + * AbstractWatcher is a superclass from which all watchers must inherit. + */ +class AbstractWatcher { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AbstractWatcher) + AbstractWatcher() : mDestroyed(false) {} + bool IsDestroyed() { return mDestroyed; } + virtual void Notify() = 0; + + protected: + virtual ~AbstractWatcher() { MOZ_ASSERT(mDestroyed); } + bool mDestroyed; +}; + +/* + * WatchTarget is a superclass from which all watchable things must inherit. + * Unlike AbstractWatcher, it is a fully-implemented Mix-in, and the subclass + * needs only to invoke NotifyWatchers when something changes. + * + * The functionality that this class provides is not threadsafe, and should only + * be used on the thread that owns that WatchTarget. + */ +class WatchTarget { + public: + explicit WatchTarget(const char* aName) : mName(aName) {} + + void AddWatcher(AbstractWatcher* aWatcher) { + MOZ_ASSERT(!mWatchers.Contains(aWatcher)); + mWatchers.AppendElement(aWatcher); + } + + void RemoveWatcher(AbstractWatcher* aWatcher) { + MOZ_ASSERT(mWatchers.Contains(aWatcher)); + mWatchers.RemoveElement(aWatcher); + } + + protected: + void NotifyWatchers() { + WATCH_LOG("%s[%p] notifying watchers\n", mName, this); + PruneWatchers(); + for (size_t i = 0; i < mWatchers.Length(); ++i) { + mWatchers[i]->Notify(); + } + } + + private: + // We don't have Watchers explicitly unregister themselves when they die, + // because then they'd need back-references to all the WatchTargets they're + // subscribed to, and WatchTargets aren't reference-counted. So instead we + // just prune dead ones at appropriate times, which works just fine. + void PruneWatchers() { + mWatchers.RemoveElementsBy( + [](const auto& watcher) { return watcher->IsDestroyed(); }); + } + + nsTArray> mWatchers; + + protected: + const char* mName; +}; + +/* + * Watchable is a wrapper class that turns any primitive into a WatchTarget. + */ +template +class Watchable : public WatchTarget { + public: + Watchable(const T& aInitialValue, const char* aName) + : WatchTarget(aName), mValue(aInitialValue) {} + + const T& Ref() const { return mValue; } + operator const T&() const { return Ref(); } + Watchable& operator=(const T& aNewValue) { + if (aNewValue != mValue) { + mValue = aNewValue; + NotifyWatchers(); + } + + return *this; + } + + private: + Watchable(const Watchable& aOther) = delete; + Watchable& operator=(const Watchable& aOther) = delete; + + T mValue; +}; + +// Manager class for state-watching. Declare one of these in any class for which +// you want to invoke method callbacks. +// +// Internally, WatchManager maintains one AbstractWatcher per callback method. +// Consumers invoke Watch/Unwatch on a particular (WatchTarget, Callback) tuple. +// This causes an AbstractWatcher for |Callback| to be instantiated if it +// doesn't already exist, and registers it with |WatchTarget|. +// +// Using Direct Tasks on the TailDispatcher, WatchManager ensures that we fire +// watch callbacks no more than once per task, once all other operations for +// that task have been completed. +// +// WatchManager is intended to be declared as a member of |OwnerType| +// objects. Given that, it and its owned objects can't hold permanent strong +// refs to the owner, since that would keep the owner alive indefinitely. +// Instead, it _only_ holds strong refs while waiting for Direct Tasks to fire. +// This ensures that everything is kept alive just long enough. +template +class WatchManager { + public: + typedef void (OwnerType::*CallbackMethod)(); + explicit WatchManager(OwnerType* aOwner, AbstractThread* aOwnerThread) + : mOwner(aOwner), mOwnerThread(aOwnerThread) {} + + ~WatchManager() { + if (!IsShutdown()) { + Shutdown(); + } + } + + bool IsShutdown() const { return !mOwner; } + + // Shutdown needs to happen on mOwnerThread. If the WatchManager will be + // destroyed on a different thread, Shutdown() must be called manually. + void Shutdown() { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + for (auto& watcher : mWatchers) { + watcher->Destroy(); + } + mWatchers.Clear(); + mOwner = nullptr; + } + + void Watch(WatchTarget& aTarget, CallbackMethod aMethod) { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + aTarget.AddWatcher(&EnsureWatcher(aMethod)); + } + + void Unwatch(WatchTarget& aTarget, CallbackMethod aMethod) { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + PerCallbackWatcher* watcher = GetWatcher(aMethod); + MOZ_ASSERT(watcher); + aTarget.RemoveWatcher(watcher); + } + + void ManualNotify(CallbackMethod aMethod) { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + PerCallbackWatcher* watcher = GetWatcher(aMethod); + MOZ_ASSERT(watcher); + watcher->Notify(); + } + + private: + class PerCallbackWatcher : public AbstractWatcher { + public: + PerCallbackWatcher(OwnerType* aOwner, AbstractThread* aOwnerThread, + CallbackMethod aMethod) + : mOwner(aOwner), + mOwnerThread(aOwnerThread), + mCallbackMethod(aMethod) {} + + void Destroy() { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + mDestroyed = true; + mOwner = nullptr; + } + + void Notify() override { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + MOZ_DIAGNOSTIC_ASSERT(mOwner, + "mOwner is only null after destruction, " + "at which point we shouldn't be notified"); + if (mNotificationPending) { + // We've already got a notification job in the pipe. + return; + } + mNotificationPending = true; + + // Queue up our notification jobs to run in a stable state. + AbstractThread::DispatchDirectTask( + NS_NewRunnableFunction("WatchManager::PerCallbackWatcher::Notify", + [self = RefPtr(this), + owner = RefPtr(mOwner)]() { + if (!self->mDestroyed) { + ((*owner).*(self->mCallbackMethod))(); + } + self->mNotificationPending = false; + })); + } + + bool CallbackMethodIs(CallbackMethod aMethod) const { + return mCallbackMethod == aMethod; + } + + private: + ~PerCallbackWatcher() = default; + + OwnerType* mOwner; // Never null. + bool mNotificationPending = false; + RefPtr mOwnerThread; + CallbackMethod mCallbackMethod; + }; + + PerCallbackWatcher* GetWatcher(CallbackMethod aMethod) { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + for (auto& watcher : mWatchers) { + if (watcher->CallbackMethodIs(aMethod)) { + return watcher; + } + } + return nullptr; + } + + PerCallbackWatcher& EnsureWatcher(CallbackMethod aMethod) { + MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn()); + PerCallbackWatcher* watcher = GetWatcher(aMethod); + if (watcher) { + return *watcher; + } + watcher = mWatchers + .AppendElement(MakeAndAddRef( + mOwner, mOwnerThread, aMethod)) + ->get(); + return *watcher; + } + + nsTArray> mWatchers; + OwnerType* mOwner; + RefPtr mOwnerThread; +}; + +# undef WATCH_LOG + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/SyncRunnable.h b/xpcom/threads/SyncRunnable.h new file mode 100644 index 0000000000..77f82ba313 --- /dev/null +++ b/xpcom/threads/SyncRunnable.h @@ -0,0 +1,157 @@ +/* -*- 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 mozilla_SyncRunnable_h +#define mozilla_SyncRunnable_h + +#include + +#include "mozilla/AbstractThread.h" +#include "mozilla/Monitor.h" +#include "mozilla/dom/JSExecutionManager.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +/** + * This class will wrap a nsIRunnable and dispatch it to the target thread + * synchronously. This is different from + * NS_DispatchAndSpinEventLoopUntilComplete: this class does not spin the event + * loop waiting for the event to be dispatched. This means that you don't risk + * reentrance from pending messages, but you must be sure that the target thread + * does not ever block on this thread, or else you will deadlock. + * + * Typical usage: + * RefPtr sr = new SyncRunnable(new myrunnable...()); + * sr->DispatchToThread(t); + * + * We also provide convenience wrappers: + * SyncRunnable::DispatchToThread(pThread, new myrunnable...()); + * SyncRunnable::DispatchToThread(pThread, NS_NewRunnableFunction(...)); + * + */ +class SyncRunnable : public Runnable { + public: + explicit SyncRunnable(nsIRunnable* aRunnable) + : Runnable("SyncRunnable"), + mRunnable(aRunnable), + mMonitor("SyncRunnable"), + mDone(false) {} + + explicit SyncRunnable(already_AddRefed aRunnable) + : Runnable("SyncRunnable"), + mRunnable(std::move(aRunnable)), + mMonitor("SyncRunnable"), + mDone(false) {} + + nsresult DispatchToThread(nsIEventTarget* aThread, + bool aForceDispatch = false) { + nsresult rv; + bool on; + + if (!aForceDispatch) { + rv = aThread->IsOnCurrentThread(&on); + MOZ_ASSERT(NS_SUCCEEDED(rv)); + if (NS_SUCCEEDED(rv) && on) { + mRunnable->Run(); + return NS_OK; + } + } + + rv = aThread->Dispatch(this, NS_DISPATCH_NORMAL); + if (NS_SUCCEEDED(rv)) { + mozilla::MonitorAutoLock lock(mMonitor); + // This could be synchronously dispatching to a thread currently waiting + // for JS execution clearance. Yield JS execution. + dom::AutoYieldJSThreadExecution yield; + + while (!mDone) { + lock.Wait(); + } + } + return rv; + } + + nsresult DispatchToThread(AbstractThread* aThread, + bool aForceDispatch = false) { + if (!aForceDispatch && aThread->IsCurrentThreadIn()) { + mRunnable->Run(); + return NS_OK; + } + + // Check we don't have tail dispatching here. Otherwise we will deadlock + // ourself when spinning the loop below. + MOZ_ASSERT(!aThread->RequiresTailDispatchFromCurrentThread()); + + nsresult rv = aThread->Dispatch(RefPtr(this).forget()); + if (NS_SUCCEEDED(rv)) { + mozilla::MonitorAutoLock lock(mMonitor); + while (!mDone) { + lock.Wait(); + } + } + return rv; + } + + static nsresult DispatchToThread(nsIEventTarget* aThread, + nsIRunnable* aRunnable, + bool aForceDispatch = false) { + RefPtr s(new SyncRunnable(aRunnable)); + return s->DispatchToThread(aThread, aForceDispatch); + } + + static nsresult DispatchToThread(AbstractThread* aThread, + nsIRunnable* aRunnable, + bool aForceDispatch = false) { + RefPtr s(new SyncRunnable(aRunnable)); + return s->DispatchToThread(aThread, aForceDispatch); + } + + static nsresult DispatchToThread(nsIEventTarget* aThread, + already_AddRefed aRunnable, + bool aForceDispatch = false) { + RefPtr s(new SyncRunnable(std::move(aRunnable))); + return s->DispatchToThread(aThread, aForceDispatch); + } + + static nsresult DispatchToThread(AbstractThread* aThread, + already_AddRefed aRunnable, + bool aForceDispatch = false) { + RefPtr s(new SyncRunnable(std::move(aRunnable))); + return s->DispatchToThread(aThread, aForceDispatch); + } + + // These deleted overloads prevent accidentally (if harmlessly) double- + // wrapping SyncRunnable, which was previously a common anti-pattern. + static nsresult DispatchToThread(nsIEventTarget* aThread, + SyncRunnable* aRunnable, + bool aForceDispatch = false) = delete; + static nsresult DispatchToThread(AbstractThread* aThread, + SyncRunnable* aRunnable, + bool aForceDispatch = false) = delete; + + protected: + NS_IMETHOD Run() override { + mRunnable->Run(); + + mozilla::MonitorAutoLock lock(mMonitor); + MOZ_ASSERT(!mDone); + + mDone = true; + mMonitor.Notify(); + + return NS_OK; + } + + private: + nsCOMPtr mRunnable; + mozilla::Monitor mMonitor; + bool mDone MOZ_GUARDED_BY(mMonitor); +}; + +} // namespace mozilla + +#endif // mozilla_SyncRunnable_h diff --git a/xpcom/threads/SynchronizedEventQueue.cpp b/xpcom/threads/SynchronizedEventQueue.cpp new file mode 100644 index 0000000000..59161b7f9d --- /dev/null +++ b/xpcom/threads/SynchronizedEventQueue.cpp @@ -0,0 +1,26 @@ +/* -*- 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 "SynchronizedEventQueue.h" +#include "nsIThreadInternal.h" + +using namespace mozilla; + +void SynchronizedEventQueue::AddObserver(nsIThreadObserver* aObserver) { + MOZ_ASSERT(aObserver); + MOZ_ASSERT(!mEventObservers.Contains(aObserver)); + mEventObservers.AppendElement(aObserver); +} + +void SynchronizedEventQueue::RemoveObserver(nsIThreadObserver* aObserver) { + MOZ_ASSERT(aObserver); + MOZ_ALWAYS_TRUE(mEventObservers.RemoveElement(aObserver)); +} + +const nsTObserverArray>& +SynchronizedEventQueue::EventObservers() { + return mEventObservers; +} diff --git a/xpcom/threads/SynchronizedEventQueue.h b/xpcom/threads/SynchronizedEventQueue.h new file mode 100644 index 0000000000..e4cf1a62af --- /dev/null +++ b/xpcom/threads/SynchronizedEventQueue.h @@ -0,0 +1,131 @@ +/* -*- 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 mozilla_SynchronizedEventQueue_h +#define mozilla_SynchronizedEventQueue_h + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/EventQueue.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "nsIThreadInternal.h" +#include "nsCOMPtr.h" +#include "nsTObserverArray.h" + +class nsIEventTarget; +class nsISerialEventTarget; +class nsIThreadObserver; + +namespace mozilla { + +// A SynchronizedEventQueue is an abstract class for event queues that can be +// used across threads. A SynchronizedEventQueue implementation will typically +// use locks and condition variables to guarantee consistency. The methods of +// SynchronizedEventQueue are split between ThreadTargetSink (which contains +// methods for posting events) and SynchronizedEventQueue (which contains +// methods for getting events). This split allows event targets (specifically +// ThreadEventTarget) to use a narrow interface, since they only need to post +// events. +// +// ThreadEventQueue is the canonical implementation of +// SynchronizedEventQueue. When Quantum DOM is implemented, we will use a +// different synchronized queue on the main thread, SchedulerEventQueue, which +// will handle the cooperative threading model. + +class ThreadTargetSink { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadTargetSink) + + virtual bool PutEvent(already_AddRefed&& aEvent, + EventQueuePriority aPriority) = 0; + + // After this method is called, no more events can be posted. + virtual void Disconnect(const MutexAutoLock& aProofOfLock) = 0; + + virtual nsresult RegisterShutdownTask(nsITargetShutdownTask* aTask) = 0; + virtual nsresult UnregisterShutdownTask(nsITargetShutdownTask* aTask) = 0; + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) { + return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf); + } + + // Not const because overrides may need to take a lock + virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) = 0; + + protected: + virtual ~ThreadTargetSink() = default; +}; + +class SynchronizedEventQueue : public ThreadTargetSink { + public: + virtual already_AddRefed GetEvent( + bool aMayWait, mozilla::TimeDuration* aLastEventDelay = nullptr) = 0; + virtual bool HasPendingEvent() = 0; + + // This method atomically checks if there are pending events and, if there are + // none, forbids future events from being posted. It returns true if there + // were no pending events. + virtual bool ShutdownIfNoPendingEvents() = 0; + + // These methods provide access to an nsIThreadObserver, whose methods are + // called when posting and processing events. SetObserver should only be + // called on the thread that processes events. GetObserver can be called from + // any thread. GetObserverOnThread must be used from the thread that processes + // events; it does not acquire a lock. + virtual already_AddRefed GetObserver() = 0; + virtual already_AddRefed GetObserverOnThread() = 0; + virtual void SetObserver(nsIThreadObserver* aObserver) = 0; + + void AddObserver(nsIThreadObserver* aObserver); + void RemoveObserver(nsIThreadObserver* aObserver); + const nsTObserverArray>& EventObservers(); + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) override { + // Normally we'd return + // mEventObservers.ShallowSizeOfExcludingThis(aMallocSizeOf); However, + // mEventObservers may be being mutated on another thread, and we don't lock + // around access, so locking here wouldn't help. They're small, so + return 0; + } + + /** + * This method causes any events currently enqueued on the thread to be + * suppressed until PopEventQueue is called, and any event dispatched to this + * thread's nsIEventTarget will queue as well. Calls to PushEventQueue may be + * nested and must each be paired with a call to PopEventQueue in order to + * restore the original state of the thread. The returned nsIEventTarget may + * be used to push events onto the nested queue. Dispatching will be disabled + * once the event queue is popped. The thread will only ever process pending + * events for the innermost event queue. Must only be called on the target + * thread. + */ + virtual already_AddRefed PushEventQueue() = 0; + + /** + * Revert a call to PushEventQueue. When an event queue is popped, any events + * remaining in the queue are appended to the elder queue. This also causes + * the nsIEventTarget returned from PushEventQueue to stop dispatching events. + * Must only be called on the target thread, and with the innermost event + * queue. + */ + virtual void PopEventQueue(nsIEventTarget* aTarget) = 0; + + /** + * Flush the list of shutdown tasks which were previously registered. After + * this is called, new shutdown tasks cannot be registered. + */ + virtual void RunShutdownTasks() = 0; + + protected: + virtual ~SynchronizedEventQueue() = default; + + private: + nsTObserverArray> mEventObservers; +}; + +} // namespace mozilla + +#endif // mozilla_SynchronizedEventQueue_h diff --git a/xpcom/threads/TaskController.cpp b/xpcom/threads/TaskController.cpp new file mode 100644 index 0000000000..8e3aae185a --- /dev/null +++ b/xpcom/threads/TaskController.cpp @@ -0,0 +1,1098 @@ +/* -*- 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 "TaskController.h" +#include "nsIIdleRunnable.h" +#include "nsIRunnable.h" +#include "nsThreadUtils.h" +#include +#include "GeckoProfiler.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/EventQueue.h" +#include "mozilla/Hal.h" +#include "mozilla/InputTaskManager.h" +#include "mozilla/VsyncTaskManager.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "nsIThreadInternal.h" +#include "nsThread.h" +#include "prenv.h" +#include "prsystem.h" + +namespace mozilla { + +StaticAutoPtr TaskController::sSingleton; + +thread_local size_t mThreadPoolIndex = -1; +std::atomic Task::sCurrentTaskSeqNo = 0; + +const int32_t kMinimumPoolThreadCount = 2; +const int32_t kMaximumPoolThreadCount = 8; + +/* static */ +int32_t TaskController::GetPoolThreadCount() { + if (PR_GetEnv("MOZ_TASKCONTROLLER_THREADCOUNT")) { + return strtol(PR_GetEnv("MOZ_TASKCONTROLLER_THREADCOUNT"), nullptr, 0); + } + + int32_t numCores = 0; +#if defined(XP_MACOSX) && defined(__aarch64__) + if (const auto& cpuInfo = hal::GetHeterogeneousCpuInfo()) { + // -1 because of the main thread. + numCores = cpuInfo->mBigCpus.Count() + cpuInfo->mMediumCpus.Count() - 1; + } else +#endif + { + numCores = std::max(1, PR_GetNumberOfProcessors()); + } + + return std::clamp(numCores, kMinimumPoolThreadCount, + kMaximumPoolThreadCount); +} + +#if defined(MOZ_COLLECTING_RUNNABLE_TELEMETRY) + +struct TaskMarker : BaseMarkerType { + static constexpr const char* Name = "Task"; + static constexpr const char* Description = + "Marker representing a task being executed in TaskController."; + + using MS = MarkerSchema; + static constexpr MS::PayloadField PayloadFields[] = { + {"name", MS::InputType::CString, "Task Name", MS::Format::String, + MS::PayloadFlags::Searchable}, + {"priority", MS::InputType::Uint32, "Priority level", + MS::Format::Integer}, + {"priorityName", MS::InputType::CString, "Priority Name"}}; + + static constexpr MS::Location Locations[] = {MS::Location::MarkerChart, + MS::Location::MarkerTable}; + static constexpr const char* ChartLabel = "{marker.data.name}"; + static constexpr const char* TableLabel = + "{marker.name} - {marker.data.name} - priority: " + "{marker.data.priorityName} ({marker.data.priority})"; + + static constexpr MS::ETWMarkerGroup Group = MS::ETWMarkerGroup::Scheduling; + + static void TranslateMarkerInputToSchema(void* aContext, + const nsCString& aName, + uint32_t aPriority) { + ETW::OutputMarkerSchema(aContext, TaskMarker{}, aName, aPriority, + ProfilerStringView("")); + } + + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const nsCString& aName, uint32_t aPriority) { + aWriter.StringProperty("name", aName); + aWriter.IntProperty("priority", aPriority); + +# define EVENT_PRIORITY(NAME, VALUE) \ + if (aPriority == (VALUE)) { \ + aWriter.StringProperty("priorityName", #NAME); \ + } else + EVENT_QUEUE_PRIORITY_LIST(EVENT_PRIORITY) +# undef EVENT_PRIORITY + { + aWriter.StringProperty("priorityName", "Invalid Value"); + } + } +}; + +class MOZ_RAII AutoProfileTask { + public: + explicit AutoProfileTask(nsACString& aName, uint64_t aPriority) + : mName(aName), mPriority(aPriority) { + if (profiler_is_collecting_markers()) { + mStartTime = TimeStamp::Now(); + } + } + + ~AutoProfileTask() { + if (!profiler_thread_is_being_profiled_for_markers()) { + return; + } + + AUTO_PROFILER_LABEL("AutoProfileTask", PROFILER); + AUTO_PROFILER_STATS(AUTO_PROFILE_TASK); + profiler_add_marker("Runnable", ::mozilla::baseprofiler::category::OTHER, + mStartTime.IsNull() + ? MarkerTiming::IntervalEnd() + : MarkerTiming::IntervalUntilNowFrom(mStartTime), + TaskMarker{}, mName, mPriority); + } + + private: + TimeStamp mStartTime; + nsAutoCString mName; + uint32_t mPriority; +}; + +# define AUTO_PROFILE_FOLLOWING_TASK(task) \ + nsAutoCString name; \ + (task)->GetName(name); \ + AUTO_PROFILER_LABEL_DYNAMIC_NSCSTRING_NONSENSITIVE("Task", OTHER, name); \ + mozilla::AutoProfileTask PROFILER_RAII(name, (task)->GetPriority()); +#else +# define AUTO_PROFILE_FOLLOWING_TASK(task) +#endif + +bool TaskManager:: + UpdateCachesForCurrentIterationAndReportPriorityModifierChanged( + const MutexAutoLock& aProofOfLock, IterationType aIterationType) { + mCurrentSuspended = IsSuspended(aProofOfLock); + + if (aIterationType == IterationType::EVENT_LOOP_TURN && !mCurrentSuspended) { + int32_t oldModifier = mCurrentPriorityModifier; + mCurrentPriorityModifier = + GetPriorityModifierForEventLoopTurn(aProofOfLock); + + if (mCurrentPriorityModifier != oldModifier) { + return true; + } + } + return false; +} + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY +class MOZ_RAII AutoSetMainThreadRunnableName { + public: + explicit AutoSetMainThreadRunnableName(const nsCString& aName) { + MOZ_ASSERT(NS_IsMainThread()); + // We want to record our current runnable's name in a static so + // that BHR can record it. + mRestoreRunnableName = nsThread::sMainThreadRunnableName; + + // Copy the name into sMainThreadRunnableName's buffer, and append a + // terminating null. + uint32_t length = std::min((uint32_t)nsThread::kRunnableNameBufSize - 1, + (uint32_t)aName.Length()); + memcpy(nsThread::sMainThreadRunnableName.begin(), aName.BeginReading(), + length); + nsThread::sMainThreadRunnableName[length] = '\0'; + } + + ~AutoSetMainThreadRunnableName() { + nsThread::sMainThreadRunnableName = mRestoreRunnableName; + } + + private: + Array mRestoreRunnableName; +}; +#endif + +Task* Task::GetHighestPriorityDependency() { + Task* currentTask = this; + + while (!currentTask->mDependencies.empty()) { + auto iter = currentTask->mDependencies.begin(); + + while (iter != currentTask->mDependencies.end()) { + if ((*iter)->mCompleted) { + auto oldIter = iter; + iter++; + // Completed tasks are removed here to prevent needlessly keeping them + // alive or iterating over them in the future. + currentTask->mDependencies.erase(oldIter); + continue; + } + + currentTask = iter->get(); + break; + } + } + + return currentTask == this ? nullptr : currentTask; +} + +void TaskController::Initialize() { + MOZ_ASSERT(!sSingleton); + sSingleton = new TaskController(); +} + +void ThreadFuncPoolThread(void* aIndex) { + mThreadPoolIndex = *reinterpret_cast(aIndex); + delete reinterpret_cast(aIndex); + TaskController::Get()->RunPoolThread(); +} + +TaskController::TaskController() + : mGraphMutex("TaskController::mGraphMutex"), + mThreadPoolCV(mGraphMutex, "TaskController::mThreadPoolCV"), + mMainThreadCV(mGraphMutex, "TaskController::mMainThreadCV"), + mRunOutOfMTTasksCounter(0) { + InputTaskManager::Init(); + VsyncTaskManager::Init(); + mMTProcessingRunnable = NS_NewRunnableFunction( + "TaskController::ExecutePendingMTTasks()", + []() { TaskController::Get()->ProcessPendingMTTask(); }); + mMTBlockingProcessingRunnable = NS_NewRunnableFunction( + "TaskController::ExecutePendingMTTasks()", + []() { TaskController::Get()->ProcessPendingMTTask(true); }); +} + +// We want our default stack size limit to be approximately 2MB, to be safe for +// JS helper tasks that can use a lot of stack, but expect most threads to use +// much less. On Linux, however, requesting a stack of 2MB or larger risks the +// kernel allocating an entire 2MB huge page for it on first access, which we do +// not want. To avoid this possibility, we subtract 2 standard VM page sizes +// from our default. +constexpr PRUint32 sBaseStackSize = 2048 * 1024 - 2 * 4096; + +// TSan enforces a minimum stack size that's just slightly larger than our +// default helper stack size. It does this to store blobs of TSan-specific data +// on each thread's stack. Unfortunately, that means that even though we'll +// actually receive a larger stack than we requested, the effective usable space +// of that stack is significantly less than what we expect. To offset TSan +// stealing our stack space from underneath us, double the default. +// +// Similarly, ASan requires more stack space due to red-zones. +#if defined(MOZ_TSAN) || defined(MOZ_ASAN) +constexpr PRUint32 sStackSize = 2 * sBaseStackSize; +#else +constexpr PRUint32 sStackSize = sBaseStackSize; +#endif + +void TaskController::InitializeThreadPool() { + mPoolInitializationMutex.AssertCurrentThreadOwns(); + MOZ_ASSERT(!mThreadPoolInitialized); + mThreadPoolInitialized = true; + + int32_t poolSize = GetPoolThreadCount(); + for (int32_t i = 0; i < poolSize; i++) { + int32_t* index = new int32_t(i); + mPoolThreads.push_back( + {PR_CreateThread(PR_USER_THREAD, ThreadFuncPoolThread, index, + PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, sStackSize), + nullptr}); + } +} + +/* static */ +size_t TaskController::GetThreadStackSize() { return sStackSize; } + +void TaskController::SetPerformanceCounterState( + PerformanceCounterState* aPerformanceCounterState) { + mPerformanceCounterState = aPerformanceCounterState; +} + +/* static */ +void TaskController::Shutdown() { + InputTaskManager::Cleanup(); + VsyncTaskManager::Cleanup(); + if (sSingleton) { + sSingleton->ShutdownThreadPoolInternal(); + sSingleton = nullptr; + } + MOZ_ASSERT(!sSingleton); +} + +void TaskController::ShutdownThreadPoolInternal() { + { + // Prevent race condition on mShuttingDown and wait. + MutexAutoLock lock(mGraphMutex); + mShuttingDown = true; + mThreadPoolCV.NotifyAll(); + } + for (PoolThread& thread : mPoolThreads) { + PR_JoinThread(thread.mThread); + } +} + +void TaskController::RunPoolThread() { + IOInterposer::RegisterCurrentThread(); + + // This is used to hold on to a task to make sure it is released outside the + // lock. This is required since it's perfectly feasible for task destructors + // to post events themselves. + RefPtr lastTask; + + nsAutoCString threadName; + threadName.AppendLiteral("TaskController #"); + threadName.AppendInt(static_cast(mThreadPoolIndex)); + AUTO_PROFILER_REGISTER_THREAD(threadName.BeginReading()); + + MutexAutoLock lock(mGraphMutex); + while (true) { + bool ranTask = false; + + if (!mThreadableTasks.empty()) { + for (auto iter = mThreadableTasks.begin(); iter != mThreadableTasks.end(); + ++iter) { + // Search for the highest priority dependency of the highest priority + // task. + + // We work with rawptrs to avoid needless refcounting. All our tasks + // are always kept alive by the graph. If one is removed from the graph + // it is kept alive by mPoolThreads[mThreadPoolIndex].mCurrentTask. + Task* task = iter->get(); + + MOZ_ASSERT(!task->mTaskManager); + + mPoolThreads[mThreadPoolIndex].mEffectiveTaskPriority = + task->GetPriority(); + + Task* nextTask; + while ((nextTask = task->GetHighestPriorityDependency())) { + task = nextTask; + } + + if (task->GetKind() == Task::Kind::MainThreadOnly || + task->mInProgress) { + continue; + } + + mPoolThreads[mThreadPoolIndex].mCurrentTask = task; + mThreadableTasks.erase(task->mIterator); + task->mIterator = mThreadableTasks.end(); + task->mInProgress = true; + + if (!mThreadableTasks.empty()) { + // Ensure at least one additional thread is woken up if there are + // more threadable tasks to process. Notifying all threads at once + // isn't actually better for performance since they all need the + // GraphMutex to proceed anyway. + mThreadPoolCV.Notify(); + } + + bool taskCompleted = false; + { + MutexAutoUnlock unlock(mGraphMutex); + lastTask = nullptr; + AUTO_PROFILE_FOLLOWING_TASK(task); + taskCompleted = task->Run() == Task::TaskResult::Complete; + ranTask = true; + } + + task->mInProgress = false; + + if (!taskCompleted) { + // Presumably this task was interrupted, leave its dependencies + // unresolved and reinsert into the queue. + auto insertion = mThreadableTasks.insert( + mPoolThreads[mThreadPoolIndex].mCurrentTask); + MOZ_ASSERT(insertion.second); + task->mIterator = insertion.first; + } else { + task->mCompleted = true; +#ifdef DEBUG + task->mIsInGraph = false; +#endif + task->mDependencies.clear(); + // This may have unblocked a main thread task. We could do this only + // if there was a main thread task before this one in the dependency + // chain. + mMayHaveMainThreadTask = true; + // Since this could have multiple dependencies thare are restricted + // to the main thread. Let's make sure that's awake. + EnsureMainThreadTasksScheduled(); + + MaybeInterruptTask(GetHighestPriorityMTTask()); + } + + // Store last task for release next time we release the lock or enter + // wait state. + lastTask = mPoolThreads[mThreadPoolIndex].mCurrentTask.forget(); + break; + } + } + + // Ensure the last task is released before we enter the wait state. + if (lastTask) { + MutexAutoUnlock unlock(mGraphMutex); + lastTask = nullptr; + + // Run another loop iteration, while we were unlocked there was an + // opportunity for another task to be posted or shutdown to be initiated. + continue; + } + + if (!ranTask) { + if (mShuttingDown) { + IOInterposer::UnregisterCurrentThread(); + MOZ_ASSERT(mThreadableTasks.empty()); + return; + } + + AUTO_PROFILER_LABEL("TaskController::RunPoolThread", IDLE); + mThreadPoolCV.Wait(); + } + } +} + +void TaskController::AddTask(already_AddRefed&& aTask) { + RefPtr task(aTask); + + if (task->GetKind() == Task::Kind::OffMainThreadOnly) { + MutexAutoLock lock(mPoolInitializationMutex); + if (!mThreadPoolInitialized) { + InitializeThreadPool(); + } + } + + MutexAutoLock lock(mGraphMutex); + + if (TaskManager* manager = task->GetManager()) { + if (manager->mTaskCount == 0) { + mTaskManagers.insert(manager); + } + manager->DidQueueTask(); + + // Set this here since if this manager's priority modifier doesn't change + // we will not reprioritize when iterating over the queue. + task->mPriorityModifier = manager->mCurrentPriorityModifier; + } + + if (profiler_is_active_and_unpaused()) { + task->mInsertionTime = TimeStamp::Now(); + } + +#ifdef DEBUG + task->mIsInGraph = true; + + for (const RefPtr& otherTask : task->mDependencies) { + MOZ_ASSERT(!otherTask->mTaskManager || + otherTask->mTaskManager == task->mTaskManager); + } +#endif + + LogTask::LogDispatch(task); + + std::pair, Task::PriorityCompare>::iterator, bool> + insertion; + switch (task->GetKind()) { + case Task::Kind::MainThreadOnly: + if (task->GetPriority() >= + static_cast(EventQueuePriority::Normal) && + !mMainThreadTasks.empty()) { + insertion = std::pair( + mMainThreadTasks.insert(--mMainThreadTasks.end(), std::move(task)), + true); + } else { + insertion = mMainThreadTasks.insert(std::move(task)); + } + break; + case Task::Kind::OffMainThreadOnly: + insertion = mThreadableTasks.insert(std::move(task)); + break; + } + (*insertion.first)->mIterator = insertion.first; + MOZ_ASSERT(insertion.second); + + MaybeInterruptTask(*insertion.first); +} + +void TaskController::WaitForTaskOrMessage() { + MutexAutoLock lock(mGraphMutex); + while (!mMayHaveMainThreadTask) { + AUTO_PROFILER_LABEL("TaskController::WaitForTaskOrMessage", IDLE); + mMainThreadCV.Wait(); + } +} + +void TaskController::ExecuteNextTaskOnlyMainThread() { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mGraphMutex); + ExecuteNextTaskOnlyMainThreadInternal(lock); +} + +void TaskController::ProcessPendingMTTask(bool aMayWait) { + MOZ_ASSERT(NS_IsMainThread()); + MutexAutoLock lock(mGraphMutex); + + for (;;) { + // We only ever process one event here. However we may sometimes + // not actually process a real event because of suspended tasks. + // This loop allows us to wait until we've processed something + // in that scenario. + + mMTTaskRunnableProcessedTask = ExecuteNextTaskOnlyMainThreadInternal(lock); + + if (mMTTaskRunnableProcessedTask || !aMayWait) { + break; + } + +#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR + // Unlock before calling into the BackgroundHangMonitor API as it uses + // the timer API. + { + MutexAutoUnlock unlock(mGraphMutex); + BackgroundHangMonitor().NotifyWait(); + } +#endif + + { + // ProcessNextEvent will also have attempted to wait, however we may have + // given it a Runnable when all the tasks in our task graph were suspended + // but we weren't able to cheaply determine that. + AUTO_PROFILER_LABEL("TaskController::ProcessPendingMTTask", IDLE); + mMainThreadCV.Wait(); + } + +#ifdef MOZ_ENABLE_BACKGROUND_HANG_MONITOR + { + MutexAutoUnlock unlock(mGraphMutex); + BackgroundHangMonitor().NotifyActivity(); + } +#endif + } + + if (mMayHaveMainThreadTask) { + EnsureMainThreadTasksScheduled(); + } +} + +void TaskController::ReprioritizeTask(Task* aTask, uint32_t aPriority) { + MutexAutoLock lock(mGraphMutex); + std::set, Task::PriorityCompare>* queue = &mMainThreadTasks; + if (aTask->GetKind() == Task::Kind::OffMainThreadOnly) { + queue = &mThreadableTasks; + } + + MOZ_ASSERT(aTask->mIterator != queue->end()); + queue->erase(aTask->mIterator); + + aTask->mPriority = aPriority; + + auto insertion = queue->insert(aTask); + MOZ_ASSERT(insertion.second); + aTask->mIterator = insertion.first; + + MaybeInterruptTask(aTask); +} + +// Code supporting runnable compatibility. +// Task that wraps a runnable. +class RunnableTask : public Task { + public: + RunnableTask(already_AddRefed&& aRunnable, int32_t aPriority, + Kind aKind) + : Task(aKind, aPriority), mRunnable(aRunnable) {} + + virtual TaskResult Run() override { + mRunnable->Run(); + mRunnable = nullptr; + return TaskResult::Complete; + } + + void SetIdleDeadline(TimeStamp aDeadline) override { + nsCOMPtr idleRunnable = do_QueryInterface(mRunnable); + if (idleRunnable) { + idleRunnable->SetDeadline(aDeadline); + } + } + + virtual bool GetName(nsACString& aName) override { +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + if (nsCOMPtr named = do_QueryInterface(mRunnable)) { + MOZ_ALWAYS_TRUE(NS_SUCCEEDED(named->GetName(aName))); + } else { + aName.AssignLiteral("non-nsINamed runnable"); + } + if (aName.IsEmpty()) { + aName.AssignLiteral("anonymous runnable"); + } + return true; +#else + return false; +#endif + } + + private: + RefPtr mRunnable; +}; + +void TaskController::DispatchRunnable(already_AddRefed&& aRunnable, + uint32_t aPriority, + TaskManager* aManager) { + RefPtr task = new RunnableTask(std::move(aRunnable), aPriority, + Task::Kind::MainThreadOnly); + + task->SetManager(aManager); + TaskController::Get()->AddTask(task.forget()); +} + +nsIRunnable* TaskController::GetRunnableForMTTask(bool aReallyWait) { + MutexAutoLock lock(mGraphMutex); + + while (mMainThreadTasks.empty()) { + if (!aReallyWait) { + return nullptr; + } + + AUTO_PROFILER_LABEL("TaskController::GetRunnableForMTTask::Wait", IDLE); + mMainThreadCV.Wait(); + } + + return aReallyWait ? mMTBlockingProcessingRunnable : mMTProcessingRunnable; +} + +bool TaskController::HasMainThreadPendingTasks() { + MOZ_ASSERT(NS_IsMainThread()); + auto resetIdleState = MakeScopeExit([&idleManager = mIdleTaskManager] { + if (idleManager) { + idleManager->State().ClearCachedIdleDeadline(); + } + }); + + for (bool considerIdle : {false, true}) { + if (considerIdle && !mIdleTaskManager) { + continue; + } + + MutexAutoLock lock(mGraphMutex); + + if (considerIdle) { + mIdleTaskManager->State().ForgetPendingTaskGuarantee(); + // Temporarily unlock so we can peek our idle deadline. + // XXX We could do this _before_ we take the lock if the API would let us. + // We do want to do this before looking at mMainThreadTasks, in case + // someone adds one while we're unlocked. + { + MutexAutoUnlock unlock(mGraphMutex); + mIdleTaskManager->State().CachePeekedIdleDeadline(unlock); + } + } + + // Return early if there's no tasks at all. + if (mMainThreadTasks.empty()) { + return false; + } + + // We can cheaply count how many tasks are suspended. + uint64_t totalSuspended = 0; + for (TaskManager* manager : mTaskManagers) { + DebugOnly modifierChanged = + manager + ->UpdateCachesForCurrentIterationAndReportPriorityModifierChanged( + lock, TaskManager::IterationType::NOT_EVENT_LOOP_TURN); + MOZ_ASSERT(!modifierChanged); + + // The idle manager should be suspended unless we're doing the idle pass. + MOZ_ASSERT(manager != mIdleTaskManager || manager->mCurrentSuspended || + considerIdle, + "Why are idle tasks not suspended here?"); + + if (manager->mCurrentSuspended) { + // XXX - If managers manage off-main-thread tasks this breaks! This + // scenario is explicitly not supported. + // + // This is only incremented inside the lock -or- decremented on the main + // thread so this is safe. + totalSuspended += manager->mTaskCount; + } + } + + // This would break down if we have a non-suspended task depending on a + // suspended task. This is why for the moment we do not allow tasks + // to be dependent on tasks managed by another taskmanager. + if (mMainThreadTasks.size() > totalSuspended) { + // If mIdleTaskManager->mTaskCount is 0, we never updated the suspended + // state of mIdleTaskManager above, hence shouldn't even check it here. + // But in that case idle tasks are not contributing to our suspended task + // count anyway. + if (mIdleTaskManager && mIdleTaskManager->mTaskCount && + !mIdleTaskManager->mCurrentSuspended) { + MOZ_ASSERT(considerIdle, "Why is mIdleTaskManager not suspended?"); + // Check whether the idle tasks were really needed to make our "we have + // an unsuspended task" decision. If they were, we need to force-enable + // idle tasks until we run our next task. + if (mMainThreadTasks.size() - mIdleTaskManager->mTaskCount <= + totalSuspended) { + mIdleTaskManager->State().EnforcePendingTaskGuarantee(); + } + } + return true; + } + } + return false; +} + +uint64_t TaskController::PendingMainthreadTaskCountIncludingSuspended() { + MutexAutoLock lock(mGraphMutex); + return mMainThreadTasks.size(); +} + +bool TaskController::ExecuteNextTaskOnlyMainThreadInternal( + const MutexAutoLock& aProofOfLock) { + MOZ_ASSERT(NS_IsMainThread()); + mGraphMutex.AssertCurrentThreadOwns(); + // Block to make it easier to jump to our cleanup. + bool taskRan = false; + do { + taskRan = DoExecuteNextTaskOnlyMainThreadInternal(aProofOfLock); + if (taskRan) { + if (mIdleTaskManager && mIdleTaskManager->mTaskCount && + mIdleTaskManager->IsSuspended(aProofOfLock)) { + uint32_t activeTasks = mMainThreadTasks.size(); + for (TaskManager* manager : mTaskManagers) { + if (manager->IsSuspended(aProofOfLock)) { + activeTasks -= manager->mTaskCount; + } else { + break; + } + } + + if (!activeTasks) { + // We have only idle (and maybe other suspended) tasks left, so need + // to update the idle state. We need to temporarily release the lock + // while we do that. + MutexAutoUnlock unlock(mGraphMutex); + mIdleTaskManager->State().RequestIdleDeadlineIfNeeded(unlock); + } + } + break; + } + + if (!mIdleTaskManager) { + break; + } + + if (mIdleTaskManager->mTaskCount) { + // We have idle tasks that we may not have gotten above because + // our idle state is not up to date. We need to update the idle state + // and try again. We need to temporarily release the lock while we do + // that. + MutexAutoUnlock unlock(mGraphMutex); + mIdleTaskManager->State().UpdateCachedIdleDeadline(unlock); + } else { + MutexAutoUnlock unlock(mGraphMutex); + mIdleTaskManager->State().RanOutOfTasks(unlock); + } + + // When we unlocked, someone may have queued a new task on us. So try to + // see whether we can run things again. + taskRan = DoExecuteNextTaskOnlyMainThreadInternal(aProofOfLock); + } while (false); + + if (mIdleTaskManager) { + // The pending task guarantee is not needed anymore, since we just tried + // running a task + mIdleTaskManager->State().ForgetPendingTaskGuarantee(); + + if (mMainThreadTasks.empty()) { + ++mRunOutOfMTTasksCounter; + + // XXX the IdlePeriodState API demands we have a MutexAutoUnlock for it. + // Otherwise we could perhaps just do this after we exit the locked block, + // by pushing the lock down into this method. Though it's not clear that + // we could check mMainThreadTasks.size() once we unlock, and whether we + // could maybe substitute mMayHaveMainThreadTask for that check. + MutexAutoUnlock unlock(mGraphMutex); + mIdleTaskManager->State().RanOutOfTasks(unlock); + } + } + + return taskRan; +} + +bool TaskController::DoExecuteNextTaskOnlyMainThreadInternal( + const MutexAutoLock& aProofOfLock) { + mGraphMutex.AssertCurrentThreadOwns(); + + nsCOMPtr mainIThread; + NS_GetMainThread(getter_AddRefs(mainIThread)); + + nsThread* mainThread = static_cast(mainIThread.get()); + if (mainThread) { + mainThread->SetRunningEventDelay(TimeDuration(), TimeStamp()); + } + + uint32_t totalSuspended = 0; + for (TaskManager* manager : mTaskManagers) { + bool modifierChanged = + manager + ->UpdateCachesForCurrentIterationAndReportPriorityModifierChanged( + aProofOfLock, TaskManager::IterationType::EVENT_LOOP_TURN); + if (modifierChanged) { + ProcessUpdatedPriorityModifier(manager); + } + if (manager->mCurrentSuspended) { + totalSuspended += manager->mTaskCount; + } + } + + MOZ_ASSERT(mMainThreadTasks.size() >= totalSuspended); + + // This would break down if we have a non-suspended task depending on a + // suspended task. This is why for the moment we do not allow tasks + // to be dependent on tasks managed by another taskmanager. + if (mMainThreadTasks.size() > totalSuspended) { + for (auto iter = mMainThreadTasks.begin(); iter != mMainThreadTasks.end(); + iter++) { + Task* task = iter->get(); + + if (task->mTaskManager && task->mTaskManager->mCurrentSuspended) { + // Even though we may want to run some dependencies of this task, we + // will run them at their own priority level and not the priority + // level of their dependents. + continue; + } + + task = GetFinalDependency(task); + + if (task->GetKind() == Task::Kind::OffMainThreadOnly || + task->mInProgress || + (task->mTaskManager && task->mTaskManager->mCurrentSuspended)) { + continue; + } + + mCurrentTasksMT.push(task); + mMainThreadTasks.erase(task->mIterator); + task->mIterator = mMainThreadTasks.end(); + task->mInProgress = true; + TaskManager* manager = task->GetManager(); + bool result = false; + + { + MutexAutoUnlock unlock(mGraphMutex); + if (manager) { + manager->WillRunTask(); + if (manager != mIdleTaskManager) { + // Notify the idle period state that we're running a non-idle task. + // This needs to happen while our mutex is not locked! + mIdleTaskManager->State().FlagNotIdle(); + } else { + TimeStamp idleDeadline = + mIdleTaskManager->State().GetCachedIdleDeadline(); + MOZ_ASSERT( + idleDeadline, + "How can we not have a deadline if our manager is enabled?"); + task->SetIdleDeadline(idleDeadline); + } + } + if (mIdleTaskManager) { + // We found a task to run; we can clear the idle deadline on our idle + // task manager. This _must_ be done before we actually run the task, + // because running the task could reenter via spinning the event loop + // and we want to make sure there's no cached idle deadline at that + // point. But we have to make sure we do it after out SetIdleDeadline + // call above, in the case when the task is actually an idle task. + mIdleTaskManager->State().ClearCachedIdleDeadline(); + } + + TimeStamp now = TimeStamp::Now(); + + if (mainThread) { + if (task->GetPriority() < uint32_t(EventQueuePriority::InputHigh) || + task->mInsertionTime.IsNull()) { + mainThread->SetRunningEventDelay(TimeDuration(), now); + } else { + mainThread->SetRunningEventDelay(now - task->mInsertionTime, now); + } + } + + nsAutoCString name; +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + task->GetName(name); +#endif + + PerformanceCounterState::Snapshot snapshot = + mPerformanceCounterState->RunnableWillRun( + now, manager == mIdleTaskManager); + + { + LogTask::Run log(task); +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + AutoSetMainThreadRunnableName nameGuard(name); +#endif + AUTO_PROFILE_FOLLOWING_TASK(task); + result = task->Run() == Task::TaskResult::Complete; + } + + // Task itself should keep manager alive. + if (manager) { + manager->DidRunTask(); + } + + mPerformanceCounterState->RunnableDidRun(name, std::move(snapshot)); + } + + // Task itself should keep manager alive. + if (manager && result && manager->mTaskCount == 0) { + mTaskManagers.erase(manager); + } + + task->mInProgress = false; + + if (!result) { + // Presumably this task was interrupted, leave its dependencies + // unresolved and reinsert into the queue. + auto insertion = + mMainThreadTasks.insert(std::move(mCurrentTasksMT.top())); + MOZ_ASSERT(insertion.second); + task->mIterator = insertion.first; + manager->WillRunTask(); + } else { + task->mCompleted = true; +#ifdef DEBUG + task->mIsInGraph = false; +#endif + // Clear dependencies to release references. + task->mDependencies.clear(); + + if (!mThreadableTasks.empty()) { + // We're going to wake up a single thread in our pool. This thread + // is responsible for waking up additional threads in the situation + // where more than one task became available. + mThreadPoolCV.Notify(); + } + } + + mCurrentTasksMT.pop(); + return true; + } + } + + mMayHaveMainThreadTask = false; + if (mIdleTaskManager) { + // We did not find a task to run. We still need to clear the cached idle + // deadline on our idle state, because that deadline was only relevant to + // the execution of this function. Had we found a task, we would have + // cleared the deadline before running that task. + mIdleTaskManager->State().ClearCachedIdleDeadline(); + } + return false; +} + +Task* TaskController::GetFinalDependency(Task* aTask) { + Task* nextTask; + + while ((nextTask = aTask->GetHighestPriorityDependency())) { + aTask = nextTask; + } + + return aTask; +} + +void TaskController::MaybeInterruptTask(Task* aTask) { + mGraphMutex.AssertCurrentThreadOwns(); + + if (!aTask) { + return; + } + + // This optimization prevents many slow lookups in long chains of similar + // priority. + if (!aTask->mDependencies.empty()) { + Task* firstDependency = aTask->mDependencies.begin()->get(); + if (aTask->GetPriority() <= firstDependency->GetPriority() && + !firstDependency->mCompleted && + aTask->GetKind() == firstDependency->GetKind()) { + // This task has the same or a higher priority as one of its dependencies, + // never any need to interrupt. + return; + } + } + + Task* finalDependency = GetFinalDependency(aTask); + + if (finalDependency->mInProgress) { + // No need to wake anything, we can't schedule this task right now anyway. + return; + } + + if (aTask->GetKind() == Task::Kind::MainThreadOnly) { + mMayHaveMainThreadTask = true; + + EnsureMainThreadTasksScheduled(); + + if (mCurrentTasksMT.empty()) { + return; + } + + // We could go through the steps above here and interrupt an off main + // thread task in case it has a lower priority. + if (finalDependency->GetKind() == Task::Kind::OffMainThreadOnly) { + return; + } + + if (mCurrentTasksMT.top()->GetPriority() < aTask->GetPriority()) { + mCurrentTasksMT.top()->RequestInterrupt(aTask->GetPriority()); + } + } else { + Task* lowestPriorityTask = nullptr; + for (PoolThread& thread : mPoolThreads) { + if (!thread.mCurrentTask) { + mThreadPoolCV.Notify(); + // There's a free thread, no need to interrupt anything. + return; + } + + if (!lowestPriorityTask) { + lowestPriorityTask = thread.mCurrentTask.get(); + continue; + } + + // This should possibly select the lowest priority task which was started + // the latest. But for now we ignore that optimization. + // This also doesn't guarantee a task is interruptable, so that's an + // avenue for improvements as well. + if (lowestPriorityTask->GetPriority() > thread.mEffectiveTaskPriority) { + lowestPriorityTask = thread.mCurrentTask.get(); + } + } + + if (lowestPriorityTask->GetPriority() < aTask->GetPriority()) { + lowestPriorityTask->RequestInterrupt(aTask->GetPriority()); + } + + // We choose not to interrupt main thread tasks for tasks which may be + // executed off the main thread. + } +} + +Task* TaskController::GetHighestPriorityMTTask() { + mGraphMutex.AssertCurrentThreadOwns(); + + if (!mMainThreadTasks.empty()) { + return mMainThreadTasks.begin()->get(); + } + return nullptr; +} + +void TaskController::EnsureMainThreadTasksScheduled() { + if (mObserver) { + mObserver->OnDispatchedEvent(); + } + if (mExternalCondVar) { + mExternalCondVar->Notify(); + } + mMainThreadCV.Notify(); +} + +void TaskController::ProcessUpdatedPriorityModifier(TaskManager* aManager) { + mGraphMutex.AssertCurrentThreadOwns(); + + MOZ_ASSERT(NS_IsMainThread()); + + int32_t modifier = aManager->mCurrentPriorityModifier; + + std::vector> storedTasks; + // Find all relevant tasks. + for (auto iter = mMainThreadTasks.begin(); iter != mMainThreadTasks.end();) { + if ((*iter)->mTaskManager == aManager) { + storedTasks.push_back(*iter); + iter = mMainThreadTasks.erase(iter); + } else { + iter++; + } + } + + // Reinsert found tasks with their new priorities. + for (RefPtr& ref : storedTasks) { + // Kept alive at first by the vector and then by mMainThreadTasks. + Task* task = ref; + task->mPriorityModifier = modifier; + auto insertion = mMainThreadTasks.insert(std::move(ref)); + MOZ_ASSERT(insertion.second); + task->mIterator = insertion.first; + } +} + +} // namespace mozilla diff --git a/xpcom/threads/TaskController.h b/xpcom/threads/TaskController.h new file mode 100644 index 0000000000..f51e61f4cd --- /dev/null +++ b/xpcom/threads/TaskController.h @@ -0,0 +1,459 @@ +/* -*- 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 mozilla_TaskController_h +#define mozilla_TaskController_h + +#include "MainThreadUtils.h" +#include "mozilla/CondVar.h" +#include "mozilla/IdlePeriodState.h" +#include "mozilla/RefPtr.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/EventQueue.h" +#include "nsISupportsImpl.h" + +#include +#include +#include +#include + +class nsIRunnable; +class nsIThreadObserver; + +namespace mozilla { + +class Task; +class TaskController; +class PerformanceCounter; +class PerformanceCounterState; + +const EventQueuePriority kDefaultPriorityValue = EventQueuePriority::Normal; + +// This file contains the core classes to access the Gecko scheduler. The +// scheduler forms a graph of prioritize tasks, and is responsible for ensuring +// the execution of tasks or their dependencies in order of inherited priority. +// +// The core class is the 'Task' class. The task class describes a single unit of +// work. Users scheduling work implement this class and are required to +// reimplement the 'Run' function in order to do work. +// +// The TaskManager class is reimplemented by users that require +// the ability to reprioritize or suspend tasks. +// +// The TaskController is responsible for scheduling the work itself. The AddTask +// function is used to schedule work. The ReprioritizeTask function may be used +// to change the priority of a task already in the task graph, without +// unscheduling it. + +// The TaskManager is the baseclass used to atomically manage a large set of +// tasks. API users reimplementing TaskManager may reimplement a number of +// functions that they may use to indicate to the scheduler changes in the state +// for any tasks they manage. They may be used to reprioritize or suspend tasks +// under their control, and will also be notified before and after tasks under +// their control are executed. Their methods will only be called once per event +// loop turn, however they may still incur some performance overhead. In +// addition to this frequent reprioritizations may incur a significant +// performance overhead and are discouraged. A TaskManager may currently only be +// used to manage tasks that are bound to the Gecko Main Thread. +class TaskManager { + public: + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TaskManager) + + TaskManager() : mTaskCount(0) {} + + // Subclasses implementing task manager will have this function called to + // determine whether their associated tasks are currently suspended. This + // will only be called once per iteration of the task queue, this means that + // suspension of tasks managed by a single TaskManager may be assumed to + // occur atomically. + virtual bool IsSuspended(const MutexAutoLock& aProofOfLock) { return false; } + + // Subclasses may implement this in order to supply a priority adjustment + // to their managed tasks. This is called once per iteration of the task + // queue, and may be assumed to occur atomically for all managed tasks. + virtual int32_t GetPriorityModifierForEventLoopTurn( + const MutexAutoLock& aProofOfLock) { + return 0; + } + + void DidQueueTask() { ++mTaskCount; } + // This is called when a managed task is about to be executed by the + // scheduler. Anyone reimplementing this should ensure to call the parent or + // decrement mTaskCount. + virtual void WillRunTask() { --mTaskCount; } + // This is called when a managed task has finished being executed by the + // scheduler. + virtual void DidRunTask() {} + uint32_t PendingTaskCount() { return mTaskCount; } + + protected: + virtual ~TaskManager() {} + + private: + friend class TaskController; + + enum class IterationType { NOT_EVENT_LOOP_TURN, EVENT_LOOP_TURN }; + bool UpdateCachesForCurrentIterationAndReportPriorityModifierChanged( + const MutexAutoLock& aProofOfLock, IterationType aIterationType); + + bool mCurrentSuspended = false; + int32_t mCurrentPriorityModifier = 0; + + std::atomic mTaskCount; +}; + +// A Task is the the base class for any unit of work that may be scheduled. +// +// Subclasses may specify their priority and whether they should be bound to +// either the Gecko Main thread or off main thread. When not bound to the main +// thread tasks may be executed on any available thread excluding the main +// thread, but they may also be executed in parallel to any other task they do +// not have a dependency relationship with. +// +// Tasks will be run in order of object creation. +class Task { + public: + enum class Kind : uint8_t { + // This task should be executed on any available thread excluding the Gecko + // Main thread. + OffMainThreadOnly, + + // This task should be executed on the Gecko Main thread. + MainThreadOnly + + // NOTE: "any available thread including the main thread" option is not + // supported (See bug 1839102). + }; + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Task) + + Kind GetKind() { return mKind; } + + // This returns the current task priority with its modifier applied. + uint32_t GetPriority() { return mPriority + mPriorityModifier; } + uint64_t GetSeqNo() { return mSeqNo; } + + // Callee needs to assume this may be called on any thread. + // aInterruptPriority passes the priority of the higher priority task that + // is ready to be executed. The task may safely ignore this function, or + // interrupt any work being done. It may return 'false' from its run function + // in order to be run automatically in the future, or true if it will + // reschedule incomplete work manually. + virtual void RequestInterrupt(uint32_t aInterruptPriority) {} + + // At the moment this -must- be called before the task is added to the + // controller. Calling this after tasks have been added to the controller + // results in undefined behavior! + // At submission, tasks must depend only on tasks managed by the same, or + // no idle manager. + void AddDependency(Task* aTask) { + MOZ_ASSERT(aTask); + MOZ_ASSERT(!mIsInGraph); + mDependencies.insert(aTask); + } + + // This sets the TaskManager for the current task. Calling this after the + // task has been added to the TaskController results in undefined behavior. + void SetManager(TaskManager* aManager) { + MOZ_ASSERT(mKind == Kind::MainThreadOnly); + MOZ_ASSERT(!mIsInGraph); + mTaskManager = aManager; + } + TaskManager* GetManager() { return mTaskManager; } + + struct PriorityCompare { + bool operator()(const RefPtr& aTaskA, + const RefPtr& aTaskB) const { + uint32_t prioA = aTaskA->GetPriority(); + uint32_t prioB = aTaskB->GetPriority(); + return (prioA > prioB) || + (prioA == prioB && (aTaskA->GetSeqNo() < aTaskB->GetSeqNo())); + } + }; + + // Tell the task about its idle deadline. Will only be called for + // tasks managed by an IdleTaskManager, right before the task runs. + virtual void SetIdleDeadline(TimeStamp aDeadline) {} + + virtual PerformanceCounter* GetPerformanceCounter() const { return nullptr; } + + // Get a name for this task. This returns false if the task has no name. +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + virtual bool GetName(nsACString& aName) = 0; +#else + virtual bool GetName(nsACString& aName) { return false; } +#endif + + protected: + Task(Kind aKind, + uint32_t aPriority = static_cast(kDefaultPriorityValue)) + : mKind(aKind), mSeqNo(sCurrentTaskSeqNo++), mPriority(aPriority) {} + + Task(Kind aKind, EventQueuePriority aPriority = kDefaultPriorityValue) + : mKind(aKind), + mSeqNo(sCurrentTaskSeqNo++), + mPriority(static_cast(aPriority)) {} + + virtual ~Task() {} + + friend class TaskController; + + enum class TaskResult { + Complete, + Incomplete, + }; + + // When this returns TaskResult::Incomplete, it will be rescheduled at the + // current 'mPriority' level. + virtual TaskResult Run() = 0; + + private: + Task* GetHighestPriorityDependency(); + + // Iterator pointing to this task's position in + // mThreadableTasks/mMainThreadTasks if, and only if this task is currently + // scheduled to be executed. This allows fast access to the task's position + // in the set, allowing for fast removal. + // This is safe, and remains valid unless the task is removed from the set. + // See also iterator invalidation in: + // https://en.cppreference.com/w/cpp/container + // + // Or the spec: + // "All Associative Containers: The insert and emplace members shall not + // affect the validity of iterators and references to the container + // [26.2.6/9]" "All Associative Containers: The erase members shall invalidate + // only iterators and references to the erased elements [26.2.6/9]" + std::set, PriorityCompare>::iterator mIterator; + std::set, PriorityCompare> mDependencies; + + RefPtr mTaskManager; + + // Access to these variables is protected by the GraphMutex. + Kind mKind; + bool mCompleted = false; + bool mInProgress = false; +#ifdef DEBUG + bool mIsInGraph = false; +#endif + + static std::atomic sCurrentTaskSeqNo; + int64_t mSeqNo; + uint32_t mPriority; + // Modifier currently being applied to this task by its taskmanager. + int32_t mPriorityModifier = 0; + // Time this task was inserted into the task graph, this is used by the + // profiler. + mozilla::TimeStamp mInsertionTime; +}; + +struct PoolThread { + PRThread* mThread; + RefPtr mCurrentTask; + // This may be higher than mCurrentTask's priority due to priority + // propagation. This is -only- valid when mCurrentTask != nullptr. + uint32_t mEffectiveTaskPriority; +}; + +// A task manager implementation for priority levels that should only +// run during idle periods. +class IdleTaskManager : public TaskManager { + public: + explicit IdleTaskManager(already_AddRefed&& aIdlePeriod) + : mIdlePeriodState(std::move(aIdlePeriod)), mProcessedTaskCount(0) {} + + IdlePeriodState& State() { return mIdlePeriodState; } + + bool IsSuspended(const MutexAutoLock& aProofOfLock) override { + TimeStamp idleDeadline = State().GetCachedIdleDeadline(); + return !idleDeadline; + } + + void DidRunTask() override { + TaskManager::DidRunTask(); + ++mProcessedTaskCount; + } + + uint64_t ProcessedTaskCount() { return mProcessedTaskCount; } + + private: + // Tracking of our idle state of various sorts. + IdlePeriodState mIdlePeriodState; + + std::atomic mProcessedTaskCount; +}; + +// The TaskController is the core class of the scheduler. It is used to +// schedule tasks to be executed, as well as to reprioritize tasks that have +// already been scheduled. The core functions to do this are AddTask and +// ReprioritizeTask. +class TaskController { + public: + TaskController(); + + static TaskController* Get() { + MOZ_ASSERT(sSingleton.get()); + return sSingleton.get(); + } + + static void Initialize(); + + void SetThreadObserver(nsIThreadObserver* aObserver) { + MutexAutoLock lock(mGraphMutex); + mObserver = aObserver; + } + void SetConditionVariable(CondVar* aExternalCondVar) { + MutexAutoLock lock(mGraphMutex); + mExternalCondVar = aExternalCondVar; + } + + void SetIdleTaskManager(IdleTaskManager* aIdleTaskManager) { + mIdleTaskManager = aIdleTaskManager; + } + IdleTaskManager* GetIdleTaskManager() { return mIdleTaskManager.get(); } + + uint64_t RunOutOfMTTasksCount() { return mRunOutOfMTTasksCounter; } + + // Initialization and shutdown code. + void SetPerformanceCounterState( + PerformanceCounterState* aPerformanceCounterState); + + static void Shutdown(); + + // This adds a task to the TaskController graph. + // This may be called on any thread. + void AddTask(already_AddRefed&& aTask); + + // This wait function is the theoretical function you would need if our main + // thread needs to also process OS messages or something along those lines. + void WaitForTaskOrMessage(); + + // This gets the next (highest priority) task that is only allowed to execute + // on the main thread. + void ExecuteNextTaskOnlyMainThread(); + + // Process all pending main thread tasks. + void ProcessPendingMTTask(bool aMayWait = false); + + // This allows reprioritization of a task already in the task graph. + // This may be called on any thread. + void ReprioritizeTask(Task* aTask, uint32_t aPriority); + + void DispatchRunnable(already_AddRefed&& aRunnable, + uint32_t aPriority, TaskManager* aManager = nullptr); + + nsIRunnable* GetRunnableForMTTask(bool aReallyWait); + + bool HasMainThreadPendingTasks(); + + uint64_t PendingMainthreadTaskCountIncludingSuspended(); + + // Let users know whether the last main thread task runnable did work. + bool MTTaskRunnableProcessedTask() { + MOZ_ASSERT(NS_IsMainThread()); + return mMTTaskRunnableProcessedTask; + } + + static int32_t GetPoolThreadCount(); + static size_t GetThreadStackSize(); + + private: + friend void ThreadFuncPoolThread(void* aIndex); + static StaticAutoPtr sSingleton; + + void InitializeThreadPool(); + + // This gets the next (highest priority) task that is only allowed to execute + // on the main thread, if any, and executes it. + // Returns true if it succeeded. + bool ExecuteNextTaskOnlyMainThreadInternal(const MutexAutoLock& aProofOfLock); + + // The guts of ExecuteNextTaskOnlyMainThreadInternal, which get idle handling + // wrapped around them. Returns whether a task actually ran. + bool DoExecuteNextTaskOnlyMainThreadInternal( + const MutexAutoLock& aProofOfLock); + + Task* GetFinalDependency(Task* aTask); + void MaybeInterruptTask(Task* aTask); + Task* GetHighestPriorityMTTask(); + + void EnsureMainThreadTasksScheduled(); + + void ProcessUpdatedPriorityModifier(TaskManager* aManager); + + void ShutdownThreadPoolInternal(); + + void RunPoolThread(); + + // This protects access to the task graph. + Mutex mGraphMutex MOZ_UNANNOTATED; + + // This protects thread pool initialization. We cannot do this from within + // the GraphMutex, since thread creation on Windows can generate events on + // the main thread that need to be handled. + Mutex mPoolInitializationMutex = + Mutex("TaskController::mPoolInitializationMutex"); + // Created under the PoolInitialization mutex, then never extended, and + // only freed when the object is freed. mThread is set at creation time; + // mCurrentTask and mEffectiveTaskPriority are only accessed from the + // thread, so no locking is needed to access this. + std::vector mPoolThreads; + + CondVar mThreadPoolCV; + CondVar mMainThreadCV; + + // Variables below are protected by mGraphMutex. + + std::stack> mCurrentTasksMT; + + // A list of all tasks ordered by priority. + std::set, Task::PriorityCompare> mThreadableTasks; + std::set, Task::PriorityCompare> mMainThreadTasks; + + // TaskManagers currently active. + // We can use a raw pointer since tasks always hold on to their TaskManager. + std::set mTaskManagers; + + // This ensures we keep running the main thread if we processed a task there. + bool mMayHaveMainThreadTask = true; + bool mShuttingDown = false; + + // This stores whether the last main thread task runnable did work. + // Accessed only on MainThread + bool mMTTaskRunnableProcessedTask = false; + + // Whether our thread pool is initialized. We use this currently to avoid + // starting the threads in processes where it's never used. This is protected + // by mPoolInitializationMutex. + bool mThreadPoolInitialized = false; + + // Whether we have scheduled a runnable on the main thread event loop. + // This is used for nsIRunnable compatibility. + RefPtr mMTProcessingRunnable; + RefPtr mMTBlockingProcessingRunnable; + + // XXX - Thread observer to notify when a new event has been dispatched + // Set immediately, then simply accessed from any thread + nsIThreadObserver* mObserver = nullptr; + // XXX - External condvar to notify when we have received an event + CondVar* mExternalCondVar = nullptr; + // Idle task manager so we can properly do idle state stuff. + RefPtr mIdleTaskManager; + + // How many times the main thread was empty. + std::atomic mRunOutOfMTTasksCounter; + + // Our tracking of our performance counter and long task state, + // shared with nsThread. + // Set once when MainThread is created, never changed, only accessed from + // DoExecuteNextTaskOnlyMainThreadInternal() + PerformanceCounterState* mPerformanceCounterState = nullptr; +}; + +} // namespace mozilla + +#endif // mozilla_TaskController_h diff --git a/xpcom/threads/TaskDispatcher.h b/xpcom/threads/TaskDispatcher.h new file mode 100644 index 0000000000..1f27c32c7d --- /dev/null +++ b/xpcom/threads/TaskDispatcher.h @@ -0,0 +1,304 @@ +/* -*- 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/. */ + +#if !defined(TaskDispatcher_h_) +# define TaskDispatcher_h_ + +# include + +# include "mozilla/AbstractThread.h" +# include "mozilla/Maybe.h" +# include "mozilla/ProfilerRunnable.h" +# include "mozilla/UniquePtr.h" +# include "nsIDirectTaskDispatcher.h" +# include "nsISupportsImpl.h" +# include "nsTArray.h" +# include "nsThreadUtils.h" + +namespace mozilla { + +class SimpleTaskQueue { + public: + SimpleTaskQueue() = default; + virtual ~SimpleTaskQueue() = default; + + void AddTask(already_AddRefed aRunnable) { + if (!mTasks) { + mTasks.emplace(); + } + mTasks->push(std::move(aRunnable)); + } + + void DrainTasks() { + if (!mTasks) { + return; + } + auto& queue = mTasks.ref(); + while (!queue.empty()) { + nsCOMPtr r = std::move(queue.front()); + queue.pop(); + AUTO_PROFILE_FOLLOWING_RUNNABLE(r); + r->Run(); + } + } + + bool HaveTasks() const { return mTasks && !mTasks->empty(); } + + private: + // We use a Maybe<> because (a) when used for DirectTasks it often doesn't get + // anything put into it, and (b) the std::queue implementation in GNU + // libstdc++ does two largish heap allocations when creating a new std::queue. + Maybe>> mTasks; +}; + +/* + * A classic approach to cross-thread communication is to dispatch asynchronous + * runnables to perform updates on other threads. This generally works well, but + * there are sometimes reasons why we might want to delay the actual dispatch of + * these tasks until a specified moment. At present, this is primarily useful to + * ensure that mirrored state gets updated atomically - but there may be other + * applications as well. + * + * TaskDispatcher is a general abstract class that accepts tasks and dispatches + * them at some later point. These groups of tasks are per-target-thread, and + * contain separate queues for several kinds of tasks (see comments below). - + * "state change tasks" (which run first, and are intended to be used to update + * the value held by mirrors), and regular tasks, which are other arbitrary + * operations that the are gated to run after all the state changes have + * completed. + */ +class TaskDispatcher { + public: + TaskDispatcher() = default; + virtual ~TaskDispatcher() = default; + + // Direct tasks are run directly (rather than dispatched asynchronously) when + // the tail dispatcher fires. A direct task may cause other tasks to be added + // to the tail dispatcher. + virtual void AddDirectTask(already_AddRefed aRunnable) = 0; + + // State change tasks are dispatched asynchronously always run before regular + // tasks. They are intended to be used to update the value held by mirrors + // before any other dispatched tasks are run on the target thread. + virtual void AddStateChangeTask(AbstractThread* aThread, + already_AddRefed aRunnable) = 0; + + // Regular tasks are dispatched asynchronously, and run after state change + // tasks. + virtual nsresult AddTask(AbstractThread* aThread, + already_AddRefed aRunnable) = 0; + + virtual nsresult DispatchTasksFor(AbstractThread* aThread) = 0; + virtual bool HasTasksFor(AbstractThread* aThread) = 0; + virtual void DrainDirectTasks() = 0; +}; + +/* + * AutoTaskDispatcher is a stack-scoped TaskDispatcher implementation that fires + * its queued tasks when it is popped off the stack. + */ +class AutoTaskDispatcher : public TaskDispatcher { + public: + explicit AutoTaskDispatcher(nsIDirectTaskDispatcher* aDirectTaskDispatcher, + bool aIsTailDispatcher = false) + : mDirectTaskDispatcher(aDirectTaskDispatcher), + mIsTailDispatcher(aIsTailDispatcher) {} + + ~AutoTaskDispatcher() { + // Given that direct tasks may trigger other code that uses the tail + // dispatcher, it's better to avoid processing them in the tail dispatcher's + // destructor. So we require TailDispatchers to manually invoke + // DrainDirectTasks before the AutoTaskDispatcher gets destroyed. In truth, + // this is only necessary in the case where this AutoTaskDispatcher can be + // accessed by the direct tasks it dispatches (true for TailDispatchers, but + // potentially not true for other hypothetical AutoTaskDispatchers). Feel + // free to loosen this restriction to apply only to mIsTailDispatcher if a + // use-case requires it. + MOZ_ASSERT(!HaveDirectTasks()); + + for (size_t i = 0; i < mTaskGroups.Length(); ++i) { + DispatchTaskGroup(std::move(mTaskGroups[i])); + } + } + + bool HaveDirectTasks() { + return mDirectTaskDispatcher && mDirectTaskDispatcher->HaveDirectTasks(); + } + + void DrainDirectTasks() override { + if (mDirectTaskDispatcher) { + mDirectTaskDispatcher->DrainDirectTasks(); + } + } + + void AddDirectTask(already_AddRefed aRunnable) override { + MOZ_ASSERT(mDirectTaskDispatcher); + mDirectTaskDispatcher->DispatchDirectTask(std::move(aRunnable)); + } + + void AddStateChangeTask(AbstractThread* aThread, + already_AddRefed aRunnable) override { + nsCOMPtr r = aRunnable; + MOZ_RELEASE_ASSERT(r); + EnsureTaskGroup(aThread).mStateChangeTasks.AppendElement(r.forget()); + } + + nsresult AddTask(AbstractThread* aThread, + already_AddRefed aRunnable) override { + nsCOMPtr r = aRunnable; + MOZ_RELEASE_ASSERT(r); + // To preserve the event order, we need to append a new group if the last + // group is not targeted for |aThread|. + // See https://bugzilla.mozilla.org/show_bug.cgi?id=1318226&mark=0-3#c0 + // for the details of the issue. + if (mTaskGroups.Length() == 0 || + mTaskGroups.LastElement()->mThread != aThread) { + mTaskGroups.AppendElement(new PerThreadTaskGroup(aThread)); + } + + PerThreadTaskGroup& group = *mTaskGroups.LastElement(); + group.mRegularTasks.AppendElement(r.forget()); + + return NS_OK; + } + + bool HasTasksFor(AbstractThread* aThread) override { + return !!GetTaskGroup(aThread) || + (aThread == AbstractThread::GetCurrent() && HaveDirectTasks()); + } + + nsresult DispatchTasksFor(AbstractThread* aThread) override { + nsresult rv = NS_OK; + + // Dispatch all groups that match |aThread|. + for (size_t i = 0; i < mTaskGroups.Length(); ++i) { + if (mTaskGroups[i]->mThread == aThread) { + nsresult rv2 = DispatchTaskGroup(std::move(mTaskGroups[i])); + + if (NS_WARN_IF(NS_FAILED(rv2)) && NS_SUCCEEDED(rv)) { + // We should try our best to call DispatchTaskGroup() as much as + // possible and return an error if any of DispatchTaskGroup() calls + // failed. + rv = rv2; + } + + mTaskGroups.RemoveElementAt(i--); + } + } + + return rv; + } + + private: + struct PerThreadTaskGroup { + public: + explicit PerThreadTaskGroup(AbstractThread* aThread) : mThread(aThread) { + MOZ_COUNT_CTOR(PerThreadTaskGroup); + } + + MOZ_COUNTED_DTOR(PerThreadTaskGroup) + + RefPtr mThread; + nsTArray> mStateChangeTasks; + nsTArray> mRegularTasks; + }; + + class TaskGroupRunnable : public Runnable { + public: + explicit TaskGroupRunnable(UniquePtr&& aTasks) + : Runnable("AutoTaskDispatcher::TaskGroupRunnable"), + mTasks(std::move(aTasks)) {} + + NS_IMETHOD Run() override { + // State change tasks get run all together before any code is run, so + // that all state changes are made in an atomic unit. + for (size_t i = 0; i < mTasks->mStateChangeTasks.Length(); ++i) { + mTasks->mStateChangeTasks[i]->Run(); + } + + // Once the state changes have completed, drain any direct tasks + // generated by those state changes (i.e. watcher notification tasks). + // This needs to be outside the loop because we don't want to run code + // that might observe intermediate states. + MaybeDrainDirectTasks(); + + for (size_t i = 0; i < mTasks->mRegularTasks.Length(); ++i) { + AUTO_PROFILE_FOLLOWING_RUNNABLE(mTasks->mRegularTasks[i]); + mTasks->mRegularTasks[i]->Run(); + + // Scope direct tasks tightly to the task that generated them. + MaybeDrainDirectTasks(); + } + + return NS_OK; + } + + private: + void MaybeDrainDirectTasks() { + AbstractThread* currentThread = AbstractThread::GetCurrent(); + if (currentThread && currentThread->MightHaveTailTasks()) { + currentThread->TailDispatcher().DrainDirectTasks(); + } + } + + UniquePtr mTasks; + }; + + PerThreadTaskGroup& EnsureTaskGroup(AbstractThread* aThread) { + PerThreadTaskGroup* existing = GetTaskGroup(aThread); + if (existing) { + return *existing; + } + + mTaskGroups.AppendElement(new PerThreadTaskGroup(aThread)); + return *mTaskGroups.LastElement(); + } + + PerThreadTaskGroup* GetTaskGroup(AbstractThread* aThread) { + for (size_t i = 0; i < mTaskGroups.Length(); ++i) { + if (mTaskGroups[i]->mThread == aThread) { + return mTaskGroups[i].get(); + } + } + + // Not found. + return nullptr; + } + + nsresult DispatchTaskGroup(UniquePtr aGroup) { + RefPtr thread = aGroup->mThread; + + AbstractThread::DispatchReason reason = + mIsTailDispatcher ? AbstractThread::TailDispatch + : AbstractThread::NormalDispatch; + nsCOMPtr r = new TaskGroupRunnable(std::move(aGroup)); + return thread->Dispatch(r.forget(), reason); + } + + // Task groups, organized by thread. + nsTArray> mTaskGroups; + + nsCOMPtr mDirectTaskDispatcher; + // True if this TaskDispatcher represents the tail dispatcher for the thread + // upon which it runs. + const bool mIsTailDispatcher; +}; + +// Little utility class to allow declaring AutoTaskDispatcher as a default +// parameter for methods that take a TaskDispatcher&. +template +class PassByRef { + public: + PassByRef() = default; + operator T&() { return mVal; } + + private: + T mVal; +}; + +} // namespace mozilla + +#endif diff --git a/xpcom/threads/TaskQueue.cpp b/xpcom/threads/TaskQueue.cpp new file mode 100644 index 0000000000..febb609784 --- /dev/null +++ b/xpcom/threads/TaskQueue.cpp @@ -0,0 +1,345 @@ +/* -*- 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/TaskQueue.h" + +#include "mozilla/ProfilerRunnable.h" +#include "nsIEventTarget.h" +#include "nsITargetShutdownTask.h" +#include "nsThreadUtils.h" +#include "nsQueryObject.h" + +namespace mozilla { + +// Handle for a TaskQueue being tracked by a TaskQueueTracker. When created, +// it is registered with the TaskQueueTracker, and when destroyed it is +// unregistered. Holds a threadsafe weak reference to the TaskQueue. +class TaskQueueTrackerEntry final + : private LinkedListElement { + public: + TaskQueueTrackerEntry(TaskQueueTracker* aTracker, + const RefPtr& aQueue) + : mTracker(aTracker), mQueue(aQueue) { + MutexAutoLock lock(mTracker->mMutex); + mTracker->mEntries.insertFront(this); + } + ~TaskQueueTrackerEntry() { + MutexAutoLock lock(mTracker->mMutex); + removeFrom(mTracker->mEntries); + } + + TaskQueueTrackerEntry(const TaskQueueTrackerEntry&) = delete; + TaskQueueTrackerEntry(TaskQueueTrackerEntry&&) = delete; + TaskQueueTrackerEntry& operator=(const TaskQueueTrackerEntry&) = delete; + TaskQueueTrackerEntry& operator=(TaskQueueTrackerEntry&&) = delete; + + RefPtr GetQueue() const { return RefPtr(mQueue); } + + private: + friend class LinkedList; + friend class LinkedListElement; + + const RefPtr mTracker; + const ThreadSafeWeakPtr mQueue; +}; + +RefPtr TaskQueue::Create(already_AddRefed aTarget, + const char* aName, + bool aSupportsTailDispatch) { + nsCOMPtr target(std::move(aTarget)); + RefPtr queue = + new TaskQueue(do_AddRef(target), aName, aSupportsTailDispatch); + + // If |target| is a TaskQueueTracker, register this TaskQueue with it. It will + // be unregistered when the TaskQueue is destroyed or shut down. + if (RefPtr tracker = do_QueryObject(target)) { + MonitorAutoLock lock(queue->mQueueMonitor); + queue->mTrackerEntry = MakeUnique(tracker, queue); + } + + return queue; +} + +TaskQueue::TaskQueue(already_AddRefed aTarget, + const char* aName, bool aSupportsTailDispatch) + : AbstractThread(aSupportsTailDispatch), + mTarget(aTarget), + mQueueMonitor("TaskQueue::Queue"), + mTailDispatcher(nullptr), + mIsRunning(false), + mIsShutdown(false), + mName(aName) {} + +TaskQueue::~TaskQueue() { + // We should never free the TaskQueue if it was destroyed abnormally, meaning + // that all cleanup tasks should be complete if we do. + MOZ_ASSERT(mShutdownTasks.IsEmpty()); +} + +NS_IMPL_ADDREF_INHERITED(TaskQueue, SupportsThreadSafeWeakPtr) +NS_IMPL_RELEASE_INHERITED(TaskQueue, SupportsThreadSafeWeakPtr) +NS_IMPL_QUERY_INTERFACE(TaskQueue, nsIDirectTaskDispatcher, + nsISerialEventTarget, nsIEventTarget) + +TaskDispatcher& TaskQueue::TailDispatcher() { + MOZ_ASSERT(IsCurrentThreadIn()); + MOZ_ASSERT(mTailDispatcher); + return *mTailDispatcher; +} + +// Note aRunnable is passed by ref to support conditional ownership transfer. +// See Dispatch() in TaskQueue.h for more details. +nsresult TaskQueue::DispatchLocked(nsCOMPtr& aRunnable, + uint32_t aFlags, DispatchReason aReason) { + mQueueMonitor.AssertCurrentThreadOwns(); + + // Continue to allow dispatches after shutdown until the last message has been + // processed, at which point no more messages will be accepted. + if (mIsShutdown && !mIsRunning) { + return NS_ERROR_UNEXPECTED; + } + + AbstractThread* currentThread; + if (aReason != TailDispatch && (currentThread = GetCurrent()) && + RequiresTailDispatch(currentThread) && + currentThread->IsTailDispatcherAvailable()) { + MOZ_ASSERT(aFlags == NS_DISPATCH_NORMAL, + "Tail dispatch doesn't support flags"); + return currentThread->TailDispatcher().AddTask(this, aRunnable.forget()); + } + + LogRunnable::LogDispatch(aRunnable); + mTasks.Push({std::move(aRunnable), aFlags}); + + if (mIsRunning) { + return NS_OK; + } + RefPtr runner(new Runner(this)); + nsresult rv = mTarget->Dispatch(runner.forget(), aFlags); + if (NS_FAILED(rv)) { + NS_WARNING("Failed to dispatch runnable to run TaskQueue"); + return rv; + } + mIsRunning = true; + + return NS_OK; +} + +nsresult TaskQueue::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MonitorAutoLock mon(mQueueMonitor); + if (mIsShutdown) { + return NS_ERROR_UNEXPECTED; + } + + MOZ_ASSERT(!mShutdownTasks.Contains(aTask)); + mShutdownTasks.AppendElement(aTask); + return NS_OK; +} + +nsresult TaskQueue::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + + MonitorAutoLock mon(mQueueMonitor); + if (mIsShutdown) { + return NS_ERROR_UNEXPECTED; + } + + return mShutdownTasks.RemoveElement(aTask) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +void TaskQueue::AwaitIdle() { + MonitorAutoLock mon(mQueueMonitor); + AwaitIdleLocked(); +} + +void TaskQueue::AwaitIdleLocked() { + // Make sure there are no tasks for this queue waiting in the caller's tail + // dispatcher. + MOZ_ASSERT_IF(AbstractThread::GetCurrent(), + !AbstractThread::GetCurrent()->HasTailTasksFor(this)); + + mQueueMonitor.AssertCurrentThreadOwns(); + MOZ_ASSERT(mIsRunning || mTasks.IsEmpty()); + while (mIsRunning) { + mQueueMonitor.Wait(); + } +} + +void TaskQueue::AwaitShutdownAndIdle() { + MOZ_ASSERT(!IsCurrentThreadIn()); + // Make sure there are no tasks for this queue waiting in the caller's tail + // dispatcher. + MOZ_ASSERT_IF(AbstractThread::GetCurrent(), + !AbstractThread::GetCurrent()->HasTailTasksFor(this)); + + MonitorAutoLock mon(mQueueMonitor); + while (!mIsShutdown) { + mQueueMonitor.Wait(); + } + AwaitIdleLocked(); +} +RefPtr TaskQueue::BeginShutdown() { + // Dispatch any tasks for this queue waiting in the caller's tail dispatcher, + // since this is the last opportunity to do so. + if (AbstractThread* currentThread = AbstractThread::GetCurrent()) { + currentThread->TailDispatchTasksFor(this); + } + + MonitorAutoLock mon(mQueueMonitor); + // Dispatch any cleanup tasks to the queue before we put it into full + // shutdown. + for (auto& task : mShutdownTasks) { + nsCOMPtr runnable{task->AsRunnable()}; + MOZ_ALWAYS_SUCCEEDS( + DispatchLocked(runnable, NS_DISPATCH_NORMAL, TailDispatch)); + } + mShutdownTasks.Clear(); + mIsShutdown = true; + + RefPtr p = mShutdownPromise.Ensure(__func__); + MaybeResolveShutdown(); + mon.NotifyAll(); + return p; +} + +void TaskQueue::MaybeResolveShutdown() { + mQueueMonitor.AssertCurrentThreadOwns(); + if (mIsShutdown && !mIsRunning) { + mShutdownPromise.ResolveIfExists(true, __func__); + // Disconnect from our target as we won't try to dispatch any more events. + mTrackerEntry = nullptr; + mTarget = nullptr; + } +} + +bool TaskQueue::IsEmpty() { + MonitorAutoLock mon(mQueueMonitor); + return mTasks.IsEmpty(); +} + +bool TaskQueue::IsCurrentThreadIn() const { + bool in = mRunningThread == PR_GetCurrentThread(); + return in; +} + +nsresult TaskQueue::Runner::Run() { + TaskStruct event; + { + MonitorAutoLock mon(mQueue->mQueueMonitor); + MOZ_ASSERT(mQueue->mIsRunning); + if (mQueue->mTasks.IsEmpty()) { + mQueue->mIsRunning = false; + mQueue->MaybeResolveShutdown(); + mon.NotifyAll(); + return NS_OK; + } + event = mQueue->mTasks.Pop(); + } + MOZ_ASSERT(event.event); + + // Note that dropping the queue monitor before running the task, and + // taking the monitor again after the task has run ensures we have memory + // fences enforced. This means that if the object we're calling wasn't + // designed to be threadsafe, it will be, provided we're only calling it + // in this task queue. + { + AutoTaskGuard g(mQueue); + SerialEventTargetGuard tg(mQueue); + { + LogRunnable::Run log(event.event); + + AUTO_PROFILE_FOLLOWING_RUNNABLE(event.event); + event.event->Run(); + + // Drop the reference to event. The event will hold a reference to the + // object it's calling, and we don't want to keep it alive, it may be + // making assumptions what holds references to it. This is especially + // the case if the object is waiting for us to shutdown, so that it + // can shutdown (like in the MediaDecoderStateMachine's SHUTDOWN case). + event.event = nullptr; + } + } + + { + MonitorAutoLock mon(mQueue->mQueueMonitor); + if (mQueue->mTasks.IsEmpty()) { + // No more events to run. Exit the task runner. + mQueue->mIsRunning = false; + mQueue->MaybeResolveShutdown(); + mon.NotifyAll(); + return NS_OK; + } + } + + // There's at least one more event that we can run. Dispatch this Runner + // to the target again to ensure it runs again. Note that we don't just + // run in a loop here so that we don't hog the target. This means we may + // run on another thread next time, but we rely on the memory fences from + // mQueueMonitor for thread safety of non-threadsafe tasks. + nsresult rv; + { + MonitorAutoLock mon(mQueue->mQueueMonitor); + rv = mQueue->mTarget->Dispatch( + this, mQueue->mTasks.FirstElement().flags | NS_DISPATCH_AT_END); + } + if (NS_FAILED(rv)) { + // Failed to dispatch, shutdown! + MonitorAutoLock mon(mQueue->mQueueMonitor); + mQueue->mIsRunning = false; + mQueue->mIsShutdown = true; + mQueue->MaybeResolveShutdown(); + mon.NotifyAll(); + } + + return NS_OK; +} + +//----------------------------------------------------------------------------- +// nsIDirectTaskDispatcher +//----------------------------------------------------------------------------- + +NS_IMETHODIMP +TaskQueue::DispatchDirectTask(already_AddRefed aEvent) { + if (!IsCurrentThreadIn()) { + return NS_ERROR_FAILURE; + } + mDirectTasks.AddTask(std::move(aEvent)); + return NS_OK; +} + +NS_IMETHODIMP TaskQueue::DrainDirectTasks() { + if (!IsCurrentThreadIn()) { + return NS_ERROR_FAILURE; + } + mDirectTasks.DrainTasks(); + return NS_OK; +} + +NS_IMETHODIMP TaskQueue::HaveDirectTasks(bool* aValue) { + if (!IsCurrentThreadIn()) { + return NS_ERROR_FAILURE; + } + + *aValue = mDirectTasks.HaveTasks(); + return NS_OK; +} + +nsTArray> TaskQueueTracker::GetAllTrackedTaskQueues() { + MutexAutoLock lock(mMutex); + nsTArray> queues; + for (auto* entry : mEntries) { + if (auto queue = entry->GetQueue()) { + queues.AppendElement(queue); + } + } + return queues; +} + +TaskQueueTracker::~TaskQueueTracker() = default; + +} // namespace mozilla diff --git a/xpcom/threads/TaskQueue.h b/xpcom/threads/TaskQueue.h new file mode 100644 index 0000000000..0d99d385d3 --- /dev/null +++ b/xpcom/threads/TaskQueue.h @@ -0,0 +1,276 @@ +/* -*- 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 TaskQueue_h_ +#define TaskQueue_h_ + +#include "mozilla/AbstractThread.h" +#include "mozilla/Maybe.h" +#include "mozilla/Monitor.h" +#include "mozilla/MozPromise.h" +#include "mozilla/Queue.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TaskDispatcher.h" +#include "mozilla/ThreadSafeWeakPtr.h" +#include "nsIDirectTaskDispatcher.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +typedef MozPromise ShutdownPromise; + +class TaskQueueTrackerEntry; + +// Abstracts executing runnables in order on an arbitrary event target. The +// runnables dispatched to the TaskQueue will be executed in the order in which +// they're received, and are guaranteed to not be executed concurrently. +// They may be executed on different threads, and a memory barrier is used +// to make this threadsafe for objects that aren't already threadsafe. +// +// Note, since a TaskQueue can also be converted to an nsIEventTarget using +// WrapAsEventTarget() its possible to construct a hierarchy of TaskQueues. +// Consider these three TaskQueues: +// +// TQ1 dispatches to the main thread +// TQ2 dispatches to TQ1 +// TQ3 dispatches to TQ1 +// +// This ensures there is only ever a single runnable from the entire chain on +// the main thread. It also ensures that TQ2 and TQ3 only have a single +// runnable in TQ1 at any time. +// +// This arrangement lets you prioritize work by dispatching runnables directly +// to TQ1. You can issue many runnables for important work. Meanwhile the TQ2 +// and TQ3 work will always execute at most one runnable and then yield. +// +// A TaskQueue does not require explicit shutdown, however it provides a +// BeginShutdown() method that places TaskQueue in a shut down state and returns +// a promise that gets resolved once all pending tasks have completed +class TaskQueue final : public AbstractThread, + public nsIDirectTaskDispatcher, + public SupportsThreadSafeWeakPtr { + class EventTargetWrapper; + + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIDIRECTTASKDISPATCHER + MOZ_DECLARE_REFCOUNTED_TYPENAME(TaskQueue) + + static RefPtr Create(already_AddRefed aTarget, + const char* aName, + bool aSupportsTailDispatch = false); + + TaskDispatcher& TailDispatcher() override; + + NS_IMETHOD Dispatch(already_AddRefed aEvent, + uint32_t aFlags) override { + nsCOMPtr runnable = aEvent; + { + MonitorAutoLock mon(mQueueMonitor); + return DispatchLocked(/* passed by ref */ runnable, aFlags, + NormalDispatch); + } + // If the ownership of |r| is not transferred in DispatchLocked() due to + // dispatch failure, it will be deleted here outside the lock. We do so + // since the destructor of the runnable might access TaskQueue and result + // in deadlocks. + } + + [[nodiscard]] nsresult Dispatch( + already_AddRefed aRunnable, + DispatchReason aReason = NormalDispatch) override { + nsCOMPtr r = aRunnable; + { + MonitorAutoLock mon(mQueueMonitor); + return DispatchLocked(/* passed by ref */ r, NS_DISPATCH_NORMAL, aReason); + } + // If the ownership of |r| is not transferred in DispatchLocked() due to + // dispatch failure, it will be deleted here outside the lock. We do so + // since the destructor of the runnable might access TaskQueue and result + // in deadlocks. + } + + // So we can access nsIEventTarget::Dispatch(nsIRunnable*, uint32_t aFlags) + using nsIEventTarget::Dispatch; + + NS_IMETHOD RegisterShutdownTask(nsITargetShutdownTask* aTask) override; + NS_IMETHOD UnregisterShutdownTask(nsITargetShutdownTask* aTask) override; + + using CancelPromise = MozPromise; + + // Puts the queue in a shutdown state and returns immediately. The queue will + // remain alive at least until all the events are drained, because the Runners + // hold a strong reference to the task queue, and one of them is always held + // by the target event queue when the task queue is non-empty. + // + // The returned promise is resolved when the queue goes empty. + RefPtr BeginShutdown(); + + // Blocks until all task finish executing. + void AwaitIdle(); + + // Blocks until the queue is flagged for shutdown and all tasks have finished + // executing. + void AwaitShutdownAndIdle(); + + bool IsEmpty(); + + // Returns true if the current thread is currently running a Runnable in + // the task queue. + bool IsCurrentThreadIn() const override; + using nsISerialEventTarget::IsOnCurrentThread; + + private: + friend class SupportsThreadSafeWeakPtr; + + TaskQueue(already_AddRefed aTarget, const char* aName, + bool aSupportsTailDispatch); + + virtual ~TaskQueue(); + + // Blocks until all task finish executing. Called internally by methods + // that need to wait until the task queue is idle. + // mQueueMonitor must be held. + void AwaitIdleLocked(); + + nsresult DispatchLocked(nsCOMPtr& aRunnable, uint32_t aFlags, + DispatchReason aReason = NormalDispatch); + + void MaybeResolveShutdown(); + + nsCOMPtr mTarget MOZ_GUARDED_BY(mQueueMonitor); + + // Handle for this TaskQueue being registered with our target if it implements + // TaskQueueTracker. + UniquePtr mTrackerEntry MOZ_GUARDED_BY(mQueueMonitor); + + // Monitor that protects the queue, mIsRunning, mIsShutdown and + // mShutdownTasks; + Monitor mQueueMonitor; + + typedef struct TaskStruct { + nsCOMPtr event; + uint32_t flags; + } TaskStruct; + + // Queue of tasks to run. + Queue mTasks MOZ_GUARDED_BY(mQueueMonitor); + + // List of tasks to run during shutdown. + nsTArray> mShutdownTasks + MOZ_GUARDED_BY(mQueueMonitor); + + // The thread currently running the task queue. We store a reference + // to this so that IsCurrentThreadIn() can tell if the current thread + // is the thread currently running in the task queue. + // + // This may be read on any thread, but may only be written on mRunningThread. + // The thread can't die while we're running in it, and we only use it for + // pointer-comparison with the current thread anyway - so we make it atomic + // and don't refcount it. + Atomic mRunningThread; + + // RAII class that gets instantiated for each dispatched task. + class AutoTaskGuard { + public: + explicit AutoTaskGuard(TaskQueue* aQueue) + : mQueue(aQueue), mLastCurrentThread(nullptr) { + // NB: We don't hold the lock to aQueue here. Don't do anything that + // might require it. + MOZ_ASSERT(!mQueue->mTailDispatcher); + mTaskDispatcher.emplace(aQueue, + /* aIsTailDispatcher = */ true); + mQueue->mTailDispatcher = mTaskDispatcher.ptr(); + + mLastCurrentThread = sCurrentThreadTLS.get(); + sCurrentThreadTLS.set(aQueue); + + MOZ_ASSERT(mQueue->mRunningThread == nullptr); + mQueue->mRunningThread = PR_GetCurrentThread(); + } + + ~AutoTaskGuard() { + mTaskDispatcher->DrainDirectTasks(); + mTaskDispatcher.reset(); + + MOZ_ASSERT(mQueue->mRunningThread == PR_GetCurrentThread()); + mQueue->mRunningThread = nullptr; + + sCurrentThreadTLS.set(mLastCurrentThread); + mQueue->mTailDispatcher = nullptr; + } + + private: + Maybe mTaskDispatcher; + TaskQueue* mQueue; + AbstractThread* mLastCurrentThread; + }; + + TaskDispatcher* mTailDispatcher; + + // True if we've dispatched an event to the target to execute events from + // the queue. + bool mIsRunning MOZ_GUARDED_BY(mQueueMonitor); + + // True if we've started our shutdown process. + bool mIsShutdown MOZ_GUARDED_BY(mQueueMonitor); + MozPromiseHolder mShutdownPromise + MOZ_GUARDED_BY(mQueueMonitor); + + // The name of this TaskQueue. Useful when debugging dispatch failures. + const char* const mName; + + SimpleTaskQueue mDirectTasks; + + class Runner : public Runnable { + public: + explicit Runner(TaskQueue* aQueue) + : Runnable("TaskQueue::Runner"), mQueue(aQueue) {} + NS_IMETHOD Run() override; + + private: + RefPtr mQueue; + }; +}; + +#define MOZILLA_TASKQUEUETRACKER_IID \ + { \ + 0x765c4b56, 0xd5f6, 0x4a9f, { \ + 0x91, 0xcf, 0x51, 0x47, 0xb3, 0xc1, 0x7e, 0xa6 \ + } \ + } + +// XPCOM "interface" which may be implemented by nsIEventTarget implementations +// which want to keep track of what TaskQueue instances are currently targeting +// them. This may be used to asynchronously shutdown TaskQueues targeting a +// threadpool or other event target before the threadpool goes away. +// +// This explicitly TaskQueue-aware tracker is used instead of +// `nsITargetShutdownTask` as the operations required to shut down a TaskQueue +// are asynchronous, which is not a requirement of that interface. +class TaskQueueTracker : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_TASKQUEUETRACKER_IID) + + // Get a strong reference to every TaskQueue currently tracked by this + // TaskQueueTracker. May be called from any thraed. + nsTArray> GetAllTrackedTaskQueues(); + + protected: + virtual ~TaskQueueTracker(); + + private: + friend class TaskQueueTrackerEntry; + + Mutex mMutex{"TaskQueueTracker"}; + LinkedList mEntries MOZ_GUARDED_BY(mMutex); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(TaskQueueTracker, MOZILLA_TASKQUEUETRACKER_IID) + +} // namespace mozilla + +#endif // TaskQueue_h_ diff --git a/xpcom/threads/ThreadBound.h b/xpcom/threads/ThreadBound.h new file mode 100644 index 0000000000..4d5e0088b5 --- /dev/null +++ b/xpcom/threads/ThreadBound.h @@ -0,0 +1,143 @@ +/* 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/. */ + +// A class for values only accessible from a single designated thread. + +#ifndef mozilla_ThreadBound_h +#define mozilla_ThreadBound_h + +#include "mozilla/Atomics.h" +#include "prthread.h" + +#include + +namespace mozilla { + +template +class ThreadBound; + +namespace detail { + +template +struct AddConstIf { + using type = T; +}; + +template +struct AddConstIf { + using type = typename std::add_const::type; +}; + +} // namespace detail + +// A ThreadBound is a T that can only be accessed by a specific +// thread. To enforce this rule, the inner T is only accessible +// through a non-copyable, immovable accessor object. +// Given a ThreadBound threadBoundData, it can be accessed like so: +// +// auto innerData = threadBoundData.Access(); +// innerData->DoStuff(); +// +// Trying to access a ThreadBound from a different thread will +// trigger a MOZ_DIAGNOSTIC_ASSERT. +// The encapsulated T is constructed during the construction of the +// enclosing ThreadBound by forwarding all of the latter's +// constructor parameters to the former. A newly constructed +// ThreadBound is bound to the thread it's constructed in. It's +// possible to rebind the data to some otherThread by calling +// +// threadBoundData.Transfer(otherThread); +// +// on the thread that threadBoundData is currently bound to, as long +// as it's not currently being accessed. (Trying to rebind from +// another thread or while an accessor exists will trigger an +// assertion.) +// +// Note: A ThreadBound may be destructed from any thread, not just +// its designated thread at the time the destructor is invoked. +template +class ThreadBound final { + public: + template + explicit ThreadBound(Args&&... aArgs) + : mData(std::forward(aArgs)...), + mThread(PR_GetCurrentThread()), + mAccessCount(0) {} + + ~ThreadBound() { AssertIsNotCurrentlyAccessed(); } + + void Transfer(const PRThread* const aDest) { + AssertIsCorrectThread(); + AssertIsNotCurrentlyAccessed(); + mThread = aDest; + } + + private: + T mData; + + // This member is (potentially) accessed by multiple threads and is + // thus the first point of synchronization between them. + Atomic mThread; + + // In order to support nested accesses (e.g. from different stack + // frames) it's necessary to maintain a counter of the existing + // accessor. Since it's possible to access a const ThreadBound, the + // counter is mutable. It's atomic because accessing it synchronizes + // access to mData (see comment in Accessor's constructor). + using AccessCountType = Atomic; + mutable AccessCountType mAccessCount; + + public: + template + class MOZ_STACK_CLASS Accessor final { + using DataType = typename detail::AddConstIf::type; + + public: + explicit Accessor( + typename detail::AddConstIf::type& aThreadBound) + : mData(aThreadBound.mData), mAccessCount(aThreadBound.mAccessCount) { + aThreadBound.AssertIsCorrectThread(); + + // This load/store serves as a memory fence that guards mData + // against accesses that would trip the thread assertion. + // (Otherwise one of the loads in the caller's instruction + // stream might be scheduled before the assertion.) + ++mAccessCount; + } + + Accessor(const Accessor&) = delete; + Accessor(Accessor&&) = delete; + Accessor& operator=(const Accessor&) = delete; + Accessor& operator=(Accessor&&) = delete; + + ~Accessor() { --mAccessCount; } + + DataType* operator->() { return &mData; } + + private: + DataType& mData; + AccessCountType& mAccessCount; + }; + + auto Access() { return Accessor{*this}; } + + auto Access() const { return Accessor{*this}; } + + private: + bool IsCorrectThread() const { return mThread == PR_GetCurrentThread(); } + + bool IsNotCurrentlyAccessed() const { return mAccessCount == 0; } + +#define MOZ_DEFINE_THREAD_BOUND_ASSERT(predicate) \ + void Assert##predicate() const { MOZ_DIAGNOSTIC_ASSERT(predicate()); } + + MOZ_DEFINE_THREAD_BOUND_ASSERT(IsCorrectThread) + MOZ_DEFINE_THREAD_BOUND_ASSERT(IsNotCurrentlyAccessed) + +#undef MOZ_DEFINE_THREAD_BOUND_ASSERT +}; + +} // namespace mozilla + +#endif // mozilla_ThreadBound_h diff --git a/xpcom/threads/ThreadDelay.cpp b/xpcom/threads/ThreadDelay.cpp new file mode 100644 index 0000000000..1c38e25510 --- /dev/null +++ b/xpcom/threads/ThreadDelay.cpp @@ -0,0 +1,38 @@ +/* -*- 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 "ThreadDelay.h" +#include "mozilla/Assertions.h" +#include "mozilla/ChaosMode.h" + +#if defined(XP_WIN) +# include +#else +# include +#endif + +namespace mozilla { + +void DelayForChaosMode(ChaosFeature aFeature, + const uint32_t aMicrosecondLimit) { + if (!ChaosMode::isActive(aFeature)) { + return; + } + + MOZ_ASSERT(aMicrosecondLimit <= 1000); +#if defined(XP_WIN) + // Windows doesn't support sleeping at less than millisecond resolution. + // We could spin here, or we could just sleep for one millisecond. + // Sleeping for a full millisecond causes heavy delays, so we don't do + // anything here for now until we have found a good way to sleep more + // precisely here. +#else + const uint32_t duration = ChaosMode::randomUint32LessThan(aMicrosecondLimit); + ::usleep(duration); +#endif +} + +} // namespace mozilla diff --git a/xpcom/threads/ThreadDelay.h b/xpcom/threads/ThreadDelay.h new file mode 100644 index 0000000000..5cfc116e4d --- /dev/null +++ b/xpcom/threads/ThreadDelay.h @@ -0,0 +1,16 @@ +/* -*- 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/ChaosMode.h" + +namespace mozilla { + +// Sleep for a random number of microseconds less than aMicrosecondLimit +// if aFeature is enabled. On Windows, the sleep will always be 1 millisecond +// due to platform limitations. +void DelayForChaosMode(ChaosFeature aFeature, const uint32_t aMicrosecondLimit); + +} // namespace mozilla diff --git a/xpcom/threads/ThreadEventQueue.cpp b/xpcom/threads/ThreadEventQueue.cpp new file mode 100644 index 0000000000..dec317beef --- /dev/null +++ b/xpcom/threads/ThreadEventQueue.cpp @@ -0,0 +1,324 @@ +/* -*- 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/ThreadEventQueue.h" +#include "mozilla/EventQueue.h" + +#include "LeakRefPtr.h" +#include "nsComponentManagerUtils.h" +#include "nsITargetShutdownTask.h" +#include "nsIThreadInternal.h" +#include "nsThreadUtils.h" +#include "nsThread.h" +#include "ThreadEventTarget.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/TaskController.h" +#include "mozilla/StaticPrefs_threads.h" + +using namespace mozilla; + +class ThreadEventQueue::NestedSink : public ThreadTargetSink { + public: + NestedSink(EventQueue* aQueue, ThreadEventQueue* aOwner) + : mQueue(aQueue), mOwner(aOwner) {} + + bool PutEvent(already_AddRefed&& aEvent, + EventQueuePriority aPriority) final { + return mOwner->PutEventInternal(std::move(aEvent), aPriority, this); + } + + void Disconnect(const MutexAutoLock& aProofOfLock) final { mQueue = nullptr; } + + nsresult RegisterShutdownTask(nsITargetShutdownTask* aTask) final { + return NS_ERROR_NOT_IMPLEMENTED; + } + nsresult UnregisterShutdownTask(nsITargetShutdownTask* aTask) final { + return NS_ERROR_NOT_IMPLEMENTED; + } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) { + if (mQueue) { + return mQueue->SizeOfIncludingThis(aMallocSizeOf); + } + return 0; + } + + private: + friend class ThreadEventQueue; + + // This is a non-owning reference. It must live at least until Disconnect is + // called to clear it out. + EventQueue* mQueue; + RefPtr mOwner; +}; + +ThreadEventQueue::ThreadEventQueue(UniquePtr aQueue, + bool aIsMainThread) + : mBaseQueue(std::move(aQueue)), + mLock("ThreadEventQueue"), + mEventsAvailable(mLock, "EventsAvail"), + mIsMainThread(aIsMainThread) { + if (aIsMainThread) { + TaskController::Get()->SetConditionVariable(&mEventsAvailable); + } +} + +ThreadEventQueue::~ThreadEventQueue() { MOZ_ASSERT(mNestedQueues.IsEmpty()); } + +bool ThreadEventQueue::PutEvent(already_AddRefed&& aEvent, + EventQueuePriority aPriority) { + return PutEventInternal(std::move(aEvent), aPriority, nullptr); +} + +bool ThreadEventQueue::PutEventInternal(already_AddRefed&& aEvent, + EventQueuePriority aPriority, + NestedSink* aSink) { + // We want to leak the reference when we fail to dispatch it, so that + // we won't release the event in a wrong thread. + LeakRefPtr event(std::move(aEvent)); + nsCOMPtr obs; + + { + // Check if the runnable wants to override the passed-in priority. + // Do this outside the lock, so runnables implemented in JS can QI + // (and possibly GC) outside of the lock. + if (mIsMainThread) { + auto* e = event.get(); // can't do_QueryInterface on LeakRefPtr. + if (nsCOMPtr runnablePrio = do_QueryInterface(e)) { + uint32_t prio = nsIRunnablePriority::PRIORITY_NORMAL; + runnablePrio->GetPriority(&prio); + if (prio == nsIRunnablePriority::PRIORITY_CONTROL) { + aPriority = EventQueuePriority::Control; + } else if (prio == nsIRunnablePriority::PRIORITY_RENDER_BLOCKING) { + aPriority = EventQueuePriority::RenderBlocking; + } else if (prio == nsIRunnablePriority::PRIORITY_VSYNC) { + aPriority = EventQueuePriority::Vsync; + } else if (prio == nsIRunnablePriority::PRIORITY_INPUT_HIGH) { + aPriority = EventQueuePriority::InputHigh; + } else if (prio == nsIRunnablePriority::PRIORITY_MEDIUMHIGH) { + aPriority = EventQueuePriority::MediumHigh; + } else if (prio == nsIRunnablePriority::PRIORITY_DEFERRED_TIMERS) { + aPriority = EventQueuePriority::DeferredTimers; + } else if (prio == nsIRunnablePriority::PRIORITY_IDLE) { + aPriority = EventQueuePriority::Idle; + } else if (prio == nsIRunnablePriority::PRIORITY_LOW) { + aPriority = EventQueuePriority::Low; + } + } + + if (aPriority == EventQueuePriority::Control && + !StaticPrefs::threads_control_event_queue_enabled()) { + aPriority = EventQueuePriority::MediumHigh; + } + } + + MutexAutoLock lock(mLock); + + if (mEventsAreDoomed) { + return false; + } + + if (aSink) { + if (!aSink->mQueue) { + return false; + } + + aSink->mQueue->PutEvent(event.take(), aPriority, lock); + } else { + mBaseQueue->PutEvent(event.take(), aPriority, lock); + } + + mEventsAvailable.Notify(); + + // Make sure to grab the observer before dropping the lock, otherwise the + // event that we just placed into the queue could run and eventually delete + // this nsThread before the calling thread is scheduled again. We would then + // crash while trying to access a dead nsThread. + obs = mObserver; + } + + if (obs) { + obs->OnDispatchedEvent(); + } + + return true; +} + +already_AddRefed ThreadEventQueue::GetEvent( + bool aMayWait, mozilla::TimeDuration* aLastEventDelay) { + nsCOMPtr event; + { + // Scope for lock. When we are about to return, we will exit this + // scope so we can do some work after releasing the lock but + // before returning. + MutexAutoLock lock(mLock); + + for (;;) { + const bool noNestedQueue = mNestedQueues.IsEmpty(); + if (noNestedQueue) { + event = mBaseQueue->GetEvent(lock, aLastEventDelay); + } else { + // We always get events from the topmost queue when there are nested + // queues. + event = + mNestedQueues.LastElement().mQueue->GetEvent(lock, aLastEventDelay); + } + + if (event) { + break; + } + + // No runnable available. Sleep waiting for one if if we're supposed to. + // Otherwise just go ahead and return null. + if (!aMayWait) { + break; + } + + AUTO_PROFILER_LABEL("ThreadEventQueue::GetEvent::Wait", IDLE); + mEventsAvailable.Wait(); + } + } + + return event.forget(); +} + +bool ThreadEventQueue::HasPendingEvent() { + MutexAutoLock lock(mLock); + + // We always get events from the topmost queue when there are nested queues. + if (mNestedQueues.IsEmpty()) { + return mBaseQueue->HasReadyEvent(lock); + } else { + return mNestedQueues.LastElement().mQueue->HasReadyEvent(lock); + } +} + +bool ThreadEventQueue::ShutdownIfNoPendingEvents() { + MutexAutoLock lock(mLock); + if (mNestedQueues.IsEmpty() && mBaseQueue->IsEmpty(lock)) { + mEventsAreDoomed = true; + return true; + } + return false; +} + +already_AddRefed ThreadEventQueue::PushEventQueue() { + auto queue = MakeUnique(); + RefPtr sink = new NestedSink(queue.get(), this); + RefPtr eventTarget = + new ThreadEventTarget(sink, NS_IsMainThread(), false); + + MutexAutoLock lock(mLock); + + mNestedQueues.AppendElement(NestedQueueItem(std::move(queue), eventTarget)); + return eventTarget.forget(); +} + +void ThreadEventQueue::PopEventQueue(nsIEventTarget* aTarget) { + MutexAutoLock lock(mLock); + + MOZ_ASSERT(!mNestedQueues.IsEmpty()); + + NestedQueueItem& item = mNestedQueues.LastElement(); + + MOZ_ASSERT(aTarget == item.mEventTarget); + + // Disconnect the event target that will be popped. + item.mEventTarget->Disconnect(lock); + + EventQueue* prevQueue = + mNestedQueues.Length() == 1 + ? mBaseQueue.get() + : mNestedQueues[mNestedQueues.Length() - 2].mQueue.get(); + + // Move events from the old queue to the new one. + nsCOMPtr event; + TimeDuration delay; + while ((event = item.mQueue->GetEvent(lock, &delay))) { + // preserve the event delay so far + prevQueue->PutEvent(event.forget(), EventQueuePriority::Normal, lock, + &delay); + } + + mNestedQueues.RemoveLastElement(); +} + +size_t ThreadEventQueue::SizeOfExcludingThis( + mozilla::MallocSizeOf aMallocSizeOf) { + size_t n = 0; + + { + MutexAutoLock lock(mLock); + n += mBaseQueue->SizeOfIncludingThis(aMallocSizeOf); + n += mNestedQueues.ShallowSizeOfExcludingThis(aMallocSizeOf); + for (auto& queue : mNestedQueues) { + n += queue.mEventTarget->SizeOfIncludingThis(aMallocSizeOf); + } + } + + return SynchronizedEventQueue::SizeOfExcludingThis(aMallocSizeOf) + n; +} + +already_AddRefed ThreadEventQueue::GetObserver() { + MutexAutoLock lock(mLock); + return do_AddRef(mObserver); +} + +already_AddRefed ThreadEventQueue::GetObserverOnThread() + MOZ_NO_THREAD_SAFETY_ANALYSIS { + // only written on this thread + return do_AddRef(mObserver); +} + +void ThreadEventQueue::SetObserver(nsIThreadObserver* aObserver) { + // Always called from the thread - single writer. + nsCOMPtr observer = aObserver; + { + MutexAutoLock lock(mLock); + mObserver.swap(observer); + } + if (NS_IsMainThread()) { + TaskController::Get()->SetThreadObserver(aObserver); + } +} + +nsresult ThreadEventQueue::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + MutexAutoLock lock(mLock); + if (mEventsAreDoomed || mShutdownTasksRun) { + return NS_ERROR_UNEXPECTED; + } + MOZ_ASSERT(!mShutdownTasks.Contains(aTask)); + mShutdownTasks.AppendElement(aTask); + return NS_OK; +} + +nsresult ThreadEventQueue::UnregisterShutdownTask( + nsITargetShutdownTask* aTask) { + NS_ENSURE_ARG(aTask); + MutexAutoLock lock(mLock); + if (mEventsAreDoomed || mShutdownTasksRun) { + return NS_ERROR_UNEXPECTED; + } + return mShutdownTasks.RemoveElement(aTask) ? NS_OK : NS_ERROR_UNEXPECTED; +} + +void ThreadEventQueue::RunShutdownTasks() { + nsTArray> shutdownTasks; + { + MutexAutoLock lock(mLock); + shutdownTasks = std::move(mShutdownTasks); + mShutdownTasks.Clear(); + mShutdownTasksRun = true; + } + for (auto& task : shutdownTasks) { + task->TargetShutdown(); + } +} + +ThreadEventQueue::NestedQueueItem::NestedQueueItem( + UniquePtr aQueue, ThreadEventTarget* aEventTarget) + : mQueue(std::move(aQueue)), mEventTarget(aEventTarget) {} diff --git a/xpcom/threads/ThreadEventQueue.h b/xpcom/threads/ThreadEventQueue.h new file mode 100644 index 0000000000..5244acb3dc --- /dev/null +++ b/xpcom/threads/ThreadEventQueue.h @@ -0,0 +1,95 @@ +/* -*- 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 mozilla_ThreadEventQueue_h +#define mozilla_ThreadEventQueue_h + +#include "mozilla/EventQueue.h" +#include "mozilla/CondVar.h" +#include "mozilla/SynchronizedEventQueue.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" + +class nsIEventTarget; +class nsISerialEventTarget; +class nsIThreadObserver; + +namespace mozilla { + +class EventQueue; +class ThreadEventTarget; + +// A ThreadEventQueue implements normal monitor-style synchronization over the +// EventQueue. It also implements PushEventQueue and PopEventQueue for workers +// (see the documentation below for an explanation of those). All threads use a +// ThreadEventQueue as their event queue. Although for the main thread this +// simply forwards events to the TaskController. +class ThreadEventQueue final : public SynchronizedEventQueue { + public: + explicit ThreadEventQueue(UniquePtr aQueue, + bool aIsMainThread = false); + + bool PutEvent(already_AddRefed&& aEvent, + EventQueuePriority aPriority) final; + + already_AddRefed GetEvent( + bool aMayWait, mozilla::TimeDuration* aLastEventDelay = nullptr) final; + bool HasPendingEvent() final; + + bool ShutdownIfNoPendingEvents() final; + + void Disconnect(const MutexAutoLock& aProofOfLock) final {} + + nsresult RegisterShutdownTask(nsITargetShutdownTask* aTask) final; + nsresult UnregisterShutdownTask(nsITargetShutdownTask* aTask) final; + void RunShutdownTasks() final; + + already_AddRefed PushEventQueue() final; + void PopEventQueue(nsIEventTarget* aTarget) final; + + already_AddRefed GetObserver() final; + already_AddRefed GetObserverOnThread() final; + void SetObserver(nsIThreadObserver* aObserver) final; + + Mutex& MutexRef() { return mLock; } + + size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) override; + + private: + class NestedSink; + + virtual ~ThreadEventQueue(); + + bool PutEventInternal(already_AddRefed&& aEvent, + EventQueuePriority aPriority, NestedSink* aQueue); + + const UniquePtr mBaseQueue MOZ_GUARDED_BY(mLock); + + struct NestedQueueItem { + UniquePtr mQueue; + RefPtr mEventTarget; + + NestedQueueItem(UniquePtr aQueue, + ThreadEventTarget* aEventTarget); + }; + + nsTArray mNestedQueues MOZ_GUARDED_BY(mLock); + + Mutex mLock; + CondVar mEventsAvailable MOZ_GUARDED_BY(mLock); + + bool mEventsAreDoomed MOZ_GUARDED_BY(mLock) = false; + nsCOMPtr mObserver MOZ_GUARDED_BY(mLock); + nsTArray> mShutdownTasks + MOZ_GUARDED_BY(mLock); + bool mShutdownTasksRun MOZ_GUARDED_BY(mLock) = false; + + const bool mIsMainThread; +}; + +} // namespace mozilla + +#endif // mozilla_ThreadEventQueue_h diff --git a/xpcom/threads/ThreadEventTarget.cpp b/xpcom/threads/ThreadEventTarget.cpp new file mode 100644 index 0000000000..17844d4886 --- /dev/null +++ b/xpcom/threads/ThreadEventTarget.cpp @@ -0,0 +1,137 @@ +/* -*- 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 "ThreadEventTarget.h" +#include "mozilla/ThreadEventQueue.h" + +#include "LeakRefPtr.h" +#include "mozilla/DelayedRunnable.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/TimeStamp.h" +#include "nsComponentManagerUtils.h" +#include "nsITimer.h" +#include "nsThreadManager.h" +#include "nsThreadSyncDispatch.h" +#include "nsThreadUtils.h" +#include "ThreadDelay.h" + +using namespace mozilla; + +#ifdef DEBUG +// This flag will be set right after XPCOMShutdownThreads finished but before +// we continue with other processing. It is exclusively meant to prime the +// assertion of ThreadEventTarget::Dispatch as early as possible. +// Please use AppShutdown::IsInOrBeyond(ShutdownPhase::???) +// elsewhere to check for shutdown phases. +static mozilla::Atomic + gXPCOMThreadsShutDownNotified(false); +#endif + +ThreadEventTarget::ThreadEventTarget(ThreadTargetSink* aSink, + bool aIsMainThread, bool aBlockDispatch) + : mSink(aSink), +#ifdef DEBUG + mIsMainThread(aIsMainThread), +#endif + mBlockDispatch(aBlockDispatch) { + mThread = PR_GetCurrentThread(); +} + +ThreadEventTarget::~ThreadEventTarget() = default; + +void ThreadEventTarget::SetCurrentThread(PRThread* aThread) { + mThread = aThread; +} + +void ThreadEventTarget::ClearCurrentThread() { mThread = nullptr; } + +NS_IMPL_ISUPPORTS(ThreadEventTarget, nsIEventTarget, nsISerialEventTarget) + +NS_IMETHODIMP +ThreadEventTarget::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) { + return Dispatch(do_AddRef(aRunnable), aFlags); +} + +#ifdef DEBUG +// static +void ThreadEventTarget::XPCOMShutdownThreadsNotificationFinished() { + gXPCOMThreadsShutDownNotified = true; +} +#endif + +NS_IMETHODIMP +ThreadEventTarget::Dispatch(already_AddRefed aEvent, + uint32_t aFlags) { + // We want to leak the reference when we fail to dispatch it, so that + // we won't release the event in a wrong thread. + LeakRefPtr event(std::move(aEvent)); + if (NS_WARN_IF(!event)) { + return NS_ERROR_INVALID_ARG; + } + + NS_ASSERTION(!gXPCOMThreadsShutDownNotified || mIsMainThread || + PR_GetCurrentThread() == mThread || + (aFlags & NS_DISPATCH_IGNORE_BLOCK_DISPATCH), + "Dispatch to non-main thread after xpcom-shutdown-threads"); + + if (mBlockDispatch && !(aFlags & NS_DISPATCH_IGNORE_BLOCK_DISPATCH)) { + MOZ_DIAGNOSTIC_ASSERT( + false, + "Attempt to dispatch to thread which does not usually process " + "dispatched runnables until shutdown"); + return NS_ERROR_NOT_IMPLEMENTED; + } + + LogRunnable::LogDispatch(event.get()); + + NS_ASSERTION((aFlags & (NS_DISPATCH_AT_END | + NS_DISPATCH_IGNORE_BLOCK_DISPATCH)) == aFlags, + "unexpected dispatch flags"); + if (!mSink->PutEvent(event.take(), EventQueuePriority::Normal)) { + return NS_ERROR_UNEXPECTED; + } + // Delay to encourage the receiving task to run before we do work. + DelayForChaosMode(ChaosFeature::TaskDispatching, 1000); + return NS_OK; +} + +NS_IMETHODIMP +ThreadEventTarget::DelayedDispatch(already_AddRefed aEvent, + uint32_t aDelayMs) { + nsCOMPtr event = aEvent; + NS_ENSURE_TRUE(!!aDelayMs, NS_ERROR_UNEXPECTED); + + RefPtr r = + new DelayedRunnable(do_AddRef(this), event.forget(), aDelayMs); + nsresult rv = r->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + return Dispatch(r.forget(), NS_DISPATCH_NORMAL); +} + +NS_IMETHODIMP +ThreadEventTarget::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return mSink->RegisterShutdownTask(aTask); +} + +NS_IMETHODIMP +ThreadEventTarget::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + return mSink->UnregisterShutdownTask(aTask); +} + +NS_IMETHODIMP +ThreadEventTarget::IsOnCurrentThread(bool* aIsOnCurrentThread) { + *aIsOnCurrentThread = IsOnCurrentThread(); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +ThreadEventTarget::IsOnCurrentThreadInfallible() { + // This method is only going to be called if `mThread` is null, which + // only happens when the thread has exited the event loop. Therefore, when + // we are called, we can never be on this thread. + return false; +} diff --git a/xpcom/threads/ThreadEventTarget.h b/xpcom/threads/ThreadEventTarget.h new file mode 100644 index 0000000000..b78411aa80 --- /dev/null +++ b/xpcom/threads/ThreadEventTarget.h @@ -0,0 +1,63 @@ +/* -*- 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 mozilla_ThreadEventTarget_h +#define mozilla_ThreadEventTarget_h + +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "mozilla/SynchronizedEventQueue.h" // for ThreadTargetSink +#include "nsISerialEventTarget.h" + +namespace mozilla { +class DelayedRunnable; + +// ThreadEventTarget handles the details of posting an event to a thread. It can +// be used with any ThreadTargetSink implementation. +class ThreadEventTarget final : public nsISerialEventTarget { + public: + ThreadEventTarget(ThreadTargetSink* aSink, bool aIsMainThread, + bool aBlockDispatch); + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + + // Disconnects the target so that it can no longer post events. + void Disconnect(const MutexAutoLock& aProofOfLock) { + mSink->Disconnect(aProofOfLock); + } + + // Sets the thread for which IsOnCurrentThread returns true to the current + // thread. + void SetCurrentThread(PRThread* aThread); + // Call ClearCurrentThread() before the PRThread is deleted on thread join. + void ClearCurrentThread(); + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + if (mSink) { + n += mSink->SizeOfIncludingThis(aMallocSizeOf); + } + return aMallocSizeOf(this) + n; + } + +#ifdef DEBUG + static void XPCOMShutdownThreadsNotificationFinished(); +#endif + + private: + ~ThreadEventTarget(); + + RefPtr mSink; +#ifdef DEBUG + const bool mIsMainThread; +#endif + const bool mBlockDispatch; +}; + +} // namespace mozilla + +#endif // mozilla_ThreadEventTarget_h diff --git a/xpcom/threads/ThreadLocalVariables.cpp b/xpcom/threads/ThreadLocalVariables.cpp new file mode 100644 index 0000000000..606c0c6384 --- /dev/null +++ b/xpcom/threads/ThreadLocalVariables.cpp @@ -0,0 +1,16 @@ +/* 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/ThreadLocal.h" + +// This variable is used to ensure creating new URI doesn't put us in an +// infinite loop +MOZ_THREAD_LOCAL(uint32_t) gTlsURLRecursionCount; + +void InitThreadLocalVariables() { + if (!gTlsURLRecursionCount.init()) { + MOZ_CRASH("Could not init gTlsURLRecursionCount"); + } + gTlsURLRecursionCount.set(0); +} diff --git a/xpcom/threads/ThrottledEventQueue.cpp b/xpcom/threads/ThrottledEventQueue.cpp new file mode 100644 index 0000000000..9e4219b305 --- /dev/null +++ b/xpcom/threads/ThrottledEventQueue.cpp @@ -0,0 +1,459 @@ +/* -*- 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 "ThrottledEventQueue.h" + +#include "mozilla/Atomics.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CondVar.h" +#include "mozilla/EventQueue.h" +#include "mozilla/Mutex.h" +#include "mozilla/Unused.h" +#include "nsThreadUtils.h" + +namespace mozilla { + +namespace {} // anonymous namespace + +// The ThrottledEventQueue is designed with inner and outer objects: +// +// XPCOM code base event target +// | | +// v v +// +-------+ +--------+ +// | Outer | +-->|executor| +// +-------+ | +--------+ +// | | | +// | +-------+ | +// +-->| Inner |<--+ +// +-------+ +// +// Client code references the outer nsIEventTarget which in turn references +// an inner object, which actually holds the queue of runnables. +// +// Whenever the queue is non-empty (and not paused), it keeps an "executor" +// runnable dispatched to the base event target. Each time the executor is run, +// it draws the next event from Inner's queue and runs it. If that queue has +// more events, the executor is dispatched to the base again. +// +// The executor holds a strong reference to the Inner object. This means that if +// the outer object is dereferenced and destroyed, the Inner object will remain +// live for as long as the executor exists - that is, until the Inner's queue is +// empty. +// +// A Paused ThrottledEventQueue does not enqueue an executor when new events are +// added. Any executor previously queued on the base event target draws no +// events from a Paused ThrottledEventQueue, and returns without re-enqueueing +// itself. Since there is no executor keeping the Inner object alive until its +// queue is empty, dropping a Paused ThrottledEventQueue may drop the Inner +// while it still owns events. This is the correct behavior: if there are no +// references to it, it will never be Resumed, and thus it will never dispatch +// events again. +// +// Resuming a ThrottledEventQueue must dispatch an executor, so calls to Resume +// are fallible for the same reasons as calls to Dispatch. +// +// The xpcom shutdown process drains the main thread's event queue several +// times, so if a ThrottledEventQueue is being driven by the main thread, it +// should get emptied out by the time we reach the "eventq shutdown" phase. +class ThrottledEventQueue::Inner final : public nsISupports { + // The runnable which is dispatched to the underlying base target. Since + // we only execute one event at a time we just re-use a single instance + // of this class while there are events left in the queue. + class Executor final : public Runnable, public nsIRunnablePriority { + // The Inner whose runnables we execute. mInner->mExecutor points + // to this executor, forming a reference loop. + RefPtr mInner; + + ~Executor() = default; + + public: + explicit Executor(Inner* aInner) + : Runnable("ThrottledEventQueue::Inner::Executor"), mInner(aInner) {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHODIMP + Run() override { + mInner->ExecuteRunnable(); + return NS_OK; + } + + NS_IMETHODIMP + GetPriority(uint32_t* aPriority) override { + *aPriority = mInner->mPriority; + return NS_OK; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHODIMP + GetName(nsACString& aName) override { return mInner->CurrentName(aName); } +#endif + }; + + mutable Mutex mMutex; + mutable CondVar mIdleCondVar MOZ_GUARDED_BY(mMutex); + + // As-of-yet unexecuted runnables queued on this ThrottledEventQueue. + // + // Used from any thread; protected by mMutex. Signals mIdleCondVar when + // emptied. + EventQueueSized<64> mEventQueue MOZ_GUARDED_BY(mMutex); + + // The event target we dispatch our events (actually, just our Executor) to. + // + // Written only during construction. Readable by any thread without locking. + const nsCOMPtr mBaseTarget; + + // The Executor that we dispatch to mBaseTarget to draw runnables from our + // queue. mExecutor->mInner points to this Inner, forming a reference loop. + // + // Used from any thread; protected by mMutex. + nsCOMPtr mExecutor MOZ_GUARDED_BY(mMutex); + + const char* const mName; + + const uint32_t mPriority; + + // True if this queue is currently paused. + // Used from any thread; protected by mMutex. + bool mIsPaused MOZ_GUARDED_BY(mMutex); + + explicit Inner(nsISerialEventTarget* aBaseTarget, const char* aName, + uint32_t aPriority) + : mMutex("ThrottledEventQueue"), + mIdleCondVar(mMutex, "ThrottledEventQueue:Idle"), + mBaseTarget(aBaseTarget), + mName(aName), + mPriority(aPriority), + mIsPaused(false) { + MOZ_ASSERT(mName, "Must pass a valid name!"); + } + + ~Inner() { +#ifdef DEBUG + MutexAutoLock lock(mMutex); + + // As long as an executor exists, it had better keep us alive, since it's + // going to call ExecuteRunnable on us. + MOZ_ASSERT(!mExecutor); + + // If we have any events in our queue, there should be an executor queued + // for them, and that should have kept us alive. The exception is that, if + // we're paused, we don't enqueue an executor. + MOZ_ASSERT(mEventQueue.IsEmpty(lock) || IsPaused(lock)); + + // Some runnables are only safe to drop on the main thread, so if our queue + // isn't empty, we'd better be on the main thread. + MOZ_ASSERT_IF(!mEventQueue.IsEmpty(lock), NS_IsMainThread()); +#endif + } + + // Make sure an executor has been queued on our base target. If we already + // have one, do nothing; otherwise, create and dispatch it. + nsresult EnsureExecutor(MutexAutoLock& lock) MOZ_REQUIRES(mMutex) { + if (mExecutor) return NS_OK; + + // Note, this creates a ref cycle keeping the inner alive + // until the queue is drained. + mExecutor = new Executor(this); + nsresult rv = mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + mExecutor = nullptr; + return rv; + } + + return NS_OK; + } + + nsresult CurrentName(nsACString& aName) { + nsCOMPtr event; + +#ifdef DEBUG + bool currentThread = false; + mBaseTarget->IsOnCurrentThread(¤tThread); + MOZ_ASSERT(currentThread); +#endif + + { + MutexAutoLock lock(mMutex); + event = mEventQueue.PeekEvent(lock); + // It is possible that mEventQueue wasn't empty when the executor + // was added to the queue, but someone processed events from mEventQueue + // before the executor, this is why mEventQueue is empty here + if (!event) { + aName.AssignLiteral("no runnables left in the ThrottledEventQueue"); + return NS_OK; + } + } + + if (nsCOMPtr named = do_QueryInterface(event)) { + nsresult rv = named->GetName(aName); + return rv; + } + + aName.AssignASCII(mName); + return NS_OK; + } + + void ExecuteRunnable() { + // Any thread + nsCOMPtr event; + +#ifdef DEBUG + bool currentThread = false; + mBaseTarget->IsOnCurrentThread(¤tThread); + MOZ_ASSERT(currentThread); +#endif + + { + MutexAutoLock lock(mMutex); + + // Normally, a paused queue doesn't dispatch any executor, but we might + // have been paused after the executor was already in flight. There's no + // way to yank the executor out of the base event target, so we just check + // for a paused queue here and return without running anything. We'll + // create a new executor when we're resumed. + if (IsPaused(lock)) { + // Note, this breaks a ref cycle. + mExecutor = nullptr; + return; + } + + // We only dispatch an executor runnable when we know there is something + // in the queue, so this should never fail. + event = mEventQueue.GetEvent(lock); + MOZ_ASSERT(event); + + // If there are more events in the queue, then dispatch the next + // executor. We do this now, before running the event, because + // the event might spin the event loop and we don't want to stall + // the queue. + if (mEventQueue.HasReadyEvent(lock)) { + // Dispatch the next base target runnable to attempt to execute + // the next throttled event. We must do this before executing + // the event in case the event spins the event loop. + MOZ_ALWAYS_SUCCEEDS( + mBaseTarget->Dispatch(mExecutor, NS_DISPATCH_NORMAL)); + } + + // Otherwise the queue is empty and we can stop dispatching the + // executor. + else { + // Break the Executor::mInner / Inner::mExecutor reference loop. + mExecutor = nullptr; + mIdleCondVar.NotifyAll(); + } + } + + // Execute the event now that we have unlocked. + LogRunnable::Run log(event); + Unused << event->Run(); + + // To cover the event's destructor code in the LogRunnable log + event = nullptr; + } + + public: + static already_AddRefed Create(nsISerialEventTarget* aBaseTarget, + const char* aName, uint32_t aPriority) { + MOZ_ASSERT(NS_IsMainThread()); + // FIXME: This assertion only worked when `sCurrentShutdownPhase` was not + // being updated. + // MOZ_ASSERT(ClearOnShutdown_Internal::sCurrentShutdownPhase == + // ShutdownPhase::NotInShutdown); + + RefPtr ref = new Inner(aBaseTarget, aName, aPriority); + return ref.forget(); + } + + bool IsEmpty() const { + // Any thread + return Length() == 0; + } + + uint32_t Length() const { + // Any thread + MutexAutoLock lock(mMutex); + return mEventQueue.Count(lock); + } + + already_AddRefed GetEvent() { + MutexAutoLock lock(mMutex); + return mEventQueue.GetEvent(lock); + } + + void AwaitIdle() const { + // Any thread, except the main thread or our base target. Blocking the + // main thread is forbidden. Blocking the base target is guaranteed to + // produce a deadlock. + MOZ_ASSERT(!NS_IsMainThread()); +#ifdef DEBUG + bool onBaseTarget = false; + Unused << mBaseTarget->IsOnCurrentThread(&onBaseTarget); + MOZ_ASSERT(!onBaseTarget); +#endif + + MutexAutoLock lock(mMutex); + while (mExecutor || IsPaused(lock)) { + mIdleCondVar.Wait(); + } + } + + bool IsPaused() const { + MutexAutoLock lock(mMutex); + return IsPaused(lock); + } + + bool IsPaused(const MutexAutoLock& aProofOfLock) const MOZ_REQUIRES(mMutex) { + return mIsPaused; + } + + nsresult SetIsPaused(bool aIsPaused) { + MutexAutoLock lock(mMutex); + + // If we will be unpaused, and we have events in our queue, make sure we + // have an executor queued on the base event target to run them. Do this + // before we actually change mIsPaused, since this is fallible. + if (!aIsPaused && !mEventQueue.IsEmpty(lock)) { + nsresult rv = EnsureExecutor(lock); + if (NS_FAILED(rv)) { + return rv; + } + } + + mIsPaused = aIsPaused; + return NS_OK; + } + + nsresult DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + // Any thread + nsCOMPtr r = aEvent; + return Dispatch(r.forget(), aFlags); + } + + nsresult Dispatch(already_AddRefed aEvent, uint32_t aFlags) { + MOZ_ASSERT(aFlags == NS_DISPATCH_NORMAL || aFlags == NS_DISPATCH_AT_END); + + // Any thread + MutexAutoLock lock(mMutex); + + if (!IsPaused(lock)) { + // Make sure we have an executor in flight to process events. This is + // fallible, so do it first. Our lock will prevent the executor from + // accessing the event queue before we add the event below. + nsresult rv = EnsureExecutor(lock); + if (NS_FAILED(rv)) return rv; + } + + // Only add the event to the underlying queue if are able to + // dispatch to our base target. + nsCOMPtr event(aEvent); + LogRunnable::LogDispatch(event); + mEventQueue.PutEvent(event.forget(), EventQueuePriority::Normal, lock); + return NS_OK; + } + + nsresult DelayedDispatch(already_AddRefed aEvent, + uint32_t aDelay) { + // The base target may implement this, but we don't. Always fail + // to provide consistent behavior. + return NS_ERROR_NOT_IMPLEMENTED; + } + + nsresult RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return mBaseTarget->RegisterShutdownTask(aTask); + } + + nsresult UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + return mBaseTarget->UnregisterShutdownTask(aTask); + } + + bool IsOnCurrentThread() { return mBaseTarget->IsOnCurrentThread(); } + + NS_DECL_THREADSAFE_ISUPPORTS +}; + +NS_IMPL_ISUPPORTS(ThrottledEventQueue::Inner, nsISupports); + +NS_IMPL_ISUPPORTS_INHERITED(ThrottledEventQueue::Inner::Executor, Runnable, + nsIRunnablePriority) + +NS_IMPL_ISUPPORTS(ThrottledEventQueue, ThrottledEventQueue, nsIEventTarget, + nsISerialEventTarget); + +ThrottledEventQueue::ThrottledEventQueue(already_AddRefed aInner) + : mInner(aInner) { + MOZ_ASSERT(mInner); +} + +already_AddRefed ThrottledEventQueue::Create( + nsISerialEventTarget* aBaseTarget, const char* aName, uint32_t aPriority) { + MOZ_ASSERT(NS_IsMainThread()); + MOZ_ASSERT(aBaseTarget); + + RefPtr inner = Inner::Create(aBaseTarget, aName, aPriority); + + RefPtr ref = new ThrottledEventQueue(inner.forget()); + return ref.forget(); +} + +bool ThrottledEventQueue::IsEmpty() const { return mInner->IsEmpty(); } + +uint32_t ThrottledEventQueue::Length() const { return mInner->Length(); } + +// Get the next runnable from the queue +already_AddRefed ThrottledEventQueue::GetEvent() { + return mInner->GetEvent(); +} + +void ThrottledEventQueue::AwaitIdle() const { return mInner->AwaitIdle(); } + +nsresult ThrottledEventQueue::SetIsPaused(bool aIsPaused) { + return mInner->SetIsPaused(aIsPaused); +} + +bool ThrottledEventQueue::IsPaused() const { return mInner->IsPaused(); } + +NS_IMETHODIMP +ThrottledEventQueue::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + return mInner->DispatchFromScript(aEvent, aFlags); +} + +NS_IMETHODIMP +ThrottledEventQueue::Dispatch(already_AddRefed aEvent, + uint32_t aFlags) { + return mInner->Dispatch(std::move(aEvent), aFlags); +} + +NS_IMETHODIMP +ThrottledEventQueue::DelayedDispatch(already_AddRefed aEvent, + uint32_t aFlags) { + return mInner->DelayedDispatch(std::move(aEvent), aFlags); +} + +NS_IMETHODIMP +ThrottledEventQueue::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return mInner->RegisterShutdownTask(aTask); +} + +NS_IMETHODIMP +ThrottledEventQueue::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + return mInner->UnregisterShutdownTask(aTask); +} + +NS_IMETHODIMP +ThrottledEventQueue::IsOnCurrentThread(bool* aResult) { + *aResult = mInner->IsOnCurrentThread(); + return NS_OK; +} + +NS_IMETHODIMP_(bool) +ThrottledEventQueue::IsOnCurrentThreadInfallible() { + return mInner->IsOnCurrentThread(); +} + +} // namespace mozilla diff --git a/xpcom/threads/ThrottledEventQueue.h b/xpcom/threads/ThrottledEventQueue.h new file mode 100644 index 0000000000..cf37a10a6d --- /dev/null +++ b/xpcom/threads/ThrottledEventQueue.h @@ -0,0 +1,118 @@ +/* -*- 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/. */ + +// nsIEventTarget wrapper for throttling event dispatch. + +#ifndef mozilla_ThrottledEventQueue_h +#define mozilla_ThrottledEventQueue_h + +#include "nsISerialEventTarget.h" + +#define NS_THROTTLEDEVENTQUEUE_IID \ + { \ + 0x8f3cf7dc, 0xfc14, 0x4ad5, { \ + 0x9f, 0xd5, 0xdb, 0x79, 0xbc, 0xe6, 0xd5, 0x08 \ + } \ + } + +namespace mozilla { + +// A ThrottledEventQueue is an event target that can be used to throttle +// events being dispatched to another base target. It maintains its +// own queue of events and only dispatches one at a time to the wrapped +// target. This can be used to avoid flooding the base target. +// +// Flooding is avoided via a very simple principle. Runnables dispatched +// to the ThrottledEventQueue are only dispatched to the base target +// one at a time. Only once that runnable has executed will we dispatch +// the next runnable to the base target. This in effect makes all +// runnables passing through the ThrottledEventQueue yield to other work +// on the base target. +// +// ThrottledEventQueue keeps runnables waiting to be dispatched to the +// base in its own internal queue. Code can query the length of this +// queue using IsEmpty() and Length(). Further, code implement back +// pressure by checking the depth of the queue and deciding to stop +// issuing runnables if they see the ThrottledEventQueue is backed up. +// Code running on other threads could even use AwaitIdle() to block +// all operation until the ThrottledEventQueue drains. +// +// Note, this class is similar to TaskQueue, but also differs in a few +// ways. First, it is a very simple nsIEventTarget implementation. It +// does not use the AbstractThread API. +// +// In addition, ThrottledEventQueue currently dispatches its next +// runnable to the base target *before* running the current event. This +// allows the event code to spin the event loop without stalling the +// ThrottledEventQueue. In contrast, TaskQueue only dispatches its next +// runnable after running the current event. That approach is necessary +// for TaskQueue in order to work with thread pool targets. +// +// So, if you are targeting a thread pool you probably want a TaskQueue. +// If you are targeting a single thread or other non-concurrent event +// target, you probably want a ThrottledEventQueue. +// +// If you drop a ThrottledEventQueue while its queue still has events to be run, +// they will continue to be dispatched as usual to the base. Only once the last +// event has run will all the ThrottledEventQueue's memory be freed. +class ThrottledEventQueue final : public nsISerialEventTarget { + class Inner; + RefPtr mInner; + + explicit ThrottledEventQueue(already_AddRefed aInner); + ~ThrottledEventQueue() = default; + + public: + // Create a ThrottledEventQueue for the given target. + static already_AddRefed Create( + nsISerialEventTarget* aBaseTarget, const char* aName, + uint32_t aPriority = nsIRunnablePriority::PRIORITY_NORMAL); + + // Determine if there are any events pending in the queue. + bool IsEmpty() const; + + // Determine how many events are pending in the queue. + uint32_t Length() const; + + already_AddRefed GetEvent(); + + // Block the current thread until the queue is empty. This may not be called + // on the main thread or the base target. The ThrottledEventQueue must not be + // paused. + void AwaitIdle() const; + + // If |aIsPaused| is true, pause execution of events from this queue. No + // events from this queue will be run until this is called with |aIsPaused| + // false. + // + // To un-pause a ThrottledEventQueue, we need to dispatch a runnable to the + // underlying event target. That operation may fail, so this method is + // fallible as well. + // + // Note that, although ThrottledEventQueue's behavior is descibed as queueing + // events on the base target, an event queued on a TEQ is never actually moved + // to any other queue. What is actually dispatched to the base is an + // "executor" event which, when run, removes an event from the TEQ and runs it + // immediately. This means that you can pause a TEQ even after the executor + // has been queued on the base target, and even so, no events from the TEQ + // will run. When the base target gets around to running the executor, the + // executor will see that the TEQ is paused, and do nothing. + [[nodiscard]] nsresult SetIsPaused(bool aIsPaused); + + // Return true if this ThrottledEventQueue is paused. + bool IsPaused() const; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + + NS_DECLARE_STATIC_IID_ACCESSOR(NS_THROTTLEDEVENTQUEUE_IID); +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(ThrottledEventQueue, NS_THROTTLEDEVENTQUEUE_IID); + +} // namespace mozilla + +#endif // mozilla_ThrottledEventQueue_h diff --git a/xpcom/threads/TimerThread.cpp b/xpcom/threads/TimerThread.cpp new file mode 100644 index 0000000000..d0ff778f20 --- /dev/null +++ b/xpcom/threads/TimerThread.cpp @@ -0,0 +1,1565 @@ +/* -*- 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 "nsTimerImpl.h" +#include "TimerThread.h" + +#include "GeckoProfiler.h" +#include "nsThreadUtils.h" + +#include "nsIObserverService.h" +#include "nsIPropertyBag2.h" +#include "mozilla/Services.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/ArenaAllocator.h" +#include "mozilla/ArrayUtils.h" +#include "mozilla/OperatorNewExtensions.h" +#include "mozilla/StaticPrefs_timer.h" + +#include "mozilla/glean/GleanMetrics.h" + +#include + +using namespace mozilla; + +#ifdef XP_WIN +// Include Windows header required for enabling high-precision timers. +# include +# include + +static constexpr UINT kTimerPeriodHiRes = 1; +static constexpr UINT kTimerPeriodLowRes = 16; + +// Helper functions to determine what Windows timer resolution to target. +static constexpr UINT GetDesiredTimerPeriod(const bool aOnBatteryPower, + const bool aLowProcessPriority) { + const bool useLowResTimer = aOnBatteryPower || aLowProcessPriority; + return useLowResTimer ? kTimerPeriodLowRes : kTimerPeriodHiRes; +} + +static_assert(GetDesiredTimerPeriod(true, false) == kTimerPeriodLowRes); +static_assert(GetDesiredTimerPeriod(false, true) == kTimerPeriodLowRes); +static_assert(GetDesiredTimerPeriod(true, true) == kTimerPeriodLowRes); +static_assert(GetDesiredTimerPeriod(false, false) == kTimerPeriodHiRes); + +UINT TimerThread::ComputeDesiredTimerPeriod() const { + const bool lowPriorityProcess = + mCachedPriority.load(std::memory_order_relaxed) < + hal::PROCESS_PRIORITY_FOREGROUND; + + // NOTE: Using short-circuiting here to avoid call to GetSystemPowerStatus() + // when we know that that result will not affect the final result. (As + // confirmed by the static_assert's above, onBatteryPower does not affect the + // result when the lowPriorityProcess is true.) + SYSTEM_POWER_STATUS status; + const bool onBatteryPower = !lowPriorityProcess && + GetSystemPowerStatus(&status) && + (status.ACLineStatus == 0); + + return GetDesiredTimerPeriod(onBatteryPower, lowPriorityProcess); +} +#endif + +// Uncomment the following line to enable runtime stats during development. +// #define TIMERS_RUNTIME_STATS + +#ifdef TIMERS_RUNTIME_STATS +// This class gathers durations and displays some basic stats when destroyed. +// It is intended to be used as a static variable (see `AUTO_TIMERS_STATS` +// below), to display stats at the end of the program. +class StaticTimersStats { + public: + explicit StaticTimersStats(const char* aName) : mName(aName) {} + + ~StaticTimersStats() { + // Using unsigned long long for computations and printfs. + using ULL = unsigned long long; + ULL n = static_cast(mCount); + if (n == 0) { + printf("[%d] Timers stats `%s`: (nothing)\n", + int(profiler_current_process_id().ToNumber()), mName); + } else if (ULL sumNs = static_cast(mSumDurationsNs); sumNs == 0) { + printf("[%d] Timers stats `%s`: %llu\n", + int(profiler_current_process_id().ToNumber()), mName, n); + } else { + printf("[%d] Timers stats `%s`: %llu ns / %llu = %llu ns, max %llu ns\n", + int(profiler_current_process_id().ToNumber()), mName, sumNs, n, + sumNs / n, static_cast(mLongestDurationNs)); + } + } + + void AddDurationFrom(TimeStamp aStart) { + // Duration between aStart and now, rounded to the nearest nanosecond. + DurationNs duration = static_cast( + (TimeStamp::Now() - aStart).ToMicroseconds() * 1000 + 0.5); + mSumDurationsNs += duration; + ++mCount; + // Update mLongestDurationNs if this one is longer. + for (;;) { + DurationNs longest = mLongestDurationNs; + if (MOZ_LIKELY(longest >= duration)) { + // This duration is not the longest, nothing to do. + break; + } + if (MOZ_LIKELY(mLongestDurationNs.compareExchange(longest, duration))) { + // Successfully updated `mLongestDurationNs` with the new value. + break; + } + // Otherwise someone else just updated `mLongestDurationNs`, we need to + // try again by looping. + } + } + + void AddCount() { + MOZ_ASSERT(mSumDurationsNs == 0, "Don't mix counts and durations"); + ++mCount; + } + + private: + using DurationNs = uint64_t; + using Count = uint32_t; + + Atomic mSumDurationsNs{0}; + Atomic mLongestDurationNs{0}; + Atomic mCount{0}; + const char* mName; +}; + +// RAII object that measures its scoped lifetime duration and reports it to a +// `StaticTimersStats`. +class MOZ_RAII AutoTimersStats { + public: + explicit AutoTimersStats(StaticTimersStats& aStats) + : mStats(aStats), mStart(TimeStamp::Now()) {} + + ~AutoTimersStats() { mStats.AddDurationFrom(mStart); } + + private: + StaticTimersStats& mStats; + TimeStamp mStart; +}; + +// Macro that should be used to collect basic statistics from measurements of +// block durations, from where this macro is, until the end of its enclosing +// scope. The name is used in the static variable name and when displaying stats +// at the end of the program; Another location could use the same name but their +// stats will not be combined, so use different name if these locations should +// be distinguished. +# define AUTO_TIMERS_STATS(name) \ + static ::StaticTimersStats sStat##name(#name); \ + ::AutoTimersStats autoStat##name(sStat##name); + +// This macro only counts the number of times it's used, not durations. +// Don't mix with AUTO_TIMERS_STATS! +# define COUNT_TIMERS_STATS(name) \ + static ::StaticTimersStats sStat##name(#name); \ + sStat##name.AddCount(); + +#else // TIMERS_RUNTIME_STATS + +# define AUTO_TIMERS_STATS(name) +# define COUNT_TIMERS_STATS(name) + +#endif // TIMERS_RUNTIME_STATS else + +NS_IMPL_ISUPPORTS_INHERITED(TimerThread, Runnable, nsIObserver) + +TimerThread::TimerThread() + : Runnable("TimerThread"), + mInitialized(false), + mMonitor("TimerThread.mMonitor"), + mShutdown(false), + mWaiting(false), + mNotified(false), + mSleeping(false), + mAllowedEarlyFiringMicroseconds(0) {} + +TimerThread::~TimerThread() { + mThread = nullptr; + + NS_ASSERTION(mTimers.IsEmpty(), "Timers remain in TimerThread::~TimerThread"); + +#if TIMER_THREAD_STATISTICS + { + MonitorAutoLock lock(mMonitor); + PrintStatistics(); + } +#endif +} + +namespace { + +class TimerObserverRunnable : public Runnable { + public: + explicit TimerObserverRunnable(nsIObserver* aObserver) + : mozilla::Runnable("TimerObserverRunnable"), mObserver(aObserver) {} + + NS_DECL_NSIRUNNABLE + + private: + nsCOMPtr mObserver; +}; + +NS_IMETHODIMP +TimerObserverRunnable::Run() { + nsCOMPtr observerService = + mozilla::services::GetObserverService(); + if (observerService) { + observerService->AddObserver(mObserver, "sleep_notification", false); + observerService->AddObserver(mObserver, "wake_notification", false); + observerService->AddObserver(mObserver, "suspend_process_notification", + false); + observerService->AddObserver(mObserver, "resume_process_notification", + false); + observerService->AddObserver(mObserver, "ipc:process-priority-changed", + false); + } + return NS_OK; +} + +} // namespace + +namespace { + +// TimerEventAllocator is a thread-safe allocator used only for nsTimerEvents. +// It's needed to avoid contention over the default allocator lock when +// firing timer events (see bug 733277). The thread-safety is required because +// nsTimerEvent objects are allocated on the timer thread, and freed on another +// thread. Because TimerEventAllocator has its own lock, contention over that +// lock is limited to the allocation and deallocation of nsTimerEvent objects. +// +// Because this is layered over ArenaAllocator, it never shrinks -- even +// "freed" nsTimerEvents aren't truly freed, they're just put onto a free-list +// for later recycling. So the amount of memory consumed will always be equal +// to the high-water mark consumption. But nsTimerEvents are small and it's +// unusual to have more than a few hundred of them, so this shouldn't be a +// problem in practice. + +class TimerEventAllocator { + private: + struct FreeEntry { + FreeEntry* mNext; + }; + + ArenaAllocator<4096> mPool MOZ_GUARDED_BY(mMonitor); + FreeEntry* mFirstFree MOZ_GUARDED_BY(mMonitor); + mozilla::Monitor mMonitor; + + public: + TimerEventAllocator() + : mFirstFree(nullptr), mMonitor("TimerEventAllocator") {} + + ~TimerEventAllocator() = default; + + void* Alloc(size_t aSize); + void Free(void* aPtr); +}; + +} // namespace + +// This is a nsICancelableRunnable because we can dispatch it to Workers and +// those can be shut down at any time, and in these cases, Cancel() is called +// instead of Run(). +class nsTimerEvent final : public CancelableRunnable { + public: + NS_IMETHOD Run() override; + + nsresult Cancel() override { + mTimer->Cancel(); + return NS_OK; + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHOD GetName(nsACString& aName) override; +#endif + + explicit nsTimerEvent(already_AddRefed aTimer, + ProfilerThreadId aTimerThreadId) + : mozilla::CancelableRunnable("nsTimerEvent"), + mTimer(aTimer), + mGeneration(mTimer->GetGeneration()), + mTimerThreadId(aTimerThreadId) { + // Note: We override operator new for this class, and the override is + // fallible! + sAllocatorUsers++; + + if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug) || + profiler_thread_is_being_profiled_for_markers(mTimerThreadId)) { + mInitTime = TimeStamp::Now(); + } + } + + static void Init(); + static void Shutdown(); + static void DeleteAllocatorIfNeeded(); + + static void* operator new(size_t aSize) noexcept(true) { + return sAllocator->Alloc(aSize); + } + void operator delete(void* aPtr) { + sAllocator->Free(aPtr); + sAllocatorUsers--; + DeleteAllocatorIfNeeded(); + } + + already_AddRefed ForgetTimer() { return mTimer.forget(); } + + private: + nsTimerEvent(const nsTimerEvent&) = delete; + nsTimerEvent& operator=(const nsTimerEvent&) = delete; + nsTimerEvent& operator=(const nsTimerEvent&&) = delete; + + ~nsTimerEvent() { + MOZ_ASSERT(!sCanDeleteAllocator || sAllocatorUsers > 0, + "This will result in us attempting to deallocate the " + "nsTimerEvent allocator twice"); + } + + TimeStamp mInitTime; + RefPtr mTimer; + const int32_t mGeneration; + ProfilerThreadId mTimerThreadId; + + static TimerEventAllocator* sAllocator; + + static Atomic sAllocatorUsers; + static Atomic sCanDeleteAllocator; +}; + +TimerEventAllocator* nsTimerEvent::sAllocator = nullptr; +Atomic nsTimerEvent::sAllocatorUsers; +Atomic nsTimerEvent::sCanDeleteAllocator; + +namespace { + +void* TimerEventAllocator::Alloc(size_t aSize) { + MOZ_ASSERT(aSize == sizeof(nsTimerEvent)); + + mozilla::MonitorAutoLock lock(mMonitor); + + void* p; + if (mFirstFree) { + p = mFirstFree; + mFirstFree = mFirstFree->mNext; + } else { + p = mPool.Allocate(aSize, fallible); + } + + return p; +} + +void TimerEventAllocator::Free(void* aPtr) { + mozilla::MonitorAutoLock lock(mMonitor); + + FreeEntry* entry = reinterpret_cast(aPtr); + + entry->mNext = mFirstFree; + mFirstFree = entry; +} + +} // namespace + +struct TimerMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("Timer"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + uint32_t aDelay, uint8_t aType, + MarkerThreadId aThreadId, bool aCanceled) { + aWriter.IntProperty("delay", aDelay); + if (!aThreadId.IsUnspecified()) { + // Tech note: If `ToNumber()` returns a uint64_t, the conversion to + // int64_t is "implementation-defined" before C++20. This is + // acceptable here, because this is a one-way conversion to a unique + // identifier that's used to visually separate data by thread on the + // front-end. + aWriter.IntProperty( + "threadId", static_cast(aThreadId.ThreadId().ToNumber())); + } + if (aCanceled) { + aWriter.BoolProperty("canceled", true); + // Show a red 'X' as a prefix on the marker chart for canceled timers. + aWriter.StringProperty("prefix", "❌"); + } + + // The string property for the timer type is not written when the type is + // one shot, as that's the type used almost all the time, and that would + // consume space in the profiler buffer and then in the profile JSON, + // getting in the way of capturing long power profiles. + // Bug 1815677 might make this cheap to capture. + if (aType != nsITimer::TYPE_ONE_SHOT) { + if (aType == nsITimer::TYPE_REPEATING_SLACK) { + aWriter.StringProperty("ttype", "repeating slack"); + } else if (aType == nsITimer::TYPE_REPEATING_PRECISE) { + aWriter.StringProperty("ttype", "repeating precise"); + } else if (aType == nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP) { + aWriter.StringProperty("ttype", "repeating precise can skip"); + } else if (aType == nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY) { + aWriter.StringProperty("ttype", "repeating slack low priority"); + } else if (aType == nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY) { + aWriter.StringProperty("ttype", "low priority"); + } + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormat("delay", "Delay", MS::Format::Milliseconds); + schema.AddKeyLabelFormat("ttype", "Timer Type", MS::Format::String); + schema.AddKeyLabelFormat("canceled", "Canceled", MS::Format::String); + schema.SetChartLabel("{marker.data.prefix} {marker.data.delay}"); + schema.SetTableLabel( + "{marker.name} - {marker.data.prefix} {marker.data.delay}"); + return schema; + } +}; + +struct AddRemoveTimerMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("AddRemoveTimer"); + } + static void StreamJSONMarkerData(baseprofiler::SpliceableJSONWriter& aWriter, + const ProfilerString8View& aTimerName, + uint32_t aDelay, MarkerThreadId aThreadId) { + aWriter.StringProperty("name", aTimerName); + aWriter.IntProperty("delay", aDelay); + if (!aThreadId.IsUnspecified()) { + // Tech note: If `ToNumber()` returns a uint64_t, the conversion to + // int64_t is "implementation-defined" before C++20. This is + // acceptable here, because this is a one-way conversion to a unique + // identifier that's used to visually separate data by thread on the + // front-end. + aWriter.IntProperty( + "threadId", static_cast(aThreadId.ThreadId().ToNumber())); + } + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormatSearchable("name", "Name", MS::Format::String, + MS::Searchable::Searchable); + schema.AddKeyLabelFormat("delay", "Delay", MS::Format::Milliseconds); + schema.SetTableLabel( + "{marker.name} - {marker.data.name} - {marker.data.delay}"); + return schema; + } +}; + +void nsTimerEvent::Init() { sAllocator = new TimerEventAllocator(); } + +void nsTimerEvent::Shutdown() { + sCanDeleteAllocator = true; + DeleteAllocatorIfNeeded(); +} + +void nsTimerEvent::DeleteAllocatorIfNeeded() { + if (sCanDeleteAllocator && sAllocatorUsers == 0) { + delete sAllocator; + sAllocator = nullptr; + } +} + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY +NS_IMETHODIMP +nsTimerEvent::GetName(nsACString& aName) { + bool current; + MOZ_RELEASE_ASSERT( + NS_SUCCEEDED(mTimer->mEventTarget->IsOnCurrentThread(¤t)) && + current); + + mTimer->GetName(aName); + return NS_OK; +} +#endif + +NS_IMETHODIMP +nsTimerEvent::Run() { + if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug)) { + TimeStamp now = TimeStamp::Now(); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] time between PostTimerEvent() and Fire(): %fms\n", this, + (now - mInitTime).ToMilliseconds())); + } + + if (profiler_thread_is_being_profiled_for_markers(mTimerThreadId)) { + MutexAutoLock lock(mTimer->mMutex); + nsAutoCString name; + mTimer->GetName(name, lock); + // This adds a marker with the timer name as the marker name, to make it + // obvious which timers are being used. This marker will be useful to + // understand which timers might be added and firing excessively often. + profiler_add_marker( + name, geckoprofiler::category::TIMER, + MarkerOptions(MOZ_LIKELY(mInitTime) + ? MarkerTiming::Interval( + mTimer->mTimeout - mTimer->mDelay, mInitTime) + : MarkerTiming::IntervalUntilNowFrom( + mTimer->mTimeout - mTimer->mDelay), + MarkerThreadId(mTimerThreadId)), + TimerMarker{}, mTimer->mDelay.ToMilliseconds(), mTimer->mType, + MarkerThreadId::CurrentThread(), false); + // This marker is meant to help understand the behavior of the timer thread. + profiler_add_marker( + "PostTimerEvent", geckoprofiler::category::OTHER, + MarkerOptions(MOZ_LIKELY(mInitTime) + ? MarkerTiming::IntervalUntilNowFrom(mInitTime) + : MarkerTiming::InstantNow(), + MarkerThreadId(mTimerThreadId)), + AddRemoveTimerMarker{}, name, mTimer->mDelay.ToMilliseconds(), + MarkerThreadId::CurrentThread()); + } + + mTimer->Fire(mGeneration); + + return NS_OK; +} + +nsresult TimerThread::Init() { + mMonitor.AssertCurrentThreadOwns(); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("TimerThread::Init [%d]\n", mInitialized)); + + if (!mInitialized) { + nsTimerEvent::Init(); + + // We hold on to mThread to keep the thread alive. + nsresult rv = + NS_NewNamedThread("Timer", getter_AddRefs(mThread), this, + {.stackSize = nsIThreadManager::DEFAULT_STACK_SIZE, + .blockDispatch = true}); + if (NS_FAILED(rv)) { + mThread = nullptr; + } else { + RefPtr r = new TimerObserverRunnable(this); + if (NS_IsMainThread()) { + r->Run(); + } else { + NS_DispatchToMainThread(r); + } + } + + mInitialized = true; + } + + if (!mThread) { + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +nsresult TimerThread::Shutdown() { + MOZ_LOG(GetTimerLog(), LogLevel::Debug, ("TimerThread::Shutdown begin\n")); + + if (!mThread) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsTArray> timers; + { + // lock scope + MonitorAutoLock lock(mMonitor); + + mShutdown = true; + + // notify the cond var so that Run() can return + if (mWaiting) { + mNotified = true; + mMonitor.Notify(); + } + + // Need to copy content of mTimers array to a local array + // because call to timers' Cancel() (and release its self) + // must not be done under the lock. Destructor of a callback + // might potentially call some code reentering the same lock + // that leads to unexpected behavior or deadlock. + // See bug 422472. + timers.SetCapacity(mTimers.Length()); + for (Entry& entry : mTimers) { + if (entry.Value()) { + timers.AppendElement(entry.Take()); + } + } + + mTimers.Clear(); + } + + for (const RefPtr& timer : timers) { + MOZ_ASSERT(timer); + timer->Cancel(); + } + + mThread->Shutdown(); // wait for the thread to die + + nsTimerEvent::Shutdown(); + + MOZ_LOG(GetTimerLog(), LogLevel::Debug, ("TimerThread::Shutdown end\n")); + return NS_OK; +} + +namespace { + +struct MicrosecondsToInterval { + PRIntervalTime operator[](size_t aMs) const { + return PR_MicrosecondsToInterval(aMs); + } +}; + +struct IntervalComparator { + int operator()(PRIntervalTime aInterval) const { + return (0 < aInterval) ? -1 : 1; + } +}; + +} // namespace + +#ifdef DEBUG +void TimerThread::VerifyTimerListConsistency() const { + mMonitor.AssertCurrentThreadOwns(); + + // Find the first non-canceled timer (and check its cached timeout if we find + // it). + const size_t timerCount = mTimers.Length(); + size_t lastNonCanceledTimerIndex = 0; + while (lastNonCanceledTimerIndex < timerCount && + !mTimers[lastNonCanceledTimerIndex].Value()) { + ++lastNonCanceledTimerIndex; + } + MOZ_ASSERT(lastNonCanceledTimerIndex == timerCount || + mTimers[lastNonCanceledTimerIndex].Value()); + MOZ_ASSERT(lastNonCanceledTimerIndex == timerCount || + mTimers[lastNonCanceledTimerIndex].Value()->mTimeout == + mTimers[lastNonCanceledTimerIndex].Timeout()); + + // Verify that mTimers is sorted and the cached timeouts are consistent. + for (size_t timerIndex = lastNonCanceledTimerIndex + 1; + timerIndex < timerCount; ++timerIndex) { + if (mTimers[timerIndex].Value()) { + MOZ_ASSERT(mTimers[timerIndex].Timeout() == + mTimers[timerIndex].Value()->mTimeout); + MOZ_ASSERT(mTimers[timerIndex].Timeout() >= + mTimers[lastNonCanceledTimerIndex].Timeout()); + lastNonCanceledTimerIndex = timerIndex; + } + } +} +#endif + +size_t TimerThread::ComputeTimerInsertionIndex(const TimeStamp& timeout) const { + mMonitor.AssertCurrentThreadOwns(); + + const size_t timerCount = mTimers.Length(); + + size_t firstGtIndex = 0; + while (firstGtIndex < timerCount && + (!mTimers[firstGtIndex].Value() || + mTimers[firstGtIndex].Timeout() <= timeout)) { + ++firstGtIndex; + } + + return firstGtIndex; +} + +TimeStamp TimerThread::ComputeWakeupTimeFromTimers() const { + mMonitor.AssertCurrentThreadOwns(); + + // Timer list should be non-empty and first timer should always be + // non-canceled at this point and we rely on that here. + MOZ_ASSERT(!mTimers.IsEmpty()); + MOZ_ASSERT(mTimers[0].Value()); + + // Overview: Find the last timer in the list that can be "bundled" together in + // the same wake-up with mTimers[0] and use its timeout as our target wake-up + // time. + + // bundleWakeup is when we should wake up in order to be able to fire all of + // the timers in our selected bundle. It will always be the timeout of the + // last timer in the bundle. + TimeStamp bundleWakeup = mTimers[0].Timeout(); + + // cutoffTime is the latest that we can wake up for the timers currently + // accepted into the bundle. These needs to be updated as we go through the + // list because later timers may have more strict delay tolerances. + const TimeDuration minTimerDelay = TimeDuration::FromMilliseconds( + StaticPrefs::timer_minimum_firing_delay_tolerance_ms()); + const TimeDuration maxTimerDelay = TimeDuration::FromMilliseconds( + StaticPrefs::timer_maximum_firing_delay_tolerance_ms()); + TimeStamp cutoffTime = + bundleWakeup + ComputeAcceptableFiringDelay(mTimers[0].Delay(), + minTimerDelay, maxTimerDelay); + + const size_t timerCount = mTimers.Length(); + for (size_t entryIndex = 1; entryIndex < timerCount; ++entryIndex) { + const Entry& curEntry = mTimers[entryIndex]; + const nsTimerImpl* curTimer = curEntry.Value(); + if (!curTimer) { + // Canceled timer - skip it + continue; + } + + const TimeStamp curTimerDue = curEntry.Timeout(); + if (curTimerDue > cutoffTime) { + // Can't include this timer in the bundle - it fires too late. + break; + } + + // This timer can be included in the bundle. Update bundleWakeup and + // cutoffTime. + bundleWakeup = curTimerDue; + cutoffTime = std::min( + curTimerDue + ComputeAcceptableFiringDelay( + curEntry.Delay(), minTimerDelay, maxTimerDelay), + cutoffTime); + MOZ_ASSERT(bundleWakeup <= cutoffTime); + } + +#if !defined(XP_WIN) + // Due to the fact that, on Windows, each TimeStamp object holds two distinct + // "values", this assert is not valid there. See bug 1829983 for the details. + MOZ_ASSERT(bundleWakeup - mTimers[0].Timeout() <= + ComputeAcceptableFiringDelay(mTimers[0].Delay(), minTimerDelay, + maxTimerDelay)); +#endif + + return bundleWakeup; +} + +TimeDuration TimerThread::ComputeAcceptableFiringDelay( + TimeDuration timerDuration, TimeDuration minDelay, + TimeDuration maxDelay) const { + // Use the timer's duration divided by this value as a base for how much + // firing delay a timer can accept. 8 was chosen specifically because it is a + // power of two which means that this division turns nicely into a shift. + constexpr int64_t timerDurationDivider = 8; + static_assert(IsPowerOfTwo(static_cast(timerDurationDivider))); + const TimeDuration tmp = timerDuration / timerDurationDivider; + return std::min(std::max(minDelay, tmp), maxDelay); +} + +NS_IMETHODIMP +TimerThread::Run() { + MonitorAutoLock lock(mMonitor); + + mProfilerThreadId = profiler_current_thread_id(); + + // TODO: Make mAllowedEarlyFiringMicroseconds const and initialize it in the + // constructor. + mAllowedEarlyFiringMicroseconds = 250; + const TimeDuration allowedEarlyFiring = + TimeDuration::FromMicroseconds(mAllowedEarlyFiringMicroseconds); + + bool forceRunNextTimer = false; + + // Queue for tracking of how many timers are fired on each wake-up. We need to + // buffer these locally and only send off to glean occasionally to avoid + // performance hit. + static constexpr size_t kMaxQueuedTimerFired = 128; + size_t queuedTimerFiredCount = 0; + AutoTArray queuedTimersFiredPerWakeup; + queuedTimersFiredPerWakeup.SetLengthAndRetainStorage(kMaxQueuedTimerFired); + +#ifdef XP_WIN + // kTimerPeriodEvalIntervalSec is the minimum amount of time that must pass + // before we will consider changing the timer period again. + static constexpr float kTimerPeriodEvalIntervalSec = 2.0f; + const TimeDuration timerPeriodEvalInterval = + TimeDuration::FromSeconds(kTimerPeriodEvalIntervalSec); + TimeStamp nextTimerPeriodEval = TimeStamp::Now() + timerPeriodEvalInterval; + + // If this is false, we will perform all of the logic but will stop short of + // actually changing the timer period. + const bool adjustTimerPeriod = + StaticPrefs::timer_auto_increase_timer_resolution(); + UINT lastTimePeriodSet = ComputeDesiredTimerPeriod(); + + if (adjustTimerPeriod) { + timeBeginPeriod(lastTimePeriodSet); + } +#endif + + uint64_t timersFiredThisWakeup = 0; + while (!mShutdown) { + // Have to use PRIntervalTime here, since PR_WaitCondVar takes it + TimeDuration waitFor; + bool forceRunThisTimer = forceRunNextTimer; + forceRunNextTimer = false; + +#ifdef DEBUG + VerifyTimerListConsistency(); +#endif + + if (mSleeping) { + // Sleep for 0.1 seconds while not firing timers. + uint32_t milliseconds = 100; + if (ChaosMode::isActive(ChaosFeature::TimerScheduling)) { + milliseconds = ChaosMode::randomUint32LessThan(200); + } + waitFor = TimeDuration::FromMilliseconds(milliseconds); + } else { + waitFor = TimeDuration::Forever(); + TimeStamp now = TimeStamp::Now(); + +#ifdef XP_WIN + if (now >= nextTimerPeriodEval) { + const UINT newTimePeriod = ComputeDesiredTimerPeriod(); + if (newTimePeriod != lastTimePeriodSet) { + if (adjustTimerPeriod) { + timeEndPeriod(lastTimePeriodSet); + timeBeginPeriod(newTimePeriod); + } + lastTimePeriodSet = newTimePeriod; + } + nextTimerPeriodEval = now + timerPeriodEvalInterval; + } +#endif + +#if TIMER_THREAD_STATISTICS + if (!mNotified && !mIntendedWakeupTime.IsNull() && + now < mIntendedWakeupTime) { + ++mEarlyWakeups; + const double earlinessms = (mIntendedWakeupTime - now).ToMilliseconds(); + mTotalEarlyWakeupTime += earlinessms; + } +#endif + + RemoveLeadingCanceledTimersInternal(); + + if (!mTimers.IsEmpty()) { + if (now + allowedEarlyFiring >= mTimers[0].Value()->mTimeout || + forceRunThisTimer) { + next: + // NB: AddRef before the Release under RemoveTimerInternal to avoid + // mRefCnt passing through zero, in case all other refs than the one + // from mTimers have gone away (the last non-mTimers[i]-ref's Release + // must be racing with us, blocked in gThread->RemoveTimer waiting + // for TimerThread::mMonitor, under nsTimerImpl::Release. + + RefPtr timerRef(mTimers[0].Take()); + RemoveFirstTimerInternal(); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("Timer thread woke up %fms from when it was supposed to\n", + fabs((now - timerRef->mTimeout).ToMilliseconds()))); + + // We are going to let the call to PostTimerEvent here handle the + // release of the timer so that we don't end up releasing the timer + // on the TimerThread instead of on the thread it targets. + { + ++timersFiredThisWakeup; + LogTimerEvent::Run run(timerRef.get()); + PostTimerEvent(timerRef.forget()); + } + + if (mShutdown) { + break; + } + + // Update now, as PostTimerEvent plus the locking may have taken a + // tick or two, and we may goto next below. + now = TimeStamp::Now(); + } + } + + RemoveLeadingCanceledTimersInternal(); + + if (!mTimers.IsEmpty()) { + TimeStamp timeout = mTimers[0].Value()->mTimeout; + + // Don't wait at all (even for PR_INTERVAL_NO_WAIT) if the next timer + // is due now or overdue. + // + // Note that we can only sleep for integer values of a certain + // resolution. We use mAllowedEarlyFiringMicroseconds, calculated + // before, to do the optimal rounding (i.e., of how to decide what + // interval is so small we should not wait at all). + double microseconds = (timeout - now).ToMicroseconds(); + + // The mean value of sFractions must be 1 to ensure that the average of + // a long sequence of timeouts converges to the actual sum of their + // times. + static constexpr double sChaosFractions[] = {0.0, 0.25, 0.5, 0.75, + 1.0, 1.75, 2.75}; + if (ChaosMode::isActive(ChaosFeature::TimerScheduling)) { + microseconds *= sChaosFractions[ChaosMode::randomUint32LessThan( + ArrayLength(sChaosFractions))]; + forceRunNextTimer = true; + } + + if (microseconds < mAllowedEarlyFiringMicroseconds) { + forceRunNextTimer = false; + goto next; // round down; execute event now + } + + // TECHNICAL NOTE: Determining waitFor (by subtracting |now| from our + // desired wake-up time) at this point is not ideal. For one thing, the + // |now| that we have at this point is somewhat old. Secondly, there is + // quite a bit of code between here and where we actually use waitFor to + // request sleep. If I am thinking about this correctly, both of these + // will contribute to us requesting more sleep than is actually needed + // to wake up at our desired time. We could avoid this problem by only + // determining our desired wake-up time here and then calculating the + // wait time when we're actually about to sleep. + const TimeStamp wakeupTime = ComputeWakeupTimeFromTimers(); + waitFor = wakeupTime - now; + + // If this were to fail that would mean that we had more timers that we + // should have fired. + MOZ_ASSERT(!waitFor.IsZero()); + + if (ChaosMode::isActive(ChaosFeature::TimerScheduling)) { + // If chaos mode is active then mess with the amount of time that we + // request to sleep (without changing what we record as our expected + // wake-up time). This will simulate unintended early/late wake-ups. + const double waitInMs = waitFor.ToMilliseconds(); + const double chaosWaitInMs = + waitInMs * sChaosFractions[ChaosMode::randomUint32LessThan( + ArrayLength(sChaosFractions))]; + waitFor = TimeDuration::FromMilliseconds(chaosWaitInMs); + } + + mIntendedWakeupTime = wakeupTime; + } else { + mIntendedWakeupTime = TimeStamp{}; + } + + if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug)) { + if (waitFor == TimeDuration::Forever()) + MOZ_LOG(GetTimerLog(), LogLevel::Debug, ("waiting forever\n")); + else + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("waiting for %f\n", waitFor.ToMilliseconds())); + } + } + + { + // About to sleep - let's make note of how many timers we processed and + // see if we should send out a new batch of telemetry. + queuedTimersFiredPerWakeup[queuedTimerFiredCount] = timersFiredThisWakeup; + ++queuedTimerFiredCount; + if (queuedTimerFiredCount == kMaxQueuedTimerFired) { + glean::timer_thread::timers_fired_per_wakeup.AccumulateSamples( + queuedTimersFiredPerWakeup); + queuedTimerFiredCount = 0; + } + } + +#if TIMER_THREAD_STATISTICS + { + size_t bucketIndex = 0; + while (bucketIndex < sTimersFiredPerWakeupBucketCount - 1 && + timersFiredThisWakeup > + sTimersFiredPerWakeupThresholds[bucketIndex]) { + ++bucketIndex; + } + MOZ_ASSERT(bucketIndex < sTimersFiredPerWakeupBucketCount); + ++mTimersFiredPerWakeup[bucketIndex]; + + ++mTotalWakeupCount; + if (mNotified) { + ++mTimersFiredPerNotifiedWakeup[bucketIndex]; + ++mTotalNotifiedWakeupCount; + } else { + ++mTimersFiredPerUnnotifiedWakeup[bucketIndex]; + ++mTotalUnnotifiedWakeupCount; + } + } +#endif + + timersFiredThisWakeup = 0; + + mWaiting = true; + mNotified = false; + + { + AUTO_PROFILER_TRACING_MARKER("TimerThread", "Wait", OTHER); + mMonitor.Wait(waitFor); + } + if (mNotified) { + forceRunNextTimer = false; + } + mWaiting = false; + } + + // About to shut down - let's send out the final batch of timers fired counts. + if (queuedTimerFiredCount != 0) { + queuedTimersFiredPerWakeup.SetLengthAndRetainStorage(queuedTimerFiredCount); + glean::timer_thread::timers_fired_per_wakeup.AccumulateSamples( + queuedTimersFiredPerWakeup); + } + +#ifdef XP_WIN + // About to shut down - let's finish off the last time period that we set. + if (adjustTimerPeriod) { + timeEndPeriod(lastTimePeriodSet); + } +#endif + + return NS_OK; +} + +nsresult TimerThread::AddTimer(nsTimerImpl* aTimer, + const MutexAutoLock& aProofOfLock) { + MonitorAutoLock lock(mMonitor); + AUTO_TIMERS_STATS(TimerThread_AddTimer); + + if (!aTimer->mEventTarget) { + return NS_ERROR_NOT_INITIALIZED; + } + + nsresult rv = Init(); + if (NS_FAILED(rv)) { + return rv; + } + + // Awaken the timer thread if: + // - This timer needs to fire *before* the Timer Thread is scheduled to wake + // up. + // AND/OR + // - The delay is 0, which is usually meant to be run as soon as possible. + // Note: Even if the thread is scheduled to wake up now/soon, on some + // systems there could be a significant delay compared to notifying, which + // is almost immediate; and some users of 0-delay depend on it being this + // fast! + const TimeDuration minTimerDelay = TimeDuration::FromMilliseconds( + StaticPrefs::timer_minimum_firing_delay_tolerance_ms()); + const TimeDuration maxTimerDelay = TimeDuration::FromMilliseconds( + StaticPrefs::timer_maximum_firing_delay_tolerance_ms()); + const TimeDuration firingDelay = ComputeAcceptableFiringDelay( + aTimer->mDelay, minTimerDelay, maxTimerDelay); + const bool firingBeforeNextWakeup = + mIntendedWakeupTime.IsNull() || + (aTimer->mTimeout + firingDelay < mIntendedWakeupTime); + const bool wakeUpTimerThread = + mWaiting && (firingBeforeNextWakeup || aTimer->mDelay.IsZero()); + +#if TIMER_THREAD_STATISTICS + if (mTotalTimersAdded == 0) { + mFirstTimerAdded = TimeStamp::Now(); + } + ++mTotalTimersAdded; +#endif + + // Add the timer to our list. + if (!AddTimerInternal(*aTimer)) { + return NS_ERROR_OUT_OF_MEMORY; + } + + if (wakeUpTimerThread) { + mNotified = true; + mMonitor.Notify(); + } + + if (profiler_thread_is_being_profiled_for_markers(mProfilerThreadId)) { + nsAutoCString name; + aTimer->GetName(name, aProofOfLock); + + nsLiteralCString prefix("Anonymous_"); + profiler_add_marker( + "AddTimer", geckoprofiler::category::OTHER, + MarkerOptions(MarkerThreadId(mProfilerThreadId), + MarkerStack::MaybeCapture( + name.Equals("nonfunction:JS") || + StringHead(name, prefix.Length()) == prefix)), + AddRemoveTimerMarker{}, name, aTimer->mDelay.ToMilliseconds(), + MarkerThreadId::CurrentThread()); + } + + return NS_OK; +} + +nsresult TimerThread::RemoveTimer(nsTimerImpl* aTimer, + const MutexAutoLock& aProofOfLock) { + MonitorAutoLock lock(mMonitor); + AUTO_TIMERS_STATS(TimerThread_RemoveTimer); + + // Remove the timer from our array. Tell callers that aTimer was not found + // by returning NS_ERROR_NOT_AVAILABLE. + + if (!RemoveTimerInternal(*aTimer)) { + return NS_ERROR_NOT_AVAILABLE; + } + +#if TIMER_THREAD_STATISTICS + ++mTotalTimersRemoved; +#endif + + // Note: The timer thread is *not* awoken. + // The removed-timer entry is just left null, and will be reused (by a new or + // re-set timer) or discarded (when the timer thread logic handles non-null + // timers around it). + // If this was the front timer, and in the unlikely case that its entry is not + // soon reused by a re-set timer, the timer thread will wake up at the + // previously-scheduled time, but will quickly notice that there is no actual + // pending timer, and will restart its wait until the following real timeout. + + if (profiler_thread_is_being_profiled_for_markers(mProfilerThreadId)) { + nsAutoCString name; + aTimer->GetName(name, aProofOfLock); + + nsLiteralCString prefix("Anonymous_"); + // This marker is meant to help understand the behavior of the timer thread. + profiler_add_marker( + "RemoveTimer", geckoprofiler::category::OTHER, + MarkerOptions(MarkerThreadId(mProfilerThreadId), + MarkerStack::MaybeCapture( + name.Equals("nonfunction:JS") || + StringHead(name, prefix.Length()) == prefix)), + AddRemoveTimerMarker{}, name, aTimer->mDelay.ToMilliseconds(), + MarkerThreadId::CurrentThread()); + // This adds a marker with the timer name as the marker name, to make it + // obvious which timers are being used. This marker will be useful to + // understand which timers might be added and removed excessively often. + profiler_add_marker(name, geckoprofiler::category::TIMER, + MarkerOptions(MarkerTiming::IntervalUntilNowFrom( + aTimer->mTimeout - aTimer->mDelay), + MarkerThreadId(mProfilerThreadId)), + TimerMarker{}, aTimer->mDelay.ToMilliseconds(), + aTimer->mType, MarkerThreadId::CurrentThread(), true); + } + + return NS_OK; +} + +TimeStamp TimerThread::FindNextFireTimeForCurrentThread(TimeStamp aDefault, + uint32_t aSearchBound) { + MonitorAutoLock lock(mMonitor); + AUTO_TIMERS_STATS(TimerThread_FindNextFireTimeForCurrentThread); + + for (const Entry& entry : mTimers) { + const nsTimerImpl* timer = entry.Value(); + if (timer) { + if (entry.Timeout() > aDefault) { + return aDefault; + } + + // Don't yield to timers created with the *_LOW_PRIORITY type. + if (!timer->IsLowPriority()) { + bool isOnCurrentThread = false; + nsresult rv = + timer->mEventTarget->IsOnCurrentThread(&isOnCurrentThread); + if (NS_SUCCEEDED(rv) && isOnCurrentThread) { + return entry.Timeout(); + } + } + + if (aSearchBound == 0) { + // Couldn't find any non-low priority timers for the current thread. + // Return a compromise between a very short and a long idle time. + TimeStamp fallbackDeadline = + TimeStamp::Now() + TimeDuration::FromMilliseconds(16); + return fallbackDeadline < aDefault ? fallbackDeadline : aDefault; + } + + --aSearchBound; + } + } + + // No timers for this thread, return the default. + return aDefault; +} + +// This function must be called from within a lock +// Also: we hold the mutex for the nsTimerImpl. +bool TimerThread::AddTimerInternal(nsTimerImpl& aTimer) { + mMonitor.AssertCurrentThreadOwns(); + aTimer.mMutex.AssertCurrentThreadOwns(); + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal); + if (mShutdown) { + return false; + } + + LogTimerEvent::LogDispatch(&aTimer); + + const TimeStamp& timeout = aTimer.mTimeout; + const size_t insertionIndex = ComputeTimerInsertionIndex(timeout); + + if (insertionIndex != 0 && !mTimers[insertionIndex - 1].Value()) { + // Very common scenario in practice: The timer just before the insertion + // point is canceled, overwrite it. + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_overwrite_before); + mTimers[insertionIndex - 1] = Entry{aTimer}; + return true; + } + + const size_t length = mTimers.Length(); + if (insertionIndex == length) { + // We're at the end (including it's the very first insertion), add new timer + // at the end. + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_append); + return mTimers.AppendElement(Entry{aTimer}, mozilla::fallible); + } + + if (!mTimers[insertionIndex].Value()) { + // The timer at the insertion point is canceled, overwrite it. + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_overwrite); + mTimers[insertionIndex] = Entry{aTimer}; + return true; + } + + // The new timer has to be inserted. + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_insert); + // The capacity should be checked first, because if it needs to be increased + // and the memory allocation fails, only the new timer should be lost. + if (length == mTimers.Capacity() && mTimers[length - 1].Value()) { + // We have reached capacity, and the last entry is not canceled, so we + // really want to increase the capacity in case the extra slot is required. + // To force-expand the array, append a canceled-timer entry with a timestamp + // far in the future. + // This empty Entry may be used below to receive the moved-from previous + // entry. If not, it may be used in a later call if we need to append a new + // timer at the end. + AUTO_TIMERS_STATS(TimerThread_AddTimerInternal_insert_expand); + if (!mTimers.AppendElement( + Entry{mTimers[length - 1].Timeout() + + TimeDuration::FromSeconds(365.0 * 24.0 * 60.0 * 60.0)}, + mozilla::fallible)) { + return false; + } + } + + // Extract the timer at the insertion point, and put the new timer in its + // place. + Entry extractedEntry = std::exchange(mTimers[insertionIndex], Entry{aTimer}); + // Following entries can be pushed until we hit a canceled timer or the end. + for (size_t i = insertionIndex + 1; i < length; ++i) { + Entry& entryRef = mTimers[i]; + if (!entryRef.Value()) { + // Canceled entry, overwrite it with the extracted entry from before. + COUNT_TIMERS_STATS(TimerThread_AddTimerInternal_insert_overwrite); + entryRef = std::move(extractedEntry); + return true; + } + // Write extracted entry from before, and extract current entry. + COUNT_TIMERS_STATS(TimerThread_AddTimerInternal_insert_shifts); + std::swap(entryRef, extractedEntry); + } + // We've reached the end of the list, with still one extracted entry to + // re-insert. We've checked the capacity above, this cannot fail. + COUNT_TIMERS_STATS(TimerThread_AddTimerInternal_insert_append); + mTimers.AppendElement(std::move(extractedEntry)); + return true; +} + +// This function must be called from within a lock +// Also: we hold the mutex for the nsTimerImpl. +bool TimerThread::RemoveTimerInternal(nsTimerImpl& aTimer) { + mMonitor.AssertCurrentThreadOwns(); + aTimer.mMutex.AssertCurrentThreadOwns(); + AUTO_TIMERS_STATS(TimerThread_RemoveTimerInternal); + if (!aTimer.IsInTimerThread()) { + COUNT_TIMERS_STATS(TimerThread_RemoveTimerInternal_not_in_list); + return false; + } + AUTO_TIMERS_STATS(TimerThread_RemoveTimerInternal_in_list); + for (auto& entry : mTimers) { + if (entry.Value() == &aTimer) { + entry.Forget(); + return true; + } + } + MOZ_ASSERT(!aTimer.IsInTimerThread(), + "Not found in the list but it should be!?"); + return false; +} + +void TimerThread::RemoveLeadingCanceledTimersInternal() { + mMonitor.AssertCurrentThreadOwns(); + AUTO_TIMERS_STATS(TimerThread_RemoveLeadingCanceledTimersInternal); + + size_t toRemove = 0; + while (toRemove < mTimers.Length() && !mTimers[toRemove].Value()) { + ++toRemove; + } + mTimers.RemoveElementsAt(0, toRemove); +} + +void TimerThread::RemoveFirstTimerInternal() { + mMonitor.AssertCurrentThreadOwns(); + AUTO_TIMERS_STATS(TimerThread_RemoveFirstTimerInternal); + MOZ_ASSERT(!mTimers.IsEmpty()); + mTimers.RemoveElementAt(0); +} + +void TimerThread::PostTimerEvent(already_AddRefed aTimerRef) { + mMonitor.AssertCurrentThreadOwns(); + AUTO_TIMERS_STATS(TimerThread_PostTimerEvent); + + RefPtr timer(aTimerRef); + +#if TIMER_THREAD_STATISTICS + const double actualFiringDelay = + std::max((TimeStamp::Now() - timer->mTimeout).ToMilliseconds(), 0.0); + if (mNotified) { + ++mTotalTimersFiredNotified; + mTotalActualTimerFiringDelayNotified += actualFiringDelay; + } else { + ++mTotalTimersFiredUnnotified; + mTotalActualTimerFiringDelayUnnotified += actualFiringDelay; + } +#endif + + if (!timer->mEventTarget) { + NS_ERROR("Attempt to post timer event to NULL event target"); + return; + } + + // XXX we may want to reuse this nsTimerEvent in the case of repeating timers. + + // Since we already addref'd 'timer', we don't need to addref here. + // We will release either in ~nsTimerEvent(), or pass the reference back to + // the caller. We need to copy the generation number from this timer into the + // event, so we can avoid firing a timer that was re-initialized after being + // canceled. + + nsCOMPtr target = timer->mEventTarget; + + void* p = nsTimerEvent::operator new(sizeof(nsTimerEvent)); + if (!p) { + return; + } + RefPtr event = + ::new (KnownNotNull, p) nsTimerEvent(timer.forget(), mProfilerThreadId); + + nsresult rv; + { + // We release mMonitor around the Dispatch because if the Dispatch interacts + // with the timer API we'll deadlock. + MonitorAutoUnlock unlock(mMonitor); + rv = target->Dispatch(event, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + timer = event->ForgetTimer(); + // We do this to avoid possible deadlock by taking the two locks in a + // different order than is used in RemoveTimer(). RemoveTimer() has + // aTimer->mMutex first. We use timer.get() to keep static analysis + // happy + // NOTE: I'm not sure that any of the below is actually necessary. It + // seems to me that the timer that we're trying to fire will have already + // been removed prior to this. + MutexAutoLock lock1(timer.get()->mMutex); + MonitorAutoLock lock2(mMonitor); + RemoveTimerInternal(*timer); + } + } +} + +void TimerThread::DoBeforeSleep() { + // Mainthread + MonitorAutoLock lock(mMonitor); + mSleeping = true; +} + +// Note: wake may be notified without preceding sleep notification +void TimerThread::DoAfterSleep() { + // Mainthread + MonitorAutoLock lock(mMonitor); + mSleeping = false; + + // Wake up the timer thread to re-process the array to ensure the sleep delay + // is correct, and fire any expired timers (perhaps quite a few) + mNotified = true; + PROFILER_MARKER_UNTYPED("AfterSleep", OTHER, + MarkerThreadId(mProfilerThreadId)); + mMonitor.Notify(); +} + +NS_IMETHODIMP +TimerThread::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + if (strcmp(aTopic, "ipc:process-priority-changed") == 0) { + nsCOMPtr props = do_QueryInterface(aSubject); + MOZ_ASSERT(props != nullptr); + + int32_t priority = static_cast(hal::PROCESS_PRIORITY_UNKNOWN); + props->GetPropertyAsInt32(u"priority"_ns, &priority); + mCachedPriority.store(static_cast(priority), + std::memory_order_relaxed); + } + + if (StaticPrefs::timer_ignore_sleep_wake_notifications()) { + return NS_OK; + } + + if (strcmp(aTopic, "sleep_notification") == 0 || + strcmp(aTopic, "suspend_process_notification") == 0) { + DoBeforeSleep(); + } else if (strcmp(aTopic, "wake_notification") == 0 || + strcmp(aTopic, "resume_process_notification") == 0) { + DoAfterSleep(); + } + + return NS_OK; +} + +uint32_t TimerThread::AllowedEarlyFiringMicroseconds() { + MonitorAutoLock lock(mMonitor); + return mAllowedEarlyFiringMicroseconds; +} + +#if TIMER_THREAD_STATISTICS +void TimerThread::PrintStatistics() const { + mMonitor.AssertCurrentThreadOwns(); + + const TimeStamp freshNow = TimeStamp::Now(); + const double timeElapsed = mFirstTimerAdded.IsNull() + ? 0.0 + : (freshNow - mFirstTimerAdded).ToSeconds(); + printf_stderr("TimerThread Stats (Total time %8.2fs)\n", timeElapsed); + + printf_stderr("Added: %6llu Removed: %6llu Fired: %6llu\n", mTotalTimersAdded, + mTotalTimersRemoved, + mTotalTimersFiredNotified + mTotalTimersFiredUnnotified); + + auto PrintTimersFiredBucket = + [](const AutoTArray& buckets, + const size_t wakeupCount, const size_t timersFiredCount, + const double totalTimerDelay, const char* label) { + printf_stderr("%s : [", label); + for (size_t bucketVal : buckets) { + printf_stderr(" %5llu", bucketVal); + } + printf_stderr( + " ] Wake-ups/timer %6llu / %6llu (%7.4f) Avg Timer Delay %7.4f\n", + wakeupCount, timersFiredCount, + static_cast(wakeupCount) / timersFiredCount, + totalTimerDelay / timersFiredCount); + }; + + printf_stderr("Wake-ups:\n"); + PrintTimersFiredBucket( + mTimersFiredPerWakeup, mTotalWakeupCount, + mTotalTimersFiredNotified + mTotalTimersFiredUnnotified, + mTotalActualTimerFiringDelayNotified + + mTotalActualTimerFiringDelayUnnotified, + "Total "); + PrintTimersFiredBucket(mTimersFiredPerNotifiedWakeup, + mTotalNotifiedWakeupCount, mTotalTimersFiredNotified, + mTotalActualTimerFiringDelayNotified, "Notified "); + PrintTimersFiredBucket(mTimersFiredPerUnnotifiedWakeup, + mTotalUnnotifiedWakeupCount, + mTotalTimersFiredUnnotified, + mTotalActualTimerFiringDelayUnnotified, "Unnotified "); + + printf_stderr("Early Wake-ups: %6llu Avg: %7.4fms\n", mEarlyWakeups, + mTotalEarlyWakeupTime / mEarlyWakeups); +} +#endif + +/* This nsReadOnlyTimer class is used for the values returned by the + * TimerThread::GetTimers method. + * It is not possible to return a strong reference to the nsTimerImpl + * instance (that could extend the lifetime of the timer and cause it to fire + * a callback pointing to already freed memory) or a weak reference + * (nsSupportsWeakReference doesn't support freeing the referee on a thread + * that isn't the thread that owns the weak reference), so instead the timer + * name, delay and type are copied to a new object. */ +class nsReadOnlyTimer final : public nsITimer { + public: + explicit nsReadOnlyTimer(const nsACString& aName, uint32_t aDelay, + uint32_t aType) + : mName(aName), mDelay(aDelay), mType(aType) {} + NS_DECL_ISUPPORTS + + NS_IMETHOD Init(nsIObserver* aObserver, uint32_t aDelayInMs, + uint32_t aType) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD InitWithCallback(nsITimerCallback* aCallback, uint32_t aDelayInMs, + uint32_t aType) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD InitHighResolutionWithCallback(nsITimerCallback* aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD Cancel(void) override { return NS_ERROR_NOT_IMPLEMENTED; } + NS_IMETHOD InitWithNamedFuncCallback(nsTimerCallbackFunc aCallback, + void* aClosure, uint32_t aDelay, + uint32_t aType, + const char* aName) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD InitHighResolutionWithNamedFuncCallback( + nsTimerCallbackFunc aCallback, void* aClosure, + const mozilla::TimeDuration& aDelay, uint32_t aType, + const char* aName) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + + NS_IMETHOD GetName(nsACString& aName) override { + aName = mName; + return NS_OK; + } + NS_IMETHOD GetDelay(uint32_t* aDelay) override { + *aDelay = mDelay; + return NS_OK; + } + NS_IMETHOD SetDelay(uint32_t aDelay) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD GetType(uint32_t* aType) override { + *aType = mType; + return NS_OK; + } + NS_IMETHOD SetType(uint32_t aType) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD GetClosure(void** aClosure) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD GetCallback(nsITimerCallback** aCallback) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD GetTarget(nsIEventTarget** aTarget) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD SetTarget(nsIEventTarget* aTarget) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + NS_IMETHOD GetAllowedEarlyFiringMicroseconds( + uint32_t* aAllowedEarlyFiringMicroseconds) override { + return NS_ERROR_NOT_IMPLEMENTED; + } + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) override { + return sizeof(*this); + } + + private: + nsCString mName; + uint32_t mDelay; + uint32_t mType; + ~nsReadOnlyTimer() = default; +}; + +NS_IMPL_ISUPPORTS(nsReadOnlyTimer, nsITimer) + +nsresult TimerThread::GetTimers(nsTArray>& aRetVal) { + nsTArray> timers; + { + MonitorAutoLock lock(mMonitor); + for (const auto& entry : mTimers) { + nsTimerImpl* timer = entry.Value(); + if (!timer) { + continue; + } + timers.AppendElement(timer); + } + } + + for (nsTimerImpl* timer : timers) { + nsAutoCString name; + timer->GetName(name); + + uint32_t delay; + timer->GetDelay(&delay); + + uint32_t type; + timer->GetType(&type); + + aRetVal.AppendElement(new nsReadOnlyTimer(name, delay, type)); + } + + return NS_OK; +} diff --git a/xpcom/threads/TimerThread.h b/xpcom/threads/TimerThread.h new file mode 100644 index 0000000000..f6a5827b94 --- /dev/null +++ b/xpcom/threads/TimerThread.h @@ -0,0 +1,247 @@ +/* -*- 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 TimerThread_h___ +#define TimerThread_h___ + +#include "nsIObserver.h" +#include "nsIRunnable.h" +#include "nsIThread.h" + +#include "nsTimerImpl.h" +#include "nsThreadUtils.h" + +#include "nsTArray.h" + +#include "mozilla/Attributes.h" +#include "mozilla/HalTypes.h" +#include "mozilla/Monitor.h" +#include "mozilla/ProfilerUtils.h" + +// Enable this to compute lots of interesting statistics and print them out when +// PrintStatistics() is called. +#define TIMER_THREAD_STATISTICS 0 + +class TimerThread final : public mozilla::Runnable, public nsIObserver { + public: + typedef mozilla::Monitor Monitor; + typedef mozilla::MutexAutoLock MutexAutoLock; + typedef mozilla::TimeStamp TimeStamp; + typedef mozilla::TimeDuration TimeDuration; + + TimerThread(); + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIOBSERVER + + nsresult Shutdown(); + + nsresult AddTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(aTimer->mMutex); + nsresult RemoveTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(aTimer->mMutex); + // Considering only the first 'aSearchBound' timers (in firing order), returns + // the timeout of the first non-low-priority timer, on the current thread, + // that will fire before 'aDefault'. If no such timer exists, 'aDefault' is + // returned. + TimeStamp FindNextFireTimeForCurrentThread(TimeStamp aDefault, + uint32_t aSearchBound); + + void DoBeforeSleep(); + void DoAfterSleep(); + + bool IsOnTimerThread() const { return mThread->IsOnCurrentThread(); } + + uint32_t AllowedEarlyFiringMicroseconds(); + nsresult GetTimers(nsTArray>& aRetVal); + + private: + ~TimerThread(); + + bool mInitialized; + + // These internal helper methods must be called while mMonitor is held. + // AddTimerInternal returns false if the insertion failed. + bool AddTimerInternal(nsTimerImpl& aTimer) MOZ_REQUIRES(mMonitor); + bool RemoveTimerInternal(nsTimerImpl& aTimer) + MOZ_REQUIRES(mMonitor, aTimer.mMutex); + void RemoveLeadingCanceledTimersInternal() MOZ_REQUIRES(mMonitor); + void RemoveFirstTimerInternal() MOZ_REQUIRES(mMonitor); + nsresult Init() MOZ_REQUIRES(mMonitor); + + void PostTimerEvent(already_AddRefed aTimerRef) + MOZ_REQUIRES(mMonitor); + + // Using atomic because this value is written to in one place, and read from + // in another, and those two locations are likely to be executed from separate + // threads. Reads/writes to an aligned value this size should be atomic even + // without using std::atomic, but doing this explicitly provides a good + // reminder that this is accessed from multiple threads. + std::atomic mCachedPriority = + mozilla::hal::PROCESS_PRIORITY_UNKNOWN; + + nsCOMPtr mThread; + // Lock ordering requirements: + // (optional) ThreadWrapper::sMutex -> + // (optional) nsTimerImpl::mMutex -> + // TimerThread::mMonitor + Monitor mMonitor; + + bool mShutdown MOZ_GUARDED_BY(mMonitor); + bool mWaiting MOZ_GUARDED_BY(mMonitor); + bool mNotified MOZ_GUARDED_BY(mMonitor); + bool mSleeping MOZ_GUARDED_BY(mMonitor); + + class Entry final { + public: + explicit Entry(nsTimerImpl& aTimerImpl) + : mTimeout(aTimerImpl.mTimeout), + mDelay(aTimerImpl.mDelay), + mTimerImpl(&aTimerImpl) { + aTimerImpl.SetIsInTimerThread(true); + } + + // Create an already-canceled entry with the given timeout. + explicit Entry(TimeStamp aTimeout) + : mTimeout(std::move(aTimeout)), mTimerImpl(nullptr) {} + + // Don't allow copies, otherwise which one would manage `IsInTimerThread`? + Entry(const Entry&) = delete; + Entry& operator=(const Entry&) = delete; + + // Move-only. + Entry(Entry&&) = default; + Entry& operator=(Entry&&) = default; + + ~Entry() { + if (mTimerImpl) { + mTimerImpl->mMutex.AssertCurrentThreadOwns(); + mTimerImpl->SetIsInTimerThread(false); + } + } + + nsTimerImpl* Value() const { return mTimerImpl; } + + void Forget() { + if (MOZ_UNLIKELY(!mTimerImpl)) { + return; + } + mTimerImpl->mMutex.AssertCurrentThreadOwns(); + mTimerImpl->SetIsInTimerThread(false); + mTimerImpl = nullptr; + } + + // Called with the Monitor held, but not the TimerImpl's mutex + already_AddRefed Take() { + if (MOZ_LIKELY(mTimerImpl)) { + MOZ_ASSERT(mTimerImpl->IsInTimerThread()); + mTimerImpl->SetIsInTimerThread(false); + } + return mTimerImpl.forget(); + } + + const TimeStamp& Timeout() const { return mTimeout; } + const TimeDuration& Delay() const { return mDelay; } + + private: + // These values are simply cached from the timer. Keeping them here is good + // for cache usage and allows us to avoid worrying about locking conflicts + // with the timer. + TimeStamp mTimeout; + TimeDuration mDelay; + + RefPtr mTimerImpl; + }; + + // Computes and returns the index in mTimers at which a new timer with the + // specified timeout should be inserted in order to maintain "sorted" order. + size_t ComputeTimerInsertionIndex(const TimeStamp& timeout) const + MOZ_REQUIRES(mMonitor); + + // Computes and returns when we should next try to wake up in order to handle + // the triggering of the timers in mTimers. Currently this is very simple and + // we always just plan to wake up for the next timer in the list. In the + // future this will be more sophisticated. + TimeStamp ComputeWakeupTimeFromTimers() const MOZ_REQUIRES(mMonitor); + + // Computes how late a timer can acceptably fire. + // timerDuration is the duration of the timer whose delay we are calculating. + // Longer timers can tolerate longer firing delays. + // minDelay is an amount by which any timer can be delayed. + // This function will never return a value smaller than minDelay (unless this + // conflicts with maxDelay). maxDelay is the upper limit on the amount by + // which we will ever delay any timer. Takes precedence over minDelay if there + // is a conflict. (Zero will effectively disable timer coalescing.) + TimeDuration ComputeAcceptableFiringDelay(TimeDuration timerDuration, + TimeDuration minDelay, + TimeDuration maxDelay) const; + +#ifdef XP_WIN + UINT ComputeDesiredTimerPeriod() const; +#endif + +#ifdef DEBUG + // Checks mTimers to see if any entries are out of order or any cached + // timeouts are incorrect and will assert if any inconsistency is found. Has + // no side effects other than asserting so has no use in non-DEBUG builds. + void VerifyTimerListConsistency() const MOZ_REQUIRES(mMonitor); +#endif + + // mTimers is maintained in a "pseudo-sorted" order wrt the timeouts. + // Specifcally, mTimers is sorted according to the timeouts *if you ignore the + // canceled entries* (those whose mTimerImpl is nullptr). Notably this means + // that you cannot use a binary search on this list. + nsTArray mTimers MOZ_GUARDED_BY(mMonitor); + + // Set only at the start of the thread's Run(): + uint32_t mAllowedEarlyFiringMicroseconds MOZ_GUARDED_BY(mMonitor); + + ProfilerThreadId mProfilerThreadId MOZ_GUARDED_BY(mMonitor); + + // Time at which we were intending to wake up the last time that we slept. + // Is "null" if we have never slept or if our last sleep was "forever". + TimeStamp mIntendedWakeupTime; + +#if TIMER_THREAD_STATISTICS + static constexpr size_t sTimersFiredPerWakeupBucketCount = 16; + static inline constexpr std::array + sTimersFiredPerWakeupThresholds = { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 20, 30, 40, 50, 70, (size_t)(-1)}; + + mutable AutoTArray + mTimersFiredPerWakeup MOZ_GUARDED_BY(mMonitor) = {0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0}; + mutable AutoTArray + mTimersFiredPerUnnotifiedWakeup MOZ_GUARDED_BY(mMonitor) = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + mutable AutoTArray + mTimersFiredPerNotifiedWakeup MOZ_GUARDED_BY(mMonitor) = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + + mutable size_t mTotalTimersAdded MOZ_GUARDED_BY(mMonitor) = 0; + mutable size_t mTotalTimersRemoved MOZ_GUARDED_BY(mMonitor) = 0; + mutable size_t mTotalTimersFiredNotified MOZ_GUARDED_BY(mMonitor) = 0; + mutable size_t mTotalTimersFiredUnnotified MOZ_GUARDED_BY(mMonitor) = 0; + + mutable size_t mTotalWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; + mutable size_t mTotalUnnotifiedWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; + mutable size_t mTotalNotifiedWakeupCount MOZ_GUARDED_BY(mMonitor) = 0; + + mutable double mTotalActualTimerFiringDelayNotified MOZ_GUARDED_BY(mMonitor) = + 0.0; + mutable double mTotalActualTimerFiringDelayUnnotified + MOZ_GUARDED_BY(mMonitor) = 0.0; + + mutable TimeStamp mFirstTimerAdded MOZ_GUARDED_BY(mMonitor); + + mutable size_t mEarlyWakeups MOZ_GUARDED_BY(mMonitor) = 0; + mutable double mTotalEarlyWakeupTime MOZ_GUARDED_BY(mMonitor) = 0.0; + + void PrintStatistics() const; +#endif +}; +#endif /* TimerThread_h___ */ diff --git a/xpcom/threads/VsyncTaskManager.cpp b/xpcom/threads/VsyncTaskManager.cpp new file mode 100644 index 0000000000..ba4201af45 --- /dev/null +++ b/xpcom/threads/VsyncTaskManager.cpp @@ -0,0 +1,22 @@ +/* -*- 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 "VsyncTaskManager.h" +#include "InputTaskManager.h" + +namespace mozilla { + +StaticRefPtr VsyncTaskManager::gHighPriorityTaskManager; + +void VsyncTaskManager::Init() { + gHighPriorityTaskManager = new VsyncTaskManager(); +} + +void VsyncTaskManager::WillRunTask() { + TaskManager::WillRunTask(); + InputTaskManager::Get()->NotifyVsync(); +}; +} // namespace mozilla diff --git a/xpcom/threads/VsyncTaskManager.h b/xpcom/threads/VsyncTaskManager.h new file mode 100644 index 0000000000..e284ebf47b --- /dev/null +++ b/xpcom/threads/VsyncTaskManager.h @@ -0,0 +1,26 @@ +/* -*- 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 mozilla_VsyncTaskManager_h +#define mozilla_VsyncTaskManager_h + +#include "TaskController.h" +#include "mozilla/StaticPtr.h" + +namespace mozilla { +class VsyncTaskManager : public TaskManager { + public: + static VsyncTaskManager* Get() { return gHighPriorityTaskManager.get(); } + static void Cleanup() { gHighPriorityTaskManager = nullptr; } + static void Init(); + + void WillRunTask() override; + + private: + static StaticRefPtr gHighPriorityTaskManager; +}; +} // namespace mozilla +#endif diff --git a/xpcom/threads/WinHandleWatcher.cpp b/xpcom/threads/WinHandleWatcher.cpp new file mode 100644 index 0000000000..d475993925 --- /dev/null +++ b/xpcom/threads/WinHandleWatcher.cpp @@ -0,0 +1,306 @@ +/* -*- 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 +#include + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Assertions.h" +#include "mozilla/Logging.h" +#include "mozilla/Mutex.h" +#include "mozilla/RefPtr.h" +#include "mozilla/ThreadSafety.h" +#include "mozilla/WinHandleWatcher.h" + +#include "nsCOMPtr.h" +#include "nsIRunnable.h" +#include "nsISerialEventTarget.h" +#include "nsISupportsImpl.h" +#include "nsITargetShutdownTask.h" +#include "nsIWeakReferenceUtils.h" +#include "nsThreadUtils.h" + +mozilla::LazyLogModule sHWLog("HandleWatcher"); + +namespace mozilla { +namespace details { +struct WaitHandleDeleter { + void operator()(PTP_WAIT waitHandle) { + MOZ_LOG(sHWLog, LogLevel::Debug, ("Closing PTP_WAIT %p", waitHandle)); + ::CloseThreadpoolWait(waitHandle); + } +}; +} // namespace details +using WaitHandlePtr = UniquePtr; + +// HandleWatcher::Impl +// +// The backing implementation of HandleWatcher is a PTP_WAIT, an OS-threadpool +// wait-object. Windows doesn't actually create a new thread per wait-object; +// OS-threadpool threads are assigned to wait-objects only when their associated +// handle become signaled -- although explicit documentation of this fact is +// somewhat obscurely placed. [1] +// +// Throughout this class, we use manual locking and unlocking guarded by Clang's +// thread-safety warnings, rather than scope-based lock-guards. See `Replace()` +// for an explanation and justification. +// +// [1]https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-waitformultipleobjects#remarks +class HandleWatcher::Impl final : public nsITargetShutdownTask { + NS_DECL_THREADSAFE_ISUPPORTS + + public: + Impl() = default; + + private: + ~Impl() { MOZ_ASSERT(IsStopped()); } + + struct Data { + // The watched handle and its callback. + HANDLE handle; + RefPtr target; + nsCOMPtr runnable; + + // Handle to the threadpool wait-object. + WaitHandlePtr waitHandle; + // A pointer to ourselves, notionally owned by the wait-object. + RefPtr self; + + // (We can't actually do this because a) it has annoying consequences in + // C++20 thanks to P1008R1, and b) Clang just ignores it anyway.) + // + // ~Data() MOZ_EXCLUDES(mMutex) = default; + }; + + mozilla::Mutex mMutex{"HandleWatcher::Impl"}; + Data mData MOZ_GUARDED_BY(mMutex) = {}; + + // Callback from OS threadpool wait-object. + static void CALLBACK WaitCallback(PTP_CALLBACK_INSTANCE, void* ctx, + PTP_WAIT aWaitHandle, + TP_WAIT_RESULT aResult) { + static_cast(ctx)->OnWaitCompleted(aWaitHandle, aResult); + } + + void OnWaitCompleted(PTP_WAIT aWaitHandle, TP_WAIT_RESULT aResult) + MOZ_EXCLUDES(mMutex) { + MOZ_ASSERT(aResult == WAIT_OBJECT_0); + + mMutex.Lock(); + // If this callback is no longer the active callback, skip out. + // All cleanup is someone else's problem. + if (aWaitHandle != mData.waitHandle.get()) { + MOZ_LOG(sHWLog, LogLevel::Debug, + ("Recv'd already-stopped callback: HW %p | PTP_WAIT %p", this, + aWaitHandle)); + mMutex.Unlock(); + return; + } + + // Take our self-pointer so that we release it on exit. + RefPtr self = std::move(mData.self); + + MOZ_LOG(sHWLog, LogLevel::Info, + ("Recv'd callback: HW %p | handle %p | target %p | PTP_WAIT %p", + this, mData.handle, mData.target.get(), aWaitHandle)); + + // This may fail if (for example) `mData.target` is being shut down, but we + // have not yet received the shutdown callback. + mData.target->Dispatch(mData.runnable.forget()); + Replace(Data{}); + } + + public: + static RefPtr Create(HANDLE aHandle, nsIEventTarget* aTarget, + already_AddRefed aRunnable) { + auto impl = MakeRefPtr(); + bool const ok [[maybe_unused]] = + impl->Watch(aHandle, aTarget, std::move(aRunnable)); + MOZ_ASSERT(ok); + return impl; + } + + private: + bool Watch(HANDLE aHandle, nsIEventTarget* aTarget, + already_AddRefed aRunnable) MOZ_EXCLUDES(mMutex) { + MOZ_ASSERT(aHandle); + MOZ_ASSERT(aTarget); + + RefPtr target(aTarget); + + WaitHandlePtr waitHandle{ + ::CreateThreadpoolWait(&WaitCallback, this, nullptr)}; + if (!waitHandle) { + return false; + } + + { + mMutex.Lock(); + + nsresult const ret = aTarget->RegisterShutdownTask(this); + if (NS_FAILED(ret)) { + mMutex.Unlock(); + return false; + } + + MOZ_LOG(sHWLog, LogLevel::Info, + ("Setting callback: HW %p | handle %p | target %p | PTP_WAIT %p", + this, aHandle, aTarget, waitHandle.get())); + + // returns `void`; presumably always succeeds given a successful + // `::CreateThreadpoolWait()` + ::SetThreadpoolWait(waitHandle.get(), aHandle, nullptr); + // After this point, you must call `FlushWaitHandle(waitHandle.get())` + // before destroying the wait handle. (Note that this must be done while + // *not* holding `mMutex`!) + + Replace(Data{.handle = aHandle, + .target = std::move(target), + .runnable = aRunnable, + .waitHandle = std::move(waitHandle), + .self = this}); + } + + return true; + } + + void TargetShutdown() MOZ_EXCLUDES(mMutex) override final { + mMutex.Lock(); + + MOZ_LOG(sHWLog, LogLevel::Debug, + ("Target shutdown: HW %p | handle %p | target %p | PTP_WAIT %p", + this, mData.handle, mData.target.get(), mData.waitHandle.get())); + + // Clear mData.target, since there's no need to unregister the shutdown task + // anymore. Hold onto it until we release the mutex, though, to avoid any + // reentrancy issues. + // + // This is more for internal consistency than safety: someone has to be + // shutting `target` down, and that someone isn't us, so there's necessarily + // another reference out there. (Although decrementing the refcount might + // still have arbitrary effects if someone's been excessively clever with + // nsISupports::Release...) + auto const oldTarget = std::move(mData.target); + Replace(Data{}); + // (Static-assert that the mutex has indeed been released.) + ([&]() MOZ_EXCLUDES(mMutex) {})(); + } + + public: + void Stop() MOZ_EXCLUDES(mMutex) { + mMutex.Lock(); + Replace(Data{}); + } + + bool IsStopped() MOZ_EXCLUDES(mMutex) { + mozilla::MutexAutoLock lock(mMutex); + return !mData.handle; + } + + private: + // Throughout this class, we use manual locking and unlocking guarded by + // Clang's thread-safety warnings, rather than scope-based lock-guards. This + // is largely driven by `Replace()`, below, which performs both operations + // which require the mutex to be held and operations which require it to not + // be held, and therefore must explicitly sequence the mutex release. + // + // These explicit locks, unlocks, and annotations are both alien to C++ and + // offensively tedious; but they _are_ still checked for state consistency at + // scope boundaries. (The concerned reader is invited to test this by + // deliberately removing an `mMutex.Unlock()` call from anywhere in the class + // and viewing the resultant compiler diagnostics.) + // + // A more principled, or at least differently-principled, implementation might + // create a scope-based lock-guard and pass it to `Replace()` to dispose of at + // the proper time. Alas, it cannot be communicated to Clang's thread-safety + // checker that such a guard is associated with `mMutex`. + // + void Replace(Data&& aData) MOZ_CAPABILITY_RELEASE(mMutex) { + // either both handles are NULL, or neither is + MOZ_ASSERT(!!aData.handle == !!aData.waitHandle); + + if (mData.handle) { + MOZ_LOG(sHWLog, LogLevel::Info, + ("Stop callback: HW %p | handle %p | target %p | PTP_WAIT %p", + this, mData.handle, mData.target.get(), mData.waitHandle.get())); + } + + if (mData.target) { + mData.target->UnregisterShutdownTask(this); + } + + // Extract the old data and insert the new -- but hold onto the old data for + // now. (See [1] and [2], below.) + Data oldData = std::exchange(mData, std::move(aData)); + + //////////////////////////////////////////////////////////////////////////// + // Release the mutex. + mMutex.Unlock(); + //////////////////////////////////////////////////////////////////////////// + + // [1] `oldData.self` will be unset if the old callback already ran (or if + // there was no old callback in the first place). If it's set, though, we + // need to explicitly clear out the wait-object first. + if (oldData.self) { + MOZ_ASSERT(oldData.waitHandle); + FlushWaitHandle(oldData.waitHandle.get()); + } + + // [2] oldData also includes several other reference-counted pointers. It's + // possible that these may be the last pointer to something, so releasing + // them may have arbitrary side-effects -- like calling this->Stop(), which + // will try to reacquire the mutex. + // + // Now that we've released the mutex, we can (implicitly) release them all + // here. + } + + // Either confirm as complete or cancel any callbacks on aWaitHandle. Block + // until this is done. (See documentation for ::CloseThreadpoolWait().) + void FlushWaitHandle(PTP_WAIT aWaitHandle) MOZ_EXCLUDES(mMutex) { + ::SetThreadpoolWait(aWaitHandle, nullptr, nullptr); + // This might block on `OnWaitCompleted()`, so we can't hold `mMutex` here. + ::WaitForThreadpoolWaitCallbacks(aWaitHandle, TRUE); + // ::CloseThreadpoolWait() itself is the caller's responsibility. + } +}; + +NS_IMPL_ISUPPORTS(HandleWatcher::Impl, nsITargetShutdownTask) + +////// +// HandleWatcher member function implementations + +HandleWatcher::HandleWatcher() : mImpl{} {} +HandleWatcher::~HandleWatcher() { + if (mImpl) { + MOZ_ASSERT(mImpl->IsStopped()); + mImpl->Stop(); // just in case, in release + } +} + +HandleWatcher::HandleWatcher(HandleWatcher&&) noexcept = default; +HandleWatcher& HandleWatcher::operator=(HandleWatcher&&) noexcept = default; + +void HandleWatcher::Watch(HANDLE aHandle, nsIEventTarget* aTarget, + already_AddRefed aRunnable) { + auto impl = Impl::Create(aHandle, aTarget, std::move(aRunnable)); + MOZ_ASSERT(impl); + + if (mImpl) { + mImpl->Stop(); + } + mImpl = std::move(impl); +} + +void HandleWatcher::Stop() { + if (mImpl) { + mImpl->Stop(); + } +} + +bool HandleWatcher::IsStopped() { return !mImpl || mImpl->IsStopped(); } + +} // namespace mozilla diff --git a/xpcom/threads/WinHandleWatcher.h b/xpcom/threads/WinHandleWatcher.h new file mode 100644 index 0000000000..eb1e1c0245 --- /dev/null +++ b/xpcom/threads/WinHandleWatcher.h @@ -0,0 +1,117 @@ +/* -*- 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 WinHandleWatcher_h__ +#define WinHandleWatcher_h__ + +#include + +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/RefPtr.h" +#include "mozilla/UniquePtr.h" + +#include "nsIEventTarget.h" +#include "nsIRunnable.h" +#include "nsIThread.h" +#include "nsThreadUtils.h" + +namespace mozilla { +/////////////////////////////////////////////////////////////////////// +// HandleWatcher +// +// Enqueues a task onto an event target when a watched Win32 synchronization +// object [1] enters the signaled state. +// +// The HandleWatcher must be stopped before either it or the synchronization +// object is destroyed. +// +////// +// +// Example of use: +// +// ``` +// class MyClass { +// /* ... */ +// +// HANDLE CreateThing(); +// void OnComplete(); +// public: +// void Fire() { +// mHandle.set(CreateThing()); +// mWatcher.Watch( +// mHandle.get(), NS_GetCurrentThread(), // (or any other thread) +// NS_NewRunnableFunction("OnComplete", [this] { OnComplete(); })); +// } +// +// ~MyClass() { mWatcher.Stop(); } +// +// HandleWatcher mWatcher; +// HandlePtr mHandle; // calls ::CloseHandle() on destruction +// }; +// ``` +// +// Note: this example demonstrates why an explicit `Stop()` is necessary in +// MyClass's destructor. Without it, the `HandlePtr` would destroy the HANDLE -- +// and possibly whatever other data `OnComplete()` depends on -- before the +// watch was stopped! +// +// Rather than make code correctness silently dependent on member object order, +// we require that HandleWatcher already be stopped at its destruction time. +// (This does not guarantee correctness, as the task may still reference a +// partially-destroyed transitive owner; but, short of RIIR, a guarantee of +// correctness is probably not possible here.) +// +////// +// +// [1]https://learn.microsoft.com/en-us/windows/win32/sync/synchronization-objects +class HandleWatcher { + public: + class Impl; + + HandleWatcher(); + ~HandleWatcher(); + + HandleWatcher(HandleWatcher const&) = delete; + HandleWatcher& operator=(HandleWatcher const&) = delete; + + HandleWatcher(HandleWatcher&&) noexcept; + HandleWatcher& operator=(HandleWatcher&&) noexcept; + + // Watches the given Win32 HANDLE, which must be a synchronization object. As + // soon as the HANDLE is signaled, posts `aRunnable` to `aTarget`. + // + // `aHandle` is merely borrowed for the duration of the watch: the + // HandleWatcher does not attempt to close it, and its lifetime must exceed + // that of the watch. + // + // If the watch is stopped for any reason other than completion, `aRunnable` + // is released immediately, on the same thread from which the Watch was + // stopped. + // + // The watch is stopped when any of the following occurs: + // * `Stop()` is called. + // * `Watch()` is called again, even without an intervening `Stop()`. + // * This object is destroyed. + // * `aTarget` shuts down. + // * `aHandle` becomes signaled. + // + void Watch(HANDLE aHandle, nsIEventTarget* aTarget, + already_AddRefed aRunnable); + + // Cancels the current watch, if any. + // + // Idempotent. Thread-safe with respect to other calls of `Stop()`. + void Stop(); + + // Potentially racy. Only intended for tests. + bool IsStopped(); + + private: + RefPtr mImpl; +}; +} // namespace mozilla + +#endif // WinHandleWatcher_h__ diff --git a/xpcom/threads/components.conf b/xpcom/threads/components.conf new file mode 100644 index 0000000000..53f76d3b89 --- /dev/null +++ b/xpcom/threads/components.conf @@ -0,0 +1,29 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +Classes = [ + { + 'cid': '{03d68f92-9513-4e25-9be9-7cb239874172}', + 'contract_ids': ['@mozilla.org/process/environment;1'], + 'legacy_constructor': 'nsEnvironment::Create', + 'headers': ['/xpcom/threads/nsEnvironment.h'], + 'js_name': 'env', + 'interfaces': ['nsIEnvironment'], + }, + { + 'cid': '{5ff24248-1dd2-11b2-8427-fbab44f29bc8}', + 'contract_ids': ['@mozilla.org/timer;1'], + 'legacy_constructor': 'nsTimer::XPCOMConstructor', + 'headers': ['/xpcom/threads/nsTimerImpl.h'], + 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_SOCKET_AND_UTILITY_PROCESS, + }, + { + 'cid': '{d39a8904-2e09-4a3a-a273-c3bec7db2bfe}', + 'contract_ids': ['@mozilla.org/timer-manager;1'], + 'headers': ['/xpcom/threads/nsTimerImpl.h'], + 'type': 'nsTimerManager', + }, +] diff --git a/xpcom/threads/moz.build b/xpcom/threads/moz.build new file mode 100644 index 0000000000..06d10ad331 --- /dev/null +++ b/xpcom/threads/moz.build @@ -0,0 +1,144 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +XPIDL_SOURCES += [ + "nsIDirectTaskDispatcher.idl", + "nsIEnvironment.idl", + "nsIEventTarget.idl", + "nsIIdlePeriod.idl", + "nsINamed.idl", + "nsIProcess.idl", + "nsIRunnable.idl", + "nsISerialEventTarget.idl", + "nsISupportsPriority.idl", + "nsIThread.idl", + "nsIThreadInternal.idl", + "nsIThreadManager.idl", + "nsIThreadPool.idl", + "nsIThreadShutdown.idl", + "nsITimer.idl", +] + +XPIDL_MODULE = "xpcom_threads" + +XPCOM_MANIFESTS += [ + "components.conf", +] + +EXPORTS += [ + "MainThreadUtils.h", + "nsICancelableRunnable.h", + "nsIDiscardableRunnable.h", + "nsIIdleRunnable.h", + "nsITargetShutdownTask.h", + "nsMemoryPressure.h", + "nsProcess.h", + "nsProxyRelease.h", + "nsThread.h", + "nsThreadManager.h", + "nsThreadPool.h", + "nsThreadUtils.h", +] + +EXPORTS.mozilla += [ + "AbstractThread.h", + "BlockingResourceBase.h", + "CondVar.h", + "CPUUsageWatcher.h", + "DataMutex.h", + "DeadlockDetector.h", + "DelayedRunnable.h", + "EventQueue.h", + "EventTargetCapability.h", + "IdlePeriodState.h", + "IdleTaskRunner.h", + "InputTaskManager.h", + "LazyIdleThread.h", + "MainThreadIdlePeriod.h", + "Monitor.h", + "MozPromise.h", + "Mutex.h", + "Queue.h", + "RecursiveMutex.h", + "ReentrantMonitor.h", + "RWLock.h", + "SchedulerGroup.h", + "SharedThreadPool.h", + "SpinEventLoopUntil.h", + "StateMirroring.h", + "StateWatching.h", + "SynchronizedEventQueue.h", + "SyncRunnable.h", + "TaskController.h", + "TaskDispatcher.h", + "TaskQueue.h", + "ThreadBound.h", + "ThreadEventQueue.h", + "ThrottledEventQueue.h", + "VsyncTaskManager.h", +] + +SOURCES += [ + "IdlePeriodState.cpp", + "IdleTaskRunner.cpp", + "ThreadDelay.cpp", +] + +UNIFIED_SOURCES += [ + "AbstractThread.cpp", + "BlockingResourceBase.cpp", + "CPUUsageWatcher.cpp", + "DelayedRunnable.cpp", + "EventQueue.cpp", + "InputTaskManager.cpp", + "LazyIdleThread.cpp", + "MainThreadIdlePeriod.cpp", + "nsEnvironment.cpp", + "nsMemoryPressure.cpp", + "nsProcessCommon.cpp", + "nsProxyRelease.cpp", + "nsThread.cpp", + "nsThreadManager.cpp", + "nsThreadPool.cpp", + "nsThreadUtils.cpp", + "nsTimerImpl.cpp", + "RecursiveMutex.cpp", + "RWLock.cpp", + "SchedulerGroup.cpp", + "SharedThreadPool.cpp", + "SynchronizedEventQueue.cpp", + "TaskController.cpp", + "TaskQueue.cpp", + "ThreadEventQueue.cpp", + "ThreadEventTarget.cpp", + "ThreadLocalVariables.cpp", + "ThrottledEventQueue.cpp", + "TimerThread.cpp", + "VsyncTaskManager.cpp", +] + +if CONFIG["OS_ARCH"] == "WINNT": + EXPORTS.mozilla += ["WinHandleWatcher.h"] + UNIFIED_SOURCES += ["WinHandleWatcher.cpp"] + +# Should match the conditions in toolkit/components/backgroundhangmonitor/moz.build +if ( + CONFIG["NIGHTLY_BUILD"] + and not CONFIG["MOZ_DEBUG"] + and not CONFIG["MOZ_TSAN"] + and not CONFIG["MOZ_ASAN"] +): + DEFINES["MOZ_ENABLE_BACKGROUND_HANG_MONITOR"] = 1 + +LOCAL_INCLUDES += [ + "../build", + "/caps", + "/tools/profiler", +] + +FINAL_LIBRARY = "xul" + +include("/ipc/chromium/chromium-config.mozbuild") diff --git a/xpcom/threads/nsEnvironment.cpp b/xpcom/threads/nsEnvironment.cpp new file mode 100644 index 0000000000..54efd9194a --- /dev/null +++ b/xpcom/threads/nsEnvironment.cpp @@ -0,0 +1,136 @@ +/* -*- 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 "nsEnvironment.h" +#include "prenv.h" +#include "nsBaseHashtable.h" +#include "nsHashKeys.h" +#include "nsPromiseFlatString.h" +#include "nsDependentString.h" +#include "nsNativeCharsetUtils.h" +#include "mozilla/Printf.h" +#include "mozilla/StaticMutex.h" + +using namespace mozilla; + +NS_IMPL_ISUPPORTS(nsEnvironment, nsIEnvironment) + +nsresult nsEnvironment::Create(REFNSIID aIID, void** aResult) { + nsresult rv; + *aResult = nullptr; + + nsEnvironment* obj = new nsEnvironment(); + + rv = obj->QueryInterface(aIID, aResult); + if (NS_FAILED(rv)) { + delete obj; + } + return rv; +} + +NS_IMETHODIMP +nsEnvironment::Exists(const nsAString& aName, bool* aOutValue) { + nsAutoCString nativeName; + nsresult rv = NS_CopyUnicodeToNative(aName, nativeName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString nativeVal; +#if defined(XP_UNIX) + /* For Unix/Linux platforms we follow the Unix definition: + * An environment variable exists when |getenv()| returns a non-nullptr + * value. An environment variable does not exist when |getenv()| returns + * nullptr. + */ + const char* value = PR_GetEnv(nativeName.get()); + *aOutValue = value && *value; +#else + /* For non-Unix/Linux platforms we have to fall back to a + * "portable" definition (which is incorrect for Unix/Linux!!!!) + * which simply checks whether the string returned by |Get()| is empty + * or not. + */ + nsAutoString value; + Get(aName, value); + *aOutValue = !value.IsEmpty(); +#endif /* XP_UNIX */ + + return NS_OK; +} + +NS_IMETHODIMP +nsEnvironment::Get(const nsAString& aName, nsAString& aOutValue) { + nsAutoCString nativeName; + nsresult rv = NS_CopyUnicodeToNative(aName, nativeName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsAutoCString nativeVal; + const char* value = PR_GetEnv(nativeName.get()); + if (value && *value) { + rv = NS_CopyNativeToUnicode(nsDependentCString(value), aOutValue); + } else { + aOutValue.Truncate(); + rv = NS_OK; + } + + return rv; +} + +/* Environment strings must have static duration; We're gonna leak all of this + * at shutdown: this is by design, caused how Unix/Linux implement environment + * vars. + */ + +typedef nsBaseHashtableET EnvEntryType; +typedef nsTHashtable EnvHashType; + +static StaticMutex gEnvHashMutex; +static EnvHashType* gEnvHash MOZ_GUARDED_BY(gEnvHashMutex) = nullptr; + +static EnvHashType* EnsureEnvHash() MOZ_REQUIRES(gEnvHashMutex) { + if (!gEnvHash) { + gEnvHash = new EnvHashType; + } + return gEnvHash; +} + +NS_IMETHODIMP +nsEnvironment::Set(const nsAString& aName, const nsAString& aValue) { + nsAutoCString nativeName; + nsAutoCString nativeVal; + + nsresult rv = NS_CopyUnicodeToNative(aName, nativeName); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + rv = NS_CopyUnicodeToNative(aValue, nativeVal); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + StaticMutexAutoLock lock(gEnvHashMutex); + EnvEntryType* entry = EnsureEnvHash()->PutEntry(nativeName.get()); + if (!entry) { + return NS_ERROR_OUT_OF_MEMORY; + } + + SmprintfPointer newData = + mozilla::Smprintf("%s=%s", nativeName.get(), nativeVal.get()); + if (!newData) { + return NS_ERROR_OUT_OF_MEMORY; + } + + PR_SetEnv(newData.get()); + if (entry->GetData()) { + free(entry->GetData()); + } + entry->SetData(newData.release()); + return NS_OK; +} diff --git a/xpcom/threads/nsEnvironment.h b/xpcom/threads/nsEnvironment.h new file mode 100644 index 0000000000..d371050ec5 --- /dev/null +++ b/xpcom/threads/nsEnvironment.h @@ -0,0 +1,34 @@ +/* -*- 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 nsEnvironment_h__ +#define nsEnvironment_h__ + +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "nsIEnvironment.h" + +#define NS_ENVIRONMENT_CID \ + { \ + 0X3D68F92UL, 0X9513, 0X4E25, { \ + 0X9B, 0XE9, 0X7C, 0XB2, 0X39, 0X87, 0X41, 0X72 \ + } \ + } +#define NS_ENVIRONMENT_CONTRACTID "@mozilla.org/process/environment;1" + +class nsEnvironment final : public nsIEnvironment { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIENVIRONMENT + + static nsresult Create(REFNSIID aIID, void** aResult); + + private: + nsEnvironment() {} + ~nsEnvironment() = default; +}; + +#endif /* !nsEnvironment_h__ */ diff --git a/xpcom/threads/nsICancelableRunnable.h b/xpcom/threads/nsICancelableRunnable.h new file mode 100644 index 0000000000..7aa98c86b6 --- /dev/null +++ b/xpcom/threads/nsICancelableRunnable.h @@ -0,0 +1,40 @@ +/* -*- 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 nsICancelableRunnable_h__ +#define nsICancelableRunnable_h__ + +#include "nsISupports.h" + +#define NS_ICANCELABLERUNNABLE_IID \ + { \ + 0xde93dc4c, 0x5eea, 0x4eb7, { \ + 0xb6, 0xd1, 0xdb, 0xf1, 0xe0, 0xce, 0xf6, 0x5c \ + } \ + } + +class nsICancelableRunnable : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ICANCELABLERUNNABLE_IID) + + /* + * Cancels a pending task, so that calling run() on the task is a no-op. + * Calling cancel after the task execution has begun will be a no-op. + * Calling this method twice is considered an error. + * + * @throws NS_ERROR_UNEXPECTED + * Indicates that the runnable has already been canceled. + */ + virtual nsresult Cancel() = 0; + + protected: + nsICancelableRunnable() = default; + virtual ~nsICancelableRunnable() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsICancelableRunnable, NS_ICANCELABLERUNNABLE_IID) + +#endif // nsICancelableRunnable_h__ diff --git a/xpcom/threads/nsIDirectTaskDispatcher.idl b/xpcom/threads/nsIDirectTaskDispatcher.idl new file mode 100644 index 0000000000..7d44608708 --- /dev/null +++ b/xpcom/threads/nsIDirectTaskDispatcher.idl @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsIRunnable.idl" + +%{C++ +#include "mozilla/AlreadyAddRefed.h" +%} + +native alreadyAddRefed_nsIRunnable(already_AddRefed); + +/* + * The primary use of this interface is to allow any nsISerialEventTarget to + * provide Direct Task dispatching which is similar (but not identical to) the + * microtask semantics of JS promises. + * New direct task may be dispatched when a current direct task is running. In + * which case they will be run in FIFO order. + */ +[uuid(e05bf0fe-94b7-4e28-8462-a8368da9c136)] +interface nsIDirectTaskDispatcher : nsISupports +{ + /** + * Dispatch an event for the nsISerialEventTarget, using the direct task + * queue. + * + * This function must be called from the same nsISerialEventTarget + * implementing direct task dispatching. + * + * @param event + * The alreadyAddRefed<> event to dispatch. + * + */ + [noscript] void dispatchDirectTask(in alreadyAddRefed_nsIRunnable event); + + /** + * Synchronously run any pending direct tasks queued. + */ + [noscript] void drainDirectTasks(); + + /** + * Returns true if any direct tasks are pending. + */ + [noscript] bool haveDirectTasks(); + + %{C++ + // Infallible version of the above. Will assert that it is successful. + bool HaveDirectTasks() { + bool value = false; + MOZ_ALWAYS_SUCCEEDS(HaveDirectTasks(&value)); + return value; + } + %} + +}; diff --git a/xpcom/threads/nsIDiscardableRunnable.h b/xpcom/threads/nsIDiscardableRunnable.h new file mode 100644 index 0000000000..873b1f5d93 --- /dev/null +++ b/xpcom/threads/nsIDiscardableRunnable.h @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=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 XPCOM_THREADS_NSIDISCARDABLERUNNABLE_H_ +#define XPCOM_THREADS_NSIDISCARDABLERUNNABLE_H_ + +#include "nsISupports.h" + +/** + * An interface implemented by nsIRunnable tasks for which nsIRunnable::Run() + * might not be called. + */ +#define NS_IDISCARDABLERUNNABLE_IID \ + { \ + 0xde93dc4c, 0x755c, 0x4cdc, { \ + 0x96, 0x76, 0x35, 0xc6, 0x48, 0x81, 0x59, 0x78 \ + } \ + } + +class NS_NO_VTABLE nsIDiscardableRunnable : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IDISCARDABLERUNNABLE_IID) + + /** + * Called exactly once on a queued task only if nsIRunnable::Run() is not + * called. + */ + virtual void OnDiscard() = 0; + + protected: + nsIDiscardableRunnable() = default; + virtual ~nsIDiscardableRunnable() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIDiscardableRunnable, + NS_IDISCARDABLERUNNABLE_IID) + +#endif // XPCOM_THREADS_NSIDISCARDABLERUNNABLE_H_ diff --git a/xpcom/threads/nsIEnvironment.idl b/xpcom/threads/nsIEnvironment.idl new file mode 100644 index 0000000000..60da8ba76a --- /dev/null +++ b/xpcom/threads/nsIEnvironment.idl @@ -0,0 +1,54 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * Scriptable access to the current process environment. + * + */ +[scriptable, uuid(101d5941-d820-4e85-a266-9a3469940807)] +interface nsIEnvironment : nsISupports +{ + /** + * Set the value of an environment variable. + * + * @param aName the variable name to set. + * @param aValue the value to set. + */ + void set(in AString aName, in AString aValue); + + /** + * Get the value of an environment variable. + * + * @param aName the variable name to retrieve. + * @return returns the value of the env variable. An empty string + * will be returned when the env variable does not exist or + * when the value itself is an empty string - please use + * |exists()| to probe whether the env variable exists + * or not. + */ + AString get(in AString aName); + + /** + * Check the existence of an environment variable. + * This method checks whether an environment variable is present in + * the environment or not. + * + * - For Unix/Linux platforms we follow the Unix definition: + * An environment variable exists when |getenv()| returns a non-NULL value. + * An environment variable does not exist when |getenv()| returns NULL. + * - For non-Unix/Linux platforms we have to fall back to a + * "portable" definition (which is incorrect for Unix/Linux!!!!) + * which simply checks whether the string returned by |Get()| is empty + * or not. + * + * @param aName the variable name to probe. + * @return if the variable has been set, the value returned is + * PR_TRUE. If the variable was not defined in the + * environment PR_FALSE will be returned. + */ + boolean exists(in AString aName); +}; diff --git a/xpcom/threads/nsIEventTarget.idl b/xpcom/threads/nsIEventTarget.idl new file mode 100644 index 0000000000..edd49f306d --- /dev/null +++ b/xpcom/threads/nsIEventTarget.idl @@ -0,0 +1,227 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsISupports.idl" +#include "nsIRunnable.idl" +%{C++ +#include "nsCOMPtr.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Atomics.h" + +class nsITargetShutdownTask; +%} + +native alreadyAddRefed_nsIRunnable(already_AddRefed); +[ptr] native nsITargetShutdownTask(nsITargetShutdownTask); + +[builtinclass, scriptable, rust_sync, uuid(a03b8b63-af8b-4164-b0e5-c41e8b2b7cfa)] +interface nsIEventTarget : nsISupports +{ + /* until we can get rid of all uses, keep the non-alreadyAddRefed<> version */ +%{C++ + nsresult Dispatch(nsIRunnable* aEvent, uint32_t aFlags) { + return Dispatch(nsCOMPtr(aEvent).forget(), aFlags); + } +%} + + /** + * This flag specifies the default mode of event dispatch, whereby the event + * is simply queued for later processing. When this flag is specified, + * dispatch returns immediately after the event is queued. + */ + const unsigned long DISPATCH_NORMAL = 0; + + // NOTE: 1 used to be DISPATCH_SYNC + + /** + * This flag specifies that the dispatch is occurring from a running event + * that was dispatched to the same event target, and that event is about to + * finish. + * + * A thread pool can use this as an optimization hint to not spin up + * another thread, since the current thread is about to become idle. + * + * These events are always async. + */ + const unsigned long DISPATCH_AT_END = 2; + + /** + * This flag specifies that the dispatched event may block the thread on + * which it executes, usually by doing some sort of I/O. This information + * may be used by the event target to execute the job on a thread + * specifically dedicated to doing I/O, leaving other threads available for + * CPU-intensive work. + */ + const unsigned long DISPATCH_EVENT_MAY_BLOCK = 4; + + /** + * This flag specifies that the dispatched event should be delivered to the + * target thread even if the thread has been configured to block dispatching + * of runnables. This is generally done for threads which have their own + * internal event loop, such as thread pools or the timer thread, and will not + * service runnables dispatched to them until shutdown. + */ + const unsigned long DISPATCH_IGNORE_BLOCK_DISPATCH = 8; + + /** + * IsOnCurrentThread() should return true if events dispatched to this target + * can possibly run on the current thread, and false otherwise. In the case + * of an nsIEventTarget for a thread pool, it should return true on all + * threads in the pool. In the case of a non-thread nsIEventTarget such as + * ThrottledEventQueue, it should return true on the thread where events are + * expected to be processed, even if no events from the queue are actually + * being processed right now. + * + * When called on an nsISerialEventTarget, IsOnCurrentThread can be used to + * ensure that no other thread has "ownership" of the event target. As such, + * it's useful for asserting that an object is only used on a particular + * thread. IsOnCurrentThread can't guarantee that the current event has been + * dispatched through a particular event target. + * + * The infallible version of IsOnCurrentThread() is optimized to avoid a + * virtual call for non-thread event targets. Thread targets should set + * mThread to their virtual PRThread. Non-thread targets should leave + * mThread null and implement IsOnCurrentThreadInfallible() to + * return the correct answer. + * + * The fallible version of IsOnCurrentThread may return errors, such as during + * shutdown. If it does not return an error, it should return the same result + * as the infallible version. The infallible method should return the correct + * result regardless of whether the fallible method returns an error. + */ + %{C++ +public: + // Infallible. Defined in nsThreadUtils.cpp. Delegates to + // IsOnCurrentThreadInfallible when mThread is null. + bool IsOnCurrentThread(); + +protected: + mozilla::Atomic mThread; + + nsIEventTarget() : mThread(nullptr) {} + %} + // Note that this method is protected. We define it through IDL, rather than + // in a %{C++ block, to ensure that the correct method indices are recorded + // for XPConnect purposes. + [noscript,notxpcom] boolean isOnCurrentThreadInfallible(); + %{C++ +public: + %} + + // Fallible version of IsOnCurrentThread. + boolean isOnCurrentThread(); + + /** + * Dispatch an event to this event target. This function may be called from + * any thread, and it may be called re-entrantly. + * + * @param event + * The alreadyAddRefed<> event to dispatch. + * NOTE that the event will be leaked if it fails to dispatch. + * @param flags + * The flags modifying event dispatch. The flags are described in detail + * below. + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that event is null. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is shutting down and has finished processing + * events, so this event would never run and has not been dispatched. + */ + [noscript, binaryname(Dispatch)] void dispatchFromC(in alreadyAddRefed_nsIRunnable event, + [default(DISPATCH_NORMAL)] in unsigned long flags); + /** + * Version of Dispatch to expose to JS, which doesn't require an alreadyAddRefed<> + * (it will be converted to that internally) + * + * @param event + * The (raw) event to dispatch. + * @param flags + * The flags modifying event dispatch. The flags are described in detail + * below. + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that event is null. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is shutting down and has finished processing + * events, so this event would never run and has not been dispatched. + */ + [binaryname(DispatchFromScript)] void dispatch(in nsIRunnable event, in unsigned long flags); + /** + * Dispatch an event to this event target, but do not run it before delay + * milliseconds have passed. This function may be called from any thread. + * + * @param event + * The alreadyAddrefed<> event to dispatch. + * @param delay + * The delay (in ms) before running the event. If event does not rise to + * the top of the event queue before the delay has passed, it will be set + * aside to execute once the delay has passed. Otherwise, it will be + * executed immediately. + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that event is null. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is shutting down and has finished processing + * events, so this event would never run and has not been dispatched, or + * that delay is zero. + */ + [noscript] void delayedDispatch(in alreadyAddRefed_nsIRunnable event, in unsigned long delay); + + /** + * Register an task to be run on this event target when it begins shutting + * down. Shutdown tasks may be run in any order, and this function may be + * called from any thread. + * + * The event target may or may not continue accepting events during or after + * the shutdown task. The precise behaviour here depends on the event target. + * + * @param task + * The task to be registered to the target thread. + * NOTE that unlike `dispatch`, this will not leak the task if it fails. + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that task is null. + * @throws NS_ERROR_NOT_IMPLEMENTED + * Indicates that this event target doesn't support shutdown tasks. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is already shutting down, and no longer + * accepting events. + */ + [noscript] void registerShutdownTask(in nsITargetShutdownTask task); + + /** + * Unregisters an task previously registered with registerShutdownTask. This + * function may be called from any thread. + * + * @param task + * The task previously registered with registerShutdownTask + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that task is null. + * @throws NS_ERROR_NOT_IMPLEMENTED + * Indicates that this event target doesn't support shutdown tasks. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is already shutting down, and no longer + * accepting events, or that the shutdown task cannot be found. + */ + [noscript] void unregisterShutdownTask(in nsITargetShutdownTask task); +}; + +%{C++ +// convenient aliases: +#define NS_DISPATCH_NORMAL nsIEventTarget::DISPATCH_NORMAL +#define NS_DISPATCH_AT_END nsIEventTarget::DISPATCH_AT_END +#define NS_DISPATCH_EVENT_MAY_BLOCK nsIEventTarget::DISPATCH_EVENT_MAY_BLOCK +#define NS_DISPATCH_IGNORE_BLOCK_DISPATCH nsIEventTarget::DISPATCH_IGNORE_BLOCK_DISPATCH + +// Convenient NS_DECL variant that includes some C++-only methods. +#define NS_DECL_NSIEVENTTARGET_FULL \ + NS_DECL_NSIEVENTTARGET \ + /* Avoid hiding these methods */ \ + using nsIEventTarget::Dispatch; \ + using nsIEventTarget::IsOnCurrentThread; +%} diff --git a/xpcom/threads/nsIIdlePeriod.idl b/xpcom/threads/nsIIdlePeriod.idl new file mode 100644 index 0000000000..03ab45d80d --- /dev/null +++ b/xpcom/threads/nsIIdlePeriod.idl @@ -0,0 +1,32 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +%{C++ +namespace mozilla { +class TimeStamp; +} +%} + +native TimeStamp(mozilla::TimeStamp); + +/** + * An instance implementing nsIIdlePeriod is used by an associated + * nsIThread to estimate when it is likely that it will receive an + * event. + */ +[uuid(21dd35a2-eae9-4bd8-b470-0dfa35a0e3b9)] +interface nsIIdlePeriod : nsISupports +{ + /** + * Return an estimate of a point in time in the future when we + * think that the associated thread will become busy. Should + * return TimeStamp() (i.e. the null time) or a time less than + * TimeStamp::Now() if the thread is currently busy or will become + * busy very soon. + */ + TimeStamp getIdlePeriodHint(); +}; diff --git a/xpcom/threads/nsIIdleRunnable.h b/xpcom/threads/nsIIdleRunnable.h new file mode 100644 index 0000000000..7fe6149154 --- /dev/null +++ b/xpcom/threads/nsIIdleRunnable.h @@ -0,0 +1,48 @@ +/* -*- 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 nsIIdleRunnable_h__ +#define nsIIdleRunnable_h__ + +#include "nsISupports.h" +#include "mozilla/TimeStamp.h" + +#define NS_IIDLERUNNABLE_IID \ + { \ + 0x688be92e, 0x7ade, 0x4fdc, { \ + 0x9d, 0x83, 0x74, 0xcb, 0xef, 0xf4, 0xa5, 0x2c \ + } \ + } + +class nsIEventTarget; + +/** + * A task interface for tasks that can schedule their work to happen + * in increments bounded by a deadline. + */ +class nsIIdleRunnable : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_IIDLERUNNABLE_IID) + + /** + * Notify the task of a point in time in the future when the task + * should stop executing. + */ + virtual void SetDeadline(mozilla::TimeStamp aDeadline){}; + virtual void SetTimer(uint32_t aDelay, nsIEventTarget* aTarget) { + MOZ_ASSERT_UNREACHABLE( + "The nsIIdleRunnable instance does not support " + "idle dispatch with timeout!"); + }; + + protected: + nsIIdleRunnable() = default; + virtual ~nsIIdleRunnable() = default; +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsIIdleRunnable, NS_IIDLERUNNABLE_IID) + +#endif // nsIIdleRunnable_h__ diff --git a/xpcom/threads/nsINamed.idl b/xpcom/threads/nsINamed.idl new file mode 100644 index 0000000000..cdb7d88f30 --- /dev/null +++ b/xpcom/threads/nsINamed.idl @@ -0,0 +1,24 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * Represents an object with a name, such as a runnable or a timer. + */ + +[scriptable, uuid(0c5fe7de-7e83-4d0d-a8a6-4a6518b9a7b3)] +interface nsINamed : nsISupports +{ + /* + * A string describing the purpose of the runnable/timer/whatever. Useful + * for debugging. This attribute is read-only, but you can change it to a + * compile-time string literal with setName. + * + * WARNING: This attribute will be included in telemetry, so it should + * never contain privacy sensitive information. + */ + readonly attribute AUTF8String name; +}; diff --git a/xpcom/threads/nsIProcess.idl b/xpcom/threads/nsIProcess.idl new file mode 100644 index 0000000000..c15ded7a2f --- /dev/null +++ b/xpcom/threads/nsIProcess.idl @@ -0,0 +1,112 @@ +/* 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 "nsISupports.idl" + +interface nsIFile; +interface nsIObserver; + +[scriptable, uuid(609610de-9954-4a63-8a7c-346350a86403)] +interface nsIProcess : nsISupports +{ + /** + * Initialises the process with an executable to be run. Call the run method + * to run the executable. + * @param executable The executable to run. + */ + void init(in nsIFile executable); + + /** + * Kills the running process. After exiting the process will either have + * been killed or a failure will have been returned. + */ + void kill(); + + /** + * Executes the file this object was initialized with + * @param blocking Whether to wait until the process terminates before + returning or not. + * @param args An array of arguments to pass to the process in the + * native character set. + * @param count The length of the args array. + */ + void run(in boolean blocking, [array, size_is(count)] in string args, + in unsigned long count); + + /** + * Executes the file this object was initialized with optionally calling + * an observer after the process has finished running. + * @param args An array of arguments to pass to the process in the + * native character set. + * @param count The length of the args array. + * @param observer An observer to notify when the process has completed. It + * will receive this process instance as the subject and + * "process-finished" or "process-failed" as the topic. The + * observer will be notified on the main thread. + * @param holdWeak Whether to use a weak reference to hold the observer. + */ + void runAsync([array, size_is(count)] in string args, in unsigned long count, + [optional] in nsIObserver observer, [optional] in boolean holdWeak); + + /** + * Executes the file this object was initialized with + * @param blocking Whether to wait until the process terminates before + returning or not. + * @param args An array of arguments to pass to the process in UTF-16 + * @param count The length of the args array. + */ + void runw(in boolean blocking, [array, size_is(count)] in wstring args, + in unsigned long count); + + /** + * Executes the file this object was initialized with optionally calling + * an observer after the process has finished running. + * @param args An array of arguments to pass to the process in UTF-16 + * @param count The length of the args array. + * @param observer An observer to notify when the process has completed. It + * will receive this process instance as the subject and + * "process-finished" or "process-failed" as the topic. The + * observer will be notified on the main thread. + * @param holdWeak Whether to use a weak reference to hold the observer. + */ + void runwAsync([array, size_is(count)] in wstring args, + in unsigned long count, + [optional] in nsIObserver observer, [optional] in boolean holdWeak); + + /** + * When set to true the process will not open a new window when started and + * will run hidden from the user. This currently affects only the Windows + * platform. + */ + attribute boolean startHidden; + + /** + * When set to true the process will be launched directly without using the + * shell. This currently affects only the Windows platform. + */ + attribute boolean noShell; + + /** + * The process identifier of the currently running process. This will only + * be available after the process has started and may not be available on + * some platforms. + */ + readonly attribute unsigned long pid; + + /** + * The exit value of the process. This is only valid after the process has + * exited. + */ + readonly attribute long exitValue; + + /** + * Returns whether the process is currently running or not. + */ + readonly attribute boolean isRunning; +}; + +%{C++ + +#define NS_PROCESS_CONTRACTID "@mozilla.org/process/util;1" +%} diff --git a/xpcom/threads/nsIRunnable.idl b/xpcom/threads/nsIRunnable.idl new file mode 100644 index 0000000000..bfe9669a9f --- /dev/null +++ b/xpcom/threads/nsIRunnable.idl @@ -0,0 +1,45 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* 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 "nsISupports.idl" + +/** + * Represents a task which can be dispatched to a thread for execution. + */ + +[scriptable, function, uuid(4a2abaf0-6886-11d3-9382-00104ba0fd40)] +interface nsIRunnable : nsISupports +{ + /** + * The function implementing the task to be run. + */ + void run(); +}; + +[scriptable, uuid(e75aa42a-80a9-11e6-afb5-e89d87348e2c)] +interface nsIRunnablePriority : nsISupports +{ + const unsigned long PRIORITY_IDLE = 0; + const unsigned long PRIORITY_DEFERRED_TIMERS = 1; + const unsigned long PRIORITY_LOW = 2; + // INPUT_LOW isn't supposed to be used directly. + // const unsigned long PRIORITY_INPUT_LOW = 3; + const unsigned long PRIORITY_NORMAL = 4; + const unsigned long PRIORITY_MEDIUMHIGH = 5; + const unsigned long PRIORITY_INPUT_HIGH = 6; + const unsigned long PRIORITY_VSYNC = 7; + // INPUT_HIGHEST is InputTaskManager's internal priority + //const unsigned long PRIORITY_INPUT_HIGHEST = 8; + const unsigned long PRIORITY_RENDER_BLOCKING = 9; + const unsigned long PRIORITY_CONTROL = 10; + + readonly attribute unsigned long priority; +}; + +[uuid(3114c36c-a482-4c6e-9523-1dcfc6f605b9)] +interface nsIRunnableIPCMessageType : nsISupports +{ + readonly attribute unsigned long type; +}; diff --git a/xpcom/threads/nsISerialEventTarget.idl b/xpcom/threads/nsISerialEventTarget.idl new file mode 100644 index 0000000000..9cc8ff9066 --- /dev/null +++ b/xpcom/threads/nsISerialEventTarget.idl @@ -0,0 +1,27 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsIEventTarget.idl" + +/** + * A serial event target is an event dispatching interface like + * nsIEventTarget. Runnables dispatched to an nsISerialEventTarget are required + * to execute serially. That is, two different runnables dispatched to the + * target should never be allowed to execute simultaneously. One exception to + * this rule is nested event loops. If a runnable spins a nested event loop, + * causing another runnable dispatched to the target to run, the target may + * still be considered "serial". + * + * Examples: + * - nsIThread is a serial event target. + * - Thread pools are not serial event targets. + * - However, one can "convert" a thread pool into an nsISerialEventTarget + * by putting a TaskQueue in front of it. + */ +[builtinclass, scriptable, rust_sync, uuid(9f982380-24b4-49f3-88f6-45e2952036c7)] +interface nsISerialEventTarget : nsIEventTarget +{ +}; diff --git a/xpcom/threads/nsISupportsPriority.idl b/xpcom/threads/nsISupportsPriority.idl new file mode 100644 index 0000000000..d0b8b9a3dd --- /dev/null +++ b/xpcom/threads/nsISupportsPriority.idl @@ -0,0 +1,45 @@ +/* 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 "nsISupports.idl" + +/** + * This interface exposes the general notion of a scheduled object with a + * integral priority value. Following UNIX conventions, smaller (and possibly + * negative) values have higher priority. + * + * This interface does not strictly define what happens when the priority of an + * object is changed. An implementation of this interface is free to define + * the side-effects of changing the priority of an object. In some cases, + * changing the priority of an object may be disallowed (resulting in an + * exception being thrown) or may simply be ignored. + */ +[scriptable, uuid(aa578b44-abd5-4c19-8b14-36d4de6fdc36)] +interface nsISupportsPriority : nsISupports +{ + /** + * Typical priority values. + */ + const long PRIORITY_HIGHEST = -20; + const long PRIORITY_HIGH = -10; + const long PRIORITY_NORMAL = 0; + const long PRIORITY_LOW = 10; + const long PRIORITY_LOWEST = 20; + + /** + * This attribute may be modified to change the priority of this object. The + * implementation of this interface is free to truncate a given priority + * value to whatever limits are appropriate. Typically, this attribute is + * initialized to PRIORITY_NORMAL, but implementations may choose to assign a + * different initial value. + */ + attribute long priority; + + /** + * This method adjusts the priority attribute by a given delta. It helps + * reduce the amount of coding required to increment or decrement the value + * of the priority attribute. + */ + void adjustPriority(in long delta); +}; diff --git a/xpcom/threads/nsITargetShutdownTask.h b/xpcom/threads/nsITargetShutdownTask.h new file mode 100644 index 0000000000..09ac3c5e5f --- /dev/null +++ b/xpcom/threads/nsITargetShutdownTask.h @@ -0,0 +1,37 @@ +/* -*- 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 XPCOM_THREADS_NSITARGETSHUTDOWNTASK_H_ +#define XPCOM_THREADS_NSITARGETSHUTDOWNTASK_H_ + +#include "nsISupports.h" +#include "nsIEventTarget.h" +#include "nsThreadUtils.h" + +#define NS_ITARGETSHUTDOWNTASK_IID \ + { \ + 0xb08647aa, 0xcfb5, 0x4630, { \ + 0x8e, 0x26, 0x9a, 0xbe, 0xb3, 0x3f, 0x08, 0x40 \ + } \ + } + +class NS_NO_VTABLE nsITargetShutdownTask : public nsISupports { + public: + NS_DECLARE_STATIC_IID_ACCESSOR(NS_ISHUTDOWNTASK_IID) + + virtual void TargetShutdown() = 0; + + already_AddRefed AsRunnable() { + // FIXME: Try QI to nsINamed if available? + return mozilla::NewRunnableMethod("nsITargetShutdownTask::TargetShutdown", + this, + &nsITargetShutdownTask::TargetShutdown); + } +}; + +NS_DEFINE_STATIC_IID_ACCESSOR(nsITargetShutdownTask, NS_ITARGETSHUTDOWNTASK_IID) + +#endif diff --git a/xpcom/threads/nsIThread.idl b/xpcom/threads/nsIThread.idl new file mode 100644 index 0000000000..3ba764ac12 --- /dev/null +++ b/xpcom/threads/nsIThread.idl @@ -0,0 +1,222 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsISerialEventTarget.idl" +#include "nsIThreadShutdown.idl" + +%{C++ +#include "mozilla/AlreadyAddRefed.h" + +namespace mozilla { +class TimeStamp; +class TimeDurationValueCalculator; +template class BaseTimeDuration; +typedef BaseTimeDuration TimeDuration; +enum class EventQueuePriority; +} +%} + +[ptr] native PRThread(PRThread); +native EventQueuePriority(mozilla::EventQueuePriority); + +native nsIEventTargetPtr(nsIEventTarget*); +native nsISerialEventTargetPtr(nsISerialEventTarget*); +native TimeStamp(mozilla::TimeStamp); +native TimeDuration(mozilla::TimeDuration); + +/** + * This interface provides a high-level abstraction for an operating system + * thread. + * + * Threads have a built-in event queue, and a thread is an event target that + * can receive nsIRunnable objects (events) to be processed on the thread. + * + * See nsIThreadManager for the API used to create and locate threads. + */ +[builtinclass, scriptable, rust_sync, uuid(5801d193-29d1-4964-a6b7-70eb697ddf2b)] +interface nsIThread : nsISerialEventTarget +{ + /** + * @returns + * The NSPR thread object corresponding to this nsIThread. + */ + [noscript] readonly attribute PRThread PRThread; + + /** + * @returns + * Whether or not this thread may call into JS. Used in the profiler + * to avoid some unnecessary locking. + */ + [noscript] attribute boolean CanInvokeJS; + + /** + * Thread QoS priorities. Currently only supported on MacOS. + */ + + cenum QoSPriority : 32 { + QOS_PRIORITY_NORMAL, + QOS_PRIORITY_LOW + }; + + /** + * Shutdown the thread. This method prevents further dispatch of events to + * the thread, and it causes any pending events to run to completion before + * the thread joins (see PR_JoinThread) with the current thread. During this + * method call, events for the current thread may be processed. + * + * This method MAY NOT be executed from the thread itself. Instead, it is + * meant to be executed from another thread (usually the thread that created + * this thread or the main application thread). When this function returns, + * the thread will be shutdown, and it will no longer be possible to dispatch + * events to the thread. + * + * @throws NS_ERROR_UNEXPECTED + * Indicates that this method was erroneously called when this thread was + * the current thread, that this thread was not created with a call to + * nsIThreadManager::NewThread, or if this method was called more than once + * on the thread object. + */ + void shutdown(); + + /** + * This method may be called to determine if there are any events ready to be + * processed. It may only be called when this thread is the current thread. + * + * Because events may be added to this thread by another thread, a "false" + * result does not mean that this thread has no pending events. It only + * means that there were no pending events when this method was called. + * + * @returns + * A boolean value that if "true" indicates that this thread has one or + * more pending events. + * + * @throws NS_ERROR_UNEXPECTED + * Indicates that this method was erroneously called when this thread was + * not the current thread. + */ + boolean hasPendingEvents(); + + /** + * Similar to above, but checks only possible high priority queue. + */ + boolean hasPendingHighPriorityEvents(); + + /** + * Process the next event. If there are no pending events, then this method + * may wait -- depending on the value of the mayWait parameter -- until an + * event is dispatched to this thread. This method is re-entrant but may + * only be called if this thread is the current thread. + * + * @param mayWait + * A boolean parameter that if "true" indicates that the method may block + * the calling thread to wait for a pending event. + * + * @returns + * A boolean value that if "true" indicates that an event was processed. + * + * @throws NS_ERROR_UNEXPECTED + * Indicates that this method was erroneously called when this thread was + * not the current thread. + */ + boolean processNextEvent(in boolean mayWait); + + /** + * Shutdown the thread asynchronously. This method immediately prevents + * further dispatch of events to the thread, and it causes any pending events + * to run to completion before this thread joins with the current thread. + * + * UNLIKE shutdown() this does not process events on the current thread. + * Instead it merely ensures that the current thread continues running until + * this thread has shut down. + * + * This method MAY NOT be executed from the thread itself. Instead, it is + * meant to be executed from another thread (usually the thread that created + * this thread or the main application thread). When this function returns, + * the thread will continue running until it exhausts its event queue. + * + * @throws NS_ERROR_UNEXPECTED + * Indicates that this method was erroneously called when this thread was + * the current thread, that this thread was not created with a call to + * nsIThreadManager::NewNamedThread, or that this method was called more + * than once on the thread object. + */ + void asyncShutdown(); + + /** + * Like `asyncShutdown`, but also returns a nsIThreadShutdown instance to + * allow observing and controlling the thread's async shutdown progress. + */ + nsIThreadShutdown beginShutdown(); + + /** + * Dispatch an event to a specified queue for the thread. This function + * may be called from any thread, and it may be called re-entrantly. + * Most users should use the NS_Dispatch*() functions in nsThreadUtils instead + * of calling this directly. + * + * @param event + * The alreadyAddRefed<> event to dispatch. + * NOTE that the event will be leaked if it fails to dispatch. + * @param queue + * Which event priority queue this should be added to + * + * @throws NS_ERROR_INVALID_ARG + * Indicates that event is null. + * @throws NS_ERROR_UNEXPECTED + * Indicates that the thread is shutting down and has finished processing + * events, so this event would never run and has not been dispatched. + */ + [noscript] void dispatchToQueue(in alreadyAddRefed_nsIRunnable event, + in EventQueuePriority queue); + + /** + * This is set to the end of the last 50+ms event that was executed on + * this thread (for MainThread only). Otherwise returns a null TimeStamp. + */ + [noscript] readonly attribute TimeStamp lastLongTaskEnd; + [noscript] readonly attribute TimeStamp lastLongNonIdleTaskEnd; + + /** + * Get information on the timing of the currently-running event. + * + * @param delay + * The amount of time the current running event in the specified queue waited + * to run. Will return TimeDuration() if the queue is empty or has not run any + * new events since event delay monitoring started. NOTE: delay will be + * TimeDuration() if this thread uses a PrioritizedEventQueue (i.e. MainThread) + * and the event priority is below Input. + * @param start + * The time the currently running event began to run, or TimeStamp() if no + * event is running. + */ + [noscript] void getRunningEventDelay(out TimeDuration delay, out TimeStamp start); + + /** + * Set information on the timing of the currently-running event. + * Overrides the values returned by getRunningEventDelay + * + * @param delay + * Delay the running event spent in queues, or TimeDuration() if + * there's no running event. + * @param start + * The time the currently running event began to run, or TimeStamp() if no + * event is running. + */ + [noscript] void setRunningEventDelay(in TimeDuration delay, in TimeStamp start); + + [noscript] void setNameForWakeupTelemetry(in ACString name); + + /** + * Set the QoS priority of threads where this may be available. Currently + * restricted to MacOS. Must be on the thread to call this method. + * + * @param aPriority + * The specified priority we will adjust to. Can be low (background) or + * normal (default / user-interactive) + */ + [noscript] void setThreadQoS(in nsIThread_QoSPriority aPriority); + +}; diff --git a/xpcom/threads/nsIThreadInternal.idl b/xpcom/threads/nsIThreadInternal.idl new file mode 100644 index 0000000000..ecc0f540f1 --- /dev/null +++ b/xpcom/threads/nsIThreadInternal.idl @@ -0,0 +1,110 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsIThread.idl" + +interface nsIRunnable; +interface nsIThreadObserver; + +/** + * The XPCOM thread object implements this interface, which allows a consumer + * to observe dispatch activity on the thread. + */ +[builtinclass, scriptable, rust_sync, uuid(a3a72e5f-71d9-4add-8f30-59a78fb6d5eb)] +interface nsIThreadInternal : nsIThread +{ + /** + * Get/set the current thread observer (may be null). This attribute may be + * read from any thread, but must only be set on the thread corresponding to + * this thread object. The observer will be released on the thread + * corresponding to this thread object after all other events have been + * processed during a call to Shutdown. + */ + attribute nsIThreadObserver observer; + + /** + * Add an observer that will *only* receive onProcessNextEvent, + * beforeProcessNextEvent. and afterProcessNextEvent callbacks. Always called + * on the target thread, and the implementation does not have to be + * threadsafe. Order of callbacks is not guaranteed (i.e. + * afterProcessNextEvent may be called first depending on whether or not the + * observer is added in a nested loop). Holds a strong ref. + */ + void addObserver(in nsIThreadObserver observer); + + /** + * Remove an observer added via the addObserver call. Once removed the + * observer will never be called again by the thread. + */ + void removeObserver(in nsIThreadObserver observer); +}; + +/** + * This interface provides the observer with hooks to implement a layered + * event queue. For example, it is possible to overlay processing events + * for a GUI toolkit on top of the events for a thread: + * + * var NativeQueue; + * Observer = { + * onDispatchedEvent() { + * NativeQueue.signal(); + * } + * onProcessNextEvent(thread, mayWait) { + * if (NativeQueue.hasNextEvent()) + * NativeQueue.processNextEvent(); + * while (mayWait && !thread.hasPendingEvent()) { + * NativeQueue.wait(); + * NativeQueue.processNextEvent(); + * } + * } + * }; + * + * NOTE: The implementation of this interface must be threadsafe. + * + * NOTE: It is valid to change the thread's observer during a call to an + * observer method. + * + * NOTE: Will be split into two interfaces soon: one for onProcessNextEvent and + * afterProcessNextEvent, then another that inherits the first and adds + * onDispatchedEvent. + */ +[uuid(cc8da053-1776-44c2-9199-b5a629d0a19d)] +interface nsIThreadObserver : nsISupports +{ + /** + * This method is called after an event has been dispatched to the thread. + * This method may be called from any thread. + */ + void onDispatchedEvent(); + + /** + * This method is called when nsIThread::ProcessNextEvent is called. It does + * not guarantee that an event is actually going to be processed. This method + * is only called on the target thread. + * + * @param thread + * The thread being asked to process another event. + * @param mayWait + * Indicates whether or not the method is allowed to block the calling + * thread. For example, this parameter is false during thread shutdown. + */ + void onProcessNextEvent(in nsIThreadInternal thread, in boolean mayWait); + + /** + * This method is called (from nsIThread::ProcessNextEvent) after an event + * is processed. It does not guarantee that an event was actually processed + * (depends on the value of |eventWasProcessed|. This method is only called + * on the target thread. DO NOT EVER RUN SCRIPT FROM THIS CALLBACK!!! + * + * @param thread + * The thread that processed another event. + * @param eventWasProcessed + * Indicates whether an event was actually processed. May be false if the + * |mayWait| flag was false when calling nsIThread::ProcessNextEvent(). + */ + void afterProcessNextEvent(in nsIThreadInternal thread, + in bool eventWasProcessed); +}; diff --git a/xpcom/threads/nsIThreadManager.idl b/xpcom/threads/nsIThreadManager.idl new file mode 100644 index 0000000000..879ec05e3a --- /dev/null +++ b/xpcom/threads/nsIThreadManager.idl @@ -0,0 +1,182 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsISupports.idl" + +[ptr] native PRThread(PRThread); + native ThreadCreationOptions(nsIThreadManager::ThreadCreationOptions); + +interface nsIEventTarget; +interface nsIRunnable; +interface nsIThread; + +%{ C++ +#include "mozilla/Maybe.h" +%} + +[scriptable, function, uuid(039a227d-0cb7-44a5-a8f9-dbb7071979f2)] +interface nsINestedEventLoopCondition : nsISupports +{ + /** + * Returns true if the current nested event loop should stop spinning. + */ + bool isDone(); +}; + +/** + * An interface for creating and locating nsIThread instances. + */ +[scriptable, uuid(1be89eca-e2f7-453b-8d38-c11ba247f6f3)] +interface nsIThreadManager : nsISupports +{ + /** + * Default number of bytes reserved for a thread's stack, if no stack size + * is specified in newThread(). + * + * Defaults can be a little overzealous for many platforms. + * + * On Linux and OS X, for instance, the default thread stack size is whatever + * getrlimit(RLIMIT_STACK) returns, which is often set at 8MB. Or, on Linux, + * if the stack size is unlimited, we fall back to 2MB. This causes particular + * problems on Linux, which allocates 2MB huge VM pages, and will often + * immediately allocate them for any stacks which are 2MB or larger. + * + * The default on Windows is 1MB, which is a little more reasonable. But the + * vast majority of our threads don't need anywhere near that much space. + * + * ASan, TSan and non-opt builds, however, often need a bit more, so give + * them the platform default. + */ +%{C++ +#if defined(MOZ_ASAN) || defined(MOZ_TSAN) || !defined(__OPTIMIZE__) + static constexpr uint32_t DEFAULT_STACK_SIZE = 0; +#else + static constexpr uint32_t DEFAULT_STACK_SIZE = 256 * 1024; +#endif + + static const uint32_t kThreadPoolStackSize = DEFAULT_STACK_SIZE; + + struct ThreadCreationOptions { + // The size in bytes to reserve for the thread's stack. A value of `0` means + // to use the platform default. + uint32_t stackSize = nsIThreadManager::DEFAULT_STACK_SIZE; + + // If set to `true`, any attempts to dispatch runnables to this thread + // without `DISPATCH_IGNORE_BLOCK_DISPATCH` will fail. + // + // This is intended to be used for threads which are expected to generally + // only service a single runnable (other than thread lifecycle runnables), + // and perform their own event dispatching internaly, such as thread pool + // threads or the timer thread. + bool blockDispatch = false; + + // (Windows-only) Whether the thread should have a MessageLoop capable of + // processing native UI events. Defaults to false. + bool isUiThread = false; + + // If set, long task markers will be collected for tasks + // longer than longTaskLength ms when profiling is enabled. + // See https://www.w3.org/TR/longtasks + mozilla::Maybe longTaskLength; + }; +%} + + /** + * Create a new thread (a global, user PRThread) with the specified name. + * + * @param name + * The name of the thread. If it is empty the thread will not be named. + * @param options + * Configuration options for the newly created thread. + * + * @returns + * The newly created nsIThread object. + */ + [noscript] nsIThread newNamedThread(in ACString name, in ThreadCreationOptions options); + + /** + * Get the main thread. + */ + readonly attribute nsIThread mainThread; + + /** + * Get the current thread. If the calling thread does not already have a + * nsIThread associated with it, then a new nsIThread will be created and + * associated with the current PRThread. + */ + readonly attribute nsIThread currentThread; + + /** + * This queues a runnable to the main thread. It's a shortcut for JS callers + * to be used instead of + * .mainThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL); + * or + * .currentThread.dispatch(runnable, Ci.nsIEventTarget.DISPATCH_NORMAL); + * C++ callers should instead use NS_DispatchToMainThread. + */ + [optional_argc] + void dispatchToMainThread(in nsIRunnable event, [optional] in uint32_t priority); + + /** + * Similar to dispatchToMainThread, but wraps the event with extra + * runnable that allocates nsAutoMicroTask. + */ + [optional_argc] + void dispatchToMainThreadWithMicroTask(in nsIRunnable event, [optional] in uint32_t priority); + + /** + * This queues a runnable to the main thread's idle queue. + * + * @param event + * The event to dispatch. + * @param timeout + * The time in milliseconds until this event should be moved from the idle + * queue to the regular queue if it hasn't been executed by then. If not + * passed or a zero value is specified, the event will never be moved to + * the regular queue. + */ + void idleDispatchToMainThread(in nsIRunnable event, + [optional] in uint32_t timeout); + + /* + * A helper method to dispatch a task through nsIDirectTaskDispatcher to the + * current thread. + */ + void dispatchDirectTaskToCurrentThread(in nsIRunnable event); + + /** + * Enter a nested event loop on the current thread, waiting on, and + * processing events until condition.isDone() returns true. + * + * If condition.isDone() throws, this function will throw as well. + * + * C++ code should not use this function, instead preferring + * mozilla::SpinEventLoopUntil. + */ + void spinEventLoopUntil(in ACString aVeryGoodReasonToDoThis, in nsINestedEventLoopCondition condition); + + /** + * Similar to the previous method, but the spinning of the event loop + * terminates when the quit application shutting down starts. + * + * C++ code should not use this function, instead preferring + * mozilla::SpinEventLoopUntil. + */ + void spinEventLoopUntilOrQuit(in ACString aVeryGoodReasonToDoThis, in nsINestedEventLoopCondition condition); + + /** + * Spin the current thread's event loop until there are no more pending + * events. This could be done with spinEventLoopUntil, but that would + * require access to the current thread from JavaScript, which we are + * moving away from. + */ + void spinEventLoopUntilEmpty(); + + /** + * Return the EventTarget for the main thread. + */ + readonly attribute nsIEventTarget mainThreadEventTarget; +}; diff --git a/xpcom/threads/nsIThreadPool.idl b/xpcom/threads/nsIThreadPool.idl new file mode 100644 index 0000000000..48b4a465d8 --- /dev/null +++ b/xpcom/threads/nsIThreadPool.idl @@ -0,0 +1,115 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsIEventTarget.idl" +#include "nsIThread.idl" + +[uuid(ef194cab-3f86-4b61-b132-e5e96a79e5d1)] +interface nsIThreadPoolListener : nsISupports +{ + /** + * Called when a new thread is created by the thread pool. The notification + * happens on the newly-created thread. + */ + void onThreadCreated(); + + /** + * Called when a thread is about to be destroyed by the thread pool. The + * notification happens on the thread that is about to be destroyed. + */ + void onThreadShuttingDown(); +}; + +/** + * An interface to a thread pool. A thread pool creates a limited number of + * anonymous (unnamed) worker threads. An event dispatched to the thread pool + * will be run on the next available worker thread. + */ +[rust_sync, uuid(76ce99c9-8e43-489a-9789-f27cc4424965)] +interface nsIThreadPool : nsIEventTarget +{ + /** + * Set the entire pool's QoS priority. If the priority has not changed, do nothing. + * Existing threads will update their QoS priority the next time they become + * active, and newly created threads will set this QoS priority upon creation. + */ + [noscript] void setQoSForThreads(in nsIThread_QoSPriority aPriority); + + /** + * Shutdown the thread pool. This method may not be executed from any thread + * in the thread pool. Instead, it is meant to be executed from another + * thread (usually the thread that created this thread pool). When this + * function returns, the thread pool and all of its threads will be shutdown, + * and it will no longer be possible to dispatch tasks to the thread pool. + * + * As a side effect, events on the current thread will be processed. + */ + void shutdown(); + + /** + * Shutdown the thread pool, but only wait for aTimeoutMs. After the timeout + * expires, any threads that have not shutdown yet are leaked and will not + * block shutdown. + * + * This method should only be used at during shutdown to cleanup threads that + * made blocking calls to code outside our control, and can't be safely + * terminated. We choose to leak them intentionally to avoid a shutdown hang. + */ + [noscript] void shutdownWithTimeout(in long aTimeoutMs); + + /** + * Get/set the maximum number of threads allowed at one time in this pool. + */ + attribute unsigned long threadLimit; + + /** + * Get/set the maximum number of idle threads kept alive. + */ + attribute unsigned long idleThreadLimit; + + /** + * Get/set the amount of time in milliseconds before an idle thread is + * destroyed. + */ + attribute unsigned long idleThreadTimeout; + + /** + * If set to true the idle timeout will be calculated as idleThreadTimeout + * divideded by the number of idle threads at the moment. This may help + * save memory allocations but still keep reasonable amount of idle threads. + * Default is false, use |idleThreadTimeout| for all threads. + */ + attribute boolean idleThreadTimeoutRegressive; + + /** + * Get/set the number of bytes reserved for the stack of all threads in + * the pool. By default this is nsIThreadManager::DEFAULT_STACK_SIZE. + */ + attribute unsigned long threadStackSize; + + /** + * An optional listener that will be notified when a thread is created or + * destroyed in the course of the thread pool's operation. + * + * A listener will only receive notifications about threads created after the + * listener is set so it is recommended that the consumer set the listener + * before dispatching the first event. A listener that receives an + * onThreadCreated() notification is guaranteed to always receive the + * corresponding onThreadShuttingDown() notification. + * + * The thread pool takes ownership of the listener and releases it when the + * shutdown() method is called. Threads created after the listener is set will + * also take ownership of the listener so that the listener will be kept alive + * long enough to receive the guaranteed onThreadShuttingDown() notification. + */ + attribute nsIThreadPoolListener listener; + + /** + * Set the label for threads in the pool. All threads will be named + * " #", where is a serial number. + */ + void setName(in ACString aName); +}; diff --git a/xpcom/threads/nsIThreadShutdown.idl b/xpcom/threads/nsIThreadShutdown.idl new file mode 100644 index 0000000000..a08d64165b --- /dev/null +++ b/xpcom/threads/nsIThreadShutdown.idl @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "nsISupports.idl" + +interface nsIRunnable; + +/** + * Handle for the ongoing shutdown progress of a given thread which can be used + * to observe and interrupt async shutdown progress. Methods on this interface + * may generally only be used on the thread which called + * `nsIThread::beginShutdown`. + */ +[scriptable, builtinclass, uuid(70a43748-6130-4ea6-a440-7c74e1b7dd7c)] +interface nsIThreadShutdown : nsISupports +{ + /** + * Register a runnable to be executed when the thread has completed shutdown, + * or shutdown has been cancelled due to `stopWaitingAndLeakThread()`. + * + * If the thread has already completed or cancelled shutdown, the runnable + * may be executed synchronously. + * + * May only be called on the thread which invoked `nsIThread::beginShutdown`. + */ + void onCompletion(in nsIRunnable aEvent); + + /** + * Check if the target thread has completed shutdown. + * + * May only be accessed on the thread which called `nsIThread::beginShutdown`. + */ + [infallible] readonly attribute boolean completed; + + /** + * Give up on waiting for the shutting down thread to exit. Calling this + * method will allow the thread to continue running, no longer block shutdown, + * and the thread will never be joined or have its resources reclaimed. + * + * Completion callbacks attached to this `nsIThreadShutdown` may be executed + * during this call. + * + * This method should NOT be called except in exceptional circumstances during + * shutdown, as it will cause resources for the shutting down thread to be + * leaked. + * + * May only be called on the thread which called `nsIThread::beginShutdown` + * + * @throws NS_ERROR_NOT_AVAILABLE + * Indicates that the target thread has already stopped running and a + * request to be joined is already being dispatched to the waiting thread. + */ + void stopWaitingAndLeakThread(); +}; diff --git a/xpcom/threads/nsITimer.idl b/xpcom/threads/nsITimer.idl new file mode 100644 index 0000000000..5d20c315b4 --- /dev/null +++ b/xpcom/threads/nsITimer.idl @@ -0,0 +1,376 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- + * 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 "nsISupports.idl" +#include "nsINamed.idl" + +interface nsIObserver; +interface nsIEventTarget; + +%{C++ +#include "mozilla/MemoryReporting.h" +#include "mozilla/TimeStamp.h" +#include + +/** + * The signature of the timer callback function passed to initWithFuncCallback. + * This is the function that will get called when the timer expires if the + * timer is initialized via initWithFuncCallback. + * + * @param aTimer the timer which has expired + * @param aClosure opaque parameter passed to initWithFuncCallback + */ +class nsITimer; +typedef void (*nsTimerCallbackFunc) (nsITimer *aTimer, void *aClosure); +%} + +native MallocSizeOf(mozilla::MallocSizeOf); +native nsTimerCallbackFunc(nsTimerCallbackFunc); +[ref] native TimeDuration(mozilla::TimeDuration); + +/** + * The callback interface for timers. + */ +interface nsITimer; + +[function, scriptable, uuid(a796816d-7d47-4348-9ab8-c7aeb3216a7d)] +interface nsITimerCallback : nsISupports +{ + /** + * @param aTimer the timer which has expired + */ + void notify(in nsITimer timer); +}; + +%{C++ +// Two timer deadlines must differ by less than half the PRIntervalTime domain. +#define DELAY_INTERVAL_LIMIT PR_BIT(8 * sizeof(PRIntervalTime) - 1) +%} + +/** + * nsITimer instances must be initialized by calling one of the "init" methods + * documented below. You may also re-initialize (using one of the init() + * methods) an existing instance to avoid the overhead of destroying and + * creating a timer. It is not necessary to cancel the timer in that case. + * + * By default a timer will fire on the thread that created it. Set the .target + * attribute to fire on a different thread. Once you have set a timer's .target + * and called one of its init functions, any further interactions with the timer + * (calling cancel(), changing member fields, etc) should only be done by the + * target thread, or races may occur with bad results like timers firing after + * they've been canceled, and/or not firing after re-initiatization. + */ +[scriptable, builtinclass, uuid(3de4b105-363c-482c-a409-baac83a01bfc)] +interface nsITimer : nsISupports +{ + /* Timer types */ + + /** + * Type of a timer that fires once only. + */ + const short TYPE_ONE_SHOT = 0; + + /** + * After firing, a TYPE_REPEATING_SLACK timer is stopped and not restarted + * until its callback completes. Specified timer period will be at least + * the time between when processing for last firing the callback completes + * and when the next firing occurs. + * + * This is the preferable repeating type for most situations. + */ + const short TYPE_REPEATING_SLACK = 1; + + /** + * TYPE_REPEATING_PRECISE is just a synonym for + * TYPE_REPEATING_PRECISE_CAN_SKIP. They used to be distinct, but the old + * TYPE_REPEATING_PRECISE kind was similar to TYPE_REPEATING_PRECISE_CAN_SKIP + * while also being less useful. So the distinction was removed. + */ + const short TYPE_REPEATING_PRECISE = 2; + + /** + * A TYPE_REPEATING_PRECISE_CAN_SKIP repeating timer aims to have constant + * period between firings. The processing time for each timer callback will + * not influence the timer period. If the callback finishes after the next + * firing(s) should have happened (either because the callback took a long + * time, or the callback was called extremely late), that firing(s) is + * skipped, but the following sequence of firing times will not be altered. + * This timer type guarantees that it will not queue up new events to fire + * the callback until the previous callback event finishes firing. This is + * the only non-slack timer available. + */ + const short TYPE_REPEATING_PRECISE_CAN_SKIP = 3; + + /** + * Same as TYPE_REPEATING_SLACK with the exception that idle events + * won't yield to timers with this type. Use this when you want an + * idle callback to be scheduled to run even though this timer is + * about to fire. + */ + const short TYPE_REPEATING_SLACK_LOW_PRIORITY = 4; + + /** + * Same as TYPE_ONE_SHOT with the exception that idle events won't + * yield to timers with this type. Use this when you want an idle + * callback to be scheduled to run even though this timer is about + * to fire. + */ + const short TYPE_ONE_SHOT_LOW_PRIORITY = 5; + + /** + * Initialize a timer that will fire after the said delay. + * A user must keep a reference to this timer till it is + * is no longer needed or has been cancelled. + * + * @param aObserver the callback object that observes the + * ``timer-callback'' topic with the subject being + * the timer itself when the timer fires: + * + * observe(nsISupports aSubject, => nsITimer + * string aTopic, => ``timer-callback'' + * wstring data => null + * + * @param aDelayInMs delay in milliseconds for timer to fire + * @param aType timer type per TYPE* consts defined above + */ + void init(in nsIObserver aObserver, in unsigned long aDelayInMs, + in unsigned long aType); + + + /** + * Initialize a timer to fire after the given millisecond interval. + * This version takes a callback object. + * + * @param aFunc nsITimerCallback interface to call when timer expires + * @param aDelayInMs The millisecond interval + * @param aType Timer type per TYPE* consts defined above + */ + void initWithCallback(in nsITimerCallback aCallback, + in unsigned long aDelayInMs, + in unsigned long aType); + + /** + * Initialize a timer to fire after the high resolution TimeDuration. + * This version takes a callback object. + * + * @param aFunc nsITimerCallback interface to call when timer expires + * @param aDelay The high resolution interval + * @param aType Timer type per TYPE* consts defined above + */ + [noscript] void initHighResolutionWithCallback(in nsITimerCallback aCallback, + [const] in TimeDuration aDelay, + in unsigned long aType); + + /** + * Cancel the timer. This method works on all types, not just on repeating + * timers -- you might want to cancel a TYPE_ONE_SHOT timer, and even reuse + * it by re-initializing it (to avoid object destruction and creation costs + * by conserving one timer instance). + */ + void cancel(); + + /** + * Like initWithFuncCallback, but also takes a name for the timer; the name + * will be used when timer profiling is enabled via the "TimerFirings" log + * module. + * + * @param aFunc The function to invoke + * @param aClosure An opaque pointer to pass to that function + * @param aDelay The millisecond interval + * @param aType Timer type per TYPE* consts defined above + * @param aName The timer's name + */ + [noscript] void initWithNamedFuncCallback(in nsTimerCallbackFunc aCallback, + in voidPtr aClosure, + in unsigned long aDelay, + in unsigned long aType, + in string aName); + + /** + * Initialize a timer to fire after the high resolution TimeDuration. + * This version takes a named function callback. + * + * @param aFunc The function to invoke + * @param aClosure An opaque pointer to pass to that function + * @param aDelay The high resolution interval + * @param aType Timer type per TYPE* consts defined above + * @param aName The timer's name + */ + [noscript] void initHighResolutionWithNamedFuncCallback( + in nsTimerCallbackFunc aCallback, + in voidPtr aClosure, + [const] in TimeDuration aDelay, + in unsigned long aType, + in string aName); + + /** + * The millisecond delay of the timeout. + * + * NOTE: Re-setting the delay on a one-shot timer that has already fired + * doesn't restart the timer. Call one of the init() methods to restart + * a one-shot timer. + */ + attribute unsigned long delay; + + /** + * The timer type - one of the above TYPE_* constants. + */ + attribute unsigned long type; + + /** + * The opaque pointer pass to initWithFuncCallback. + */ + [noscript] readonly attribute voidPtr closure; + + /** + * The nsITimerCallback object passed to initWithCallback. + */ + readonly attribute nsITimerCallback callback; + + /** + * The nsIEventTarget where the callback will be dispatched. Note that this + * target may only be set before the call to one of the init methods above. + * + * By default the target is the thread that created the timer. + */ + attribute nsIEventTarget target; + + readonly attribute ACString name; + + /** + * The number of microseconds this nsITimer implementation can possibly + * fire early. + */ + [noscript] readonly attribute unsigned long allowedEarlyFiringMicroseconds; + + [notxpcom, nostdcall] size_t sizeOfIncludingThis(in MallocSizeOf aMallocSizeOf); +}; + +%{C++ +#include "nsCOMPtr.h" + +already_AddRefed NS_NewTimer(); + +already_AddRefed NS_NewTimer(nsIEventTarget* aTarget); + +nsresult +NS_NewTimerWithObserver(nsITimer** aTimer, + nsIObserver* aObserver, + uint32_t aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithObserver(nsIObserver* aObserver, + uint32_t aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithCallback(nsITimer** aTimer, + nsITimerCallback* aCallback, + uint32_t aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithCallback(nsITimerCallback* aCallback, + uint32_t aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithCallback(nsITimer** aTimer, + nsITimerCallback* aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithCallback(nsITimerCallback* aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithCallback(nsITimer** aTimer, + std::function&& aCallback, + uint32_t aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithCallback(std::function&& aCallback, + uint32_t aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithCallback(nsITimer** aTimer, + std::function&& aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithCallback(std::function&& aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithFuncCallback(nsITimer** aTimer, + nsTimerCallbackFunc aCallback, + void* aClosure, + uint32_t aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithFuncCallback(nsTimerCallbackFunc aCallback, + void* aClosure, + uint32_t aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); + +nsresult +NS_NewTimerWithFuncCallback(nsITimer** aTimer, + nsTimerCallbackFunc aCallback, + void* aClosure, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); +mozilla::Result, nsresult> +NS_NewTimerWithFuncCallback(nsTimerCallbackFunc aCallback, + void* aClosure, + const mozilla::TimeDuration& aDelay, + uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget = nullptr); + +#define NS_TIMER_CALLBACK_TOPIC "timer-callback" + +#ifndef RELEASE_OR_BETA +#undef NS_DECL_NSITIMERCALLBACK +#define NS_DECL_NSITIMERCALLBACK \ + NS_IMETHOD Notify(nsITimer *timer) override; \ + inline void _ensure_GetName_exists(void) { \ + static_assert(std::is_convertible::value, \ + "nsITimerCallback implementations must also implement nsINamed"); \ + } +#endif +%} + +[scriptable, builtinclass, uuid(5482506d-1d21-4d08-b01c-95c87e1295ad)] +interface nsITimerManager : nsISupports +{ + /** + * Returns a read-only list of nsITimer objects, implementing only the name, + * delay and type attribute getters. + * This is meant to be used for tests, to verify that no timer is leftover + * at the end of a test. */ + Array getTimers(); +}; diff --git a/xpcom/threads/nsMemoryPressure.cpp b/xpcom/threads/nsMemoryPressure.cpp new file mode 100644 index 0000000000..dbd3a92f79 --- /dev/null +++ b/xpcom/threads/nsMemoryPressure.cpp @@ -0,0 +1,104 @@ +/* -*- 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 "nsMemoryPressure.h" +#include "mozilla/Assertions.h" +#include "mozilla/Atomics.h" +#include "mozilla/Services.h" + +#include "nsThreadUtils.h" +#include "nsIObserverService.h" + +using namespace mozilla; + +const char* const kTopicMemoryPressure = "memory-pressure"; +const char* const kTopicMemoryPressureStop = "memory-pressure-stop"; +const char16_t* const kSubTopicLowMemoryNew = u"low-memory"; +const char16_t* const kSubTopicLowMemoryOngoing = u"low-memory-ongoing"; + +// This is accessed from any thread through NS_NotifyOfEventualMemoryPressure +static Atomic sMemoryPressurePending( + MemoryPressureState::NoPressure); + +void NS_NotifyOfEventualMemoryPressure(MemoryPressureState aState) { + MOZ_ASSERT(aState != MemoryPressureState::None); + + /* + * A new memory pressure event erases an ongoing (or stop of) memory pressure, + * but an existing "new" memory pressure event takes precedence over a new + * "ongoing" or "stop" memory pressure event. + */ + switch (aState) { + case MemoryPressureState::None: + case MemoryPressureState::LowMemory: + sMemoryPressurePending = aState; + break; + case MemoryPressureState::NoPressure: + sMemoryPressurePending.compareExchange(MemoryPressureState::None, aState); + break; + } +} + +nsresult NS_NotifyOfMemoryPressure(MemoryPressureState aState) { + NS_NotifyOfEventualMemoryPressure(aState); + nsCOMPtr event = + new Runnable("NS_DispatchEventualMemoryPressure"); + return NS_DispatchToMainThread(event); +} + +void NS_DispatchMemoryPressure() { + MOZ_ASSERT(NS_IsMainThread()); + static MemoryPressureState sMemoryPressureStatus = + MemoryPressureState::NoPressure; + + MemoryPressureState mpPending = + sMemoryPressurePending.exchange(MemoryPressureState::None); + if (mpPending == MemoryPressureState::None) { + return; + } + + nsCOMPtr os = services::GetObserverService(); + if (!os) { + NS_WARNING("Can't get observer service!"); + return; + } + + switch (mpPending) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("Already handled this case above."); + break; + case MemoryPressureState::LowMemory: + switch (sMemoryPressureStatus) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("The internal status should never be None."); + break; + case MemoryPressureState::NoPressure: + sMemoryPressureStatus = MemoryPressureState::LowMemory; + os->NotifyObservers(nullptr, kTopicMemoryPressure, + kSubTopicLowMemoryNew); + break; + case MemoryPressureState::LowMemory: + os->NotifyObservers(nullptr, kTopicMemoryPressure, + kSubTopicLowMemoryOngoing); + break; + } + break; + case MemoryPressureState::NoPressure: + switch (sMemoryPressureStatus) { + case MemoryPressureState::None: + MOZ_ASSERT_UNREACHABLE("The internal status should never be None."); + break; + case MemoryPressureState::NoPressure: + // Already no pressure. Do nothing. + break; + case MemoryPressureState::LowMemory: + sMemoryPressureStatus = MemoryPressureState::NoPressure; + os->NotifyObservers(nullptr, kTopicMemoryPressureStop, nullptr); + break; + } + break; + } +} diff --git a/xpcom/threads/nsMemoryPressure.h b/xpcom/threads/nsMemoryPressure.h new file mode 100644 index 0000000000..5a68b0bce5 --- /dev/null +++ b/xpcom/threads/nsMemoryPressure.h @@ -0,0 +1,77 @@ +/* -*- 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 nsMemoryPressure_h__ +#define nsMemoryPressure_h__ + +#include "nscore.h" + +/* + * These pre-defined strings are the topic to pass to the observer + * service to declare the memory-pressure or lift the memory-pressure. + * + * 1. Notify kTopicMemoryPressure with kSubTopicLowMemoryNew + * New memory pressure deteced + * On a new memory pressure, we stop everything to start cleaning + * aggresively the memory used, in order to free as much memory as + * possible. + * + * 2. Notify kTopicMemoryPressure with kSubTopicLowMemoryOngoing + * Repeated memory pressure. + * A repeated memory pressure implies to clean softly recent allocations. + * It is supposed to happen after a new memory pressure which already + * cleaned aggressivley. So there is no need to damage the reactivity of + * Gecko by stopping the world again. + * + * In case of conflict with an new memory pressue, the new memory pressure + * takes precedence over an ongoing memory pressure. The reason being + * that if no events are processed between 2 notifications (new followed + * by ongoing, or ongoing followed by a new) we want to be as aggresive as + * possible on the clean-up of the memory. After all, we are trying to + * keep Gecko alive as long as possible. + * + * 3. Notify kTopicMemoryPressureStop with nullptr + * Memory pressure stopped. + * We're no longer under acute memory pressure, so we might want to have a + * chance of (cautiously) re-enabling some things we previously turned off. + * As above, an already enqueued new memory pressure event takes precedence. + * The priority ordering between concurrent attempts to queue both stopped + * and ongoing memory pressure is currently not defined. + */ +extern const char* const kTopicMemoryPressure; +extern const char* const kTopicMemoryPressureStop; +extern const char16_t* const kSubTopicLowMemoryNew; +extern const char16_t* const kSubTopicLowMemoryOngoing; + +enum class MemoryPressureState : uint32_t { + None, // For internal use. Don't use this value. + LowMemory, + NoPressure, +}; + +/** + * This function causes the main thread to fire a memory pressure event + * before processing the next event, but if there are no events pending in + * the main thread's event queue, the memory pressure event would not be + * dispatched until one is enqueued. It is infallible and does not allocate + * any memory. + * + * You may call this function from any thread. + */ +void NS_NotifyOfEventualMemoryPressure(MemoryPressureState aState); + +/** + * This function causes the main thread to fire a memory pressure event + * before processing the next event. We wake up the main thread by adding a + * dummy event to its event loop, so, unlike with + * NS_NotifyOfEventualMemoryPressure, this memory-pressure event is always + * fired relatively quickly, even if the event loop is otherwise empty. + * + * You may call this function from any thread. + */ +nsresult NS_NotifyOfMemoryPressure(MemoryPressureState aState); + +#endif // nsMemoryPressure_h__ diff --git a/xpcom/threads/nsProcess.h b/xpcom/threads/nsProcess.h new file mode 100644 index 0000000000..95d6748640 --- /dev/null +++ b/xpcom/threads/nsProcess.h @@ -0,0 +1,82 @@ +/* -*- 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 _nsPROCESSWIN_H_ +#define _nsPROCESSWIN_H_ + +#if defined(XP_WIN) +# define PROCESSMODEL_WINAPI +#endif + +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "nsIProcess.h" +#include "nsIObserver.h" +#include "nsMaybeWeakPtr.h" +#include "nsString.h" +#ifndef XP_UNIX +# include "prproces.h" +#endif +#if defined(PROCESSMODEL_WINAPI) +# include +# include +#endif + +#define NS_PROCESS_CID \ + { \ + 0x7b4eeb20, 0xd781, 0x11d4, { \ + 0x8A, 0x83, 0x00, 0x10, 0xa4, 0xe0, 0xc9, 0xca \ + } \ + } + +class nsIFile; + +class nsProcess final : public nsIProcess, public nsIObserver { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIPROCESS + NS_DECL_NSIOBSERVER + + nsProcess(); + + private: + ~nsProcess(); + PRThread* CreateMonitorThread(); + static void Monitor(void* aArg); + void ProcessComplete(); + nsresult CopyArgsAndRunProcess(bool aBlocking, const char** aArgs, + uint32_t aCount, nsIObserver* aObserver, + bool aHoldWeak); + nsresult CopyArgsAndRunProcessw(bool aBlocking, const char16_t** aArgs, + uint32_t aCount, nsIObserver* aObserver, + bool aHoldWeak); + // The 'args' array is null-terminated. + nsresult RunProcess(bool aBlocking, char** aArgs, nsIObserver* aObserver, + bool aHoldWeak, bool aArgsUTF8); + + PRThread* mThread; + mozilla::Mutex mLock; + bool mShutdown MOZ_GUARDED_BY(mLock); + bool mBlocking; + bool mStartHidden; + bool mNoShell; + + nsCOMPtr mExecutable; + nsString mTargetPath; + int32_t mPid; + nsMaybeWeakPtr mObserver; + + // These members are modified by multiple threads, any accesses should be + // protected with mLock. + int32_t mExitValue MOZ_GUARDED_BY(mLock); +#if defined(PROCESSMODEL_WINAPI) + HANDLE mProcess MOZ_GUARDED_BY(mLock); +#elif !defined(XP_UNIX) + PRProcess* mProcess MOZ_GUARDED_BY(mLock); +#endif +}; + +#endif diff --git a/xpcom/threads/nsProcessCommon.cpp b/xpcom/threads/nsProcessCommon.cpp new file mode 100644 index 0000000000..dbd9993f54 --- /dev/null +++ b/xpcom/threads/nsProcessCommon.cpp @@ -0,0 +1,600 @@ +/* -*- 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/. */ + +/***************************************************************************** + * + * nsProcess is used to execute new processes and specify if you want to + * wait (blocking) or continue (non-blocking). + * + ***************************************************************************** + */ + +#include "mozilla/ArrayUtils.h" + +#include "nsCOMPtr.h" +#include "nsIFile.h" +#include "nsProcess.h" +#include "prio.h" +#include "prenv.h" +#include "nsCRT.h" +#include "nsThreadUtils.h" +#include "nsIObserverService.h" +#include "nsXULAppAPI.h" +#include "mozilla/Services.h" + +#include + +#if defined(PROCESSMODEL_WINAPI) +# include "nsString.h" +# include "nsLiteralString.h" +# include "nsReadableUtils.h" +# include "mozilla/AssembleCmdLine.h" +# include "mozilla/UniquePtrExtensions.h" +#else +# ifdef XP_MACOSX +# include +# include +# endif +# ifdef XP_UNIX +# ifndef XP_MACOSX +# include "base/process_util.h" +# endif +# include +# include +# endif +# include +# include +#endif + +using namespace mozilla; + +//-------------------------------------------------------------------// +// nsIProcess implementation +//-------------------------------------------------------------------// +NS_IMPL_ISUPPORTS(nsProcess, nsIProcess, nsIObserver) + +// Constructor +nsProcess::nsProcess() + : mThread(nullptr), + mLock("nsProcess.mLock"), + mShutdown(false), + mBlocking(false), + mStartHidden(false), + mNoShell(false), + mPid(-1), + mExitValue(-1) +#if !defined(XP_UNIX) + , + mProcess(nullptr) +#endif +{ +} + +// Destructor +nsProcess::~nsProcess() = default; + +NS_IMETHODIMP +nsProcess::Init(nsIFile* aExecutable) { + if (mExecutable) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + if (NS_WARN_IF(!aExecutable)) { + return NS_ERROR_INVALID_ARG; + } + bool isFile; + + // First make sure the file exists + nsresult rv = aExecutable->IsFile(&isFile); + if (NS_FAILED(rv)) { + return rv; + } + if (!isFile) { + return NS_ERROR_FAILURE; + } + + // Store the nsIFile in mExecutable + mExecutable = aExecutable; + // Get the path because it is needed by the NSPR process creation +#ifdef XP_WIN + rv = mExecutable->GetTarget(mTargetPath); + if (NS_FAILED(rv) || mTargetPath.IsEmpty()) +#endif + rv = mExecutable->GetPath(mTargetPath); + + return rv; +} + +void nsProcess::Monitor(void* aArg) { + RefPtr process = dont_AddRef(static_cast(aArg)); + + if (!process->mBlocking) { + NS_SetCurrentThreadName("RunProcess"); + } + +#if defined(PROCESSMODEL_WINAPI) + HANDLE processHandle; + { + // The mutex region cannot include WaitForSingleObject otherwise we'll + // block calls such as Kill. So lock on access and store a local. + MutexAutoLock lock(process->mLock); + processHandle = process->mProcess; + } + + DWORD dwRetVal; + unsigned long exitCode = -1; + + dwRetVal = WaitForSingleObject(processHandle, INFINITE); + if (dwRetVal != WAIT_FAILED) { + if (GetExitCodeProcess(processHandle, &exitCode) == FALSE) { + exitCode = -1; + } + } + + // Lock in case Kill or GetExitCode are called during this. + { + MutexAutoLock lock(process->mLock); + CloseHandle(process->mProcess); + process->mProcess = nullptr; + process->mExitValue = exitCode; + if (process->mShutdown) { + return; + } + } +#else +# ifdef XP_UNIX + int exitCode = -1; + int status = 0; + pid_t result; + do { + result = waitpid(process->mPid, &status, 0); + } while (result == -1 && errno == EINTR); + if (result == process->mPid) { + if (WIFEXITED(status)) { + exitCode = WEXITSTATUS(status); + } else if (WIFSIGNALED(status)) { + exitCode = 256; // match NSPR's signal exit status + } + } +# else + int32_t exitCode = -1; + PRProcess* prProcess; + { + // The mutex region cannot include PR_WaitProcess otherwise we'll + // block calls such as Kill. So lock on access and store a local. + MutexAutoLock lock(process->mLock); + prProcess = process->mProcess; + } + if (PR_WaitProcess(prProcess, &exitCode) != PR_SUCCESS) { + exitCode = -1; + } +# endif + + // Lock in case Kill or GetExitCode are called during this + { + MutexAutoLock lock(process->mLock); +# if !defined(XP_UNIX) + process->mProcess = nullptr; +# endif + process->mExitValue = exitCode; + if (process->mShutdown) { + return; + } + } +#endif + + // If we ran a background thread for the monitor then notify on the main + // thread + if (NS_IsMainThread()) { + process->ProcessComplete(); + } else { + NS_DispatchToMainThread(NewRunnableMethod( + "nsProcess::ProcessComplete", process, &nsProcess::ProcessComplete)); + } +} + +void nsProcess::ProcessComplete() { + if (mThread) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(this, "xpcom-shutdown"); + } + PR_JoinThread(mThread); + mThread = nullptr; + } + + const char* topic; + { + MutexAutoLock lock(mLock); + if (mExitValue != 0) { + topic = "process-failed"; + } else { + topic = "process-finished"; + } + } + + mPid = -1; + nsCOMPtr observer = mObserver.GetValue(); + mObserver = nullptr; + + if (observer) { + observer->Observe(NS_ISUPPORTS_CAST(nsIProcess*, this), topic, nullptr); + } +} + +// XXXldb |aArgs| has the wrong const-ness +NS_IMETHODIMP +nsProcess::Run(bool aBlocking, const char** aArgs, uint32_t aCount) { + return CopyArgsAndRunProcess(aBlocking, aArgs, aCount, nullptr, false); +} + +// XXXldb |aArgs| has the wrong const-ness +NS_IMETHODIMP +nsProcess::RunAsync(const char** aArgs, uint32_t aCount, nsIObserver* aObserver, + bool aHoldWeak) { + return CopyArgsAndRunProcess(false, aArgs, aCount, aObserver, aHoldWeak); +} + +nsresult nsProcess::CopyArgsAndRunProcess(bool aBlocking, const char** aArgs, + uint32_t aCount, + nsIObserver* aObserver, + bool aHoldWeak) { + // Add one to the aCount for the program name and one for null termination. + char** my_argv = nullptr; + my_argv = (char**)moz_xmalloc(sizeof(char*) * (aCount + 2)); + + my_argv[0] = ToNewUTF8String(mTargetPath); + + for (uint32_t i = 0; i < aCount; ++i) { + my_argv[i + 1] = const_cast(aArgs[i]); + } + + my_argv[aCount + 1] = nullptr; + + nsresult rv = RunProcess(aBlocking, my_argv, aObserver, aHoldWeak, false); + + free(my_argv[0]); + free(my_argv); + return rv; +} + +// XXXldb |aArgs| has the wrong const-ness +NS_IMETHODIMP +nsProcess::Runw(bool aBlocking, const char16_t** aArgs, uint32_t aCount) { + return CopyArgsAndRunProcessw(aBlocking, aArgs, aCount, nullptr, false); +} + +// XXXldb |aArgs| has the wrong const-ness +NS_IMETHODIMP +nsProcess::RunwAsync(const char16_t** aArgs, uint32_t aCount, + nsIObserver* aObserver, bool aHoldWeak) { + return CopyArgsAndRunProcessw(false, aArgs, aCount, aObserver, aHoldWeak); +} + +nsresult nsProcess::CopyArgsAndRunProcessw(bool aBlocking, + const char16_t** aArgs, + uint32_t aCount, + nsIObserver* aObserver, + bool aHoldWeak) { + // Add one to the aCount for the program name and one for null termination. + char** my_argv = nullptr; + my_argv = (char**)moz_xmalloc(sizeof(char*) * (aCount + 2)); + + my_argv[0] = ToNewUTF8String(mTargetPath); + + for (uint32_t i = 0; i < aCount; i++) { + my_argv[i + 1] = ToNewUTF8String(nsDependentString(aArgs[i])); + } + + my_argv[aCount + 1] = nullptr; + + nsresult rv = RunProcess(aBlocking, my_argv, aObserver, aHoldWeak, true); + + for (uint32_t i = 0; i <= aCount; ++i) { + free(my_argv[i]); + } + free(my_argv); + return rv; +} + +nsresult nsProcess::RunProcess(bool aBlocking, char** aMyArgv, + nsIObserver* aObserver, bool aHoldWeak, + bool aArgsUTF8) { + NS_WARNING_ASSERTION(!XRE_IsContentProcess(), + "No launching of new processes in the content process"); + + if (NS_WARN_IF(!mExecutable)) { + return NS_ERROR_NOT_INITIALIZED; + } + if (NS_WARN_IF(mThread)) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + if (aObserver) { + if (aHoldWeak) { + nsresult rv = NS_OK; + mObserver = do_GetWeakReference(aObserver, &rv); + NS_ENSURE_SUCCESS(rv, rv); + } else { + mObserver = aObserver; + } + } + + { + MutexAutoLock lock(mLock); + mExitValue = -1; + mPid = -1; + } + +#if defined(PROCESSMODEL_WINAPI) + BOOL retVal; + UniqueFreePtr cmdLine; + + // |aMyArgv| is null-terminated and always starts with the program path. If + // the second slot is non-null then arguments are being passed. + if (aMyArgv[1] || mNoShell) { + // Pass the executable path as argv[0] to the launched program when calling + // CreateProcess(). + char** argv = mNoShell ? aMyArgv : aMyArgv + 1; + + wchar_t* assembledCmdLine = nullptr; + if (assembleCmdLine(argv, &assembledCmdLine, + aArgsUTF8 ? CP_UTF8 : CP_ACP) == -1) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + cmdLine.reset(assembledCmdLine); + } + + // The program name in aMyArgv[0] is always UTF-8 + NS_ConvertUTF8toUTF16 wideFile(aMyArgv[0]); + + if (mNoShell) { + STARTUPINFO startupInfo; + ZeroMemory(&startupInfo, sizeof(startupInfo)); + startupInfo.cb = sizeof(startupInfo); + startupInfo.dwFlags = STARTF_USESHOWWINDOW; + startupInfo.wShowWindow = mStartHidden ? SW_HIDE : SW_SHOWNORMAL; + + PROCESS_INFORMATION processInfo; + retVal = CreateProcess(/* lpApplicationName = */ wideFile.get(), + /* lpCommandLine */ cmdLine.get(), + /* lpProcessAttributes = */ NULL, + /* lpThreadAttributes = */ NULL, + /* bInheritHandles = */ FALSE, + /* dwCreationFlags = */ 0, + /* lpEnvironment = */ NULL, + /* lpCurrentDirectory = */ NULL, + /* lpStartupInfo = */ &startupInfo, + /* lpProcessInformation */ &processInfo); + + if (!retVal) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + + CloseHandle(processInfo.hThread); + + // TODO(bug 1763051): assess if we need further work around this locking. + MutexAutoLock lock(mLock); + mProcess = processInfo.hProcess; + } else { + SHELLEXECUTEINFOW sinfo; + memset(&sinfo, 0, sizeof(SHELLEXECUTEINFOW)); + sinfo.cbSize = sizeof(SHELLEXECUTEINFOW); + sinfo.hwnd = nullptr; + sinfo.lpFile = wideFile.get(); + sinfo.nShow = mStartHidden ? SW_HIDE : SW_SHOWNORMAL; + + /* The SEE_MASK_NO_CONSOLE flag is important to prevent console windows + * from appearing. This makes behavior the same on all platforms. The flag + * will not have any effect on non-console applications. + */ + sinfo.fMask = + SEE_MASK_FLAG_DDEWAIT | SEE_MASK_NO_CONSOLE | SEE_MASK_NOCLOSEPROCESS; + + if (cmdLine) { + sinfo.lpParameters = cmdLine.get(); + } + + retVal = ShellExecuteExW(&sinfo); + if (!retVal) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + + MutexAutoLock lock(mLock); + mProcess = sinfo.hProcess; + } + + { + MutexAutoLock lock(mLock); + mPid = GetProcessId(mProcess); + } +#elif defined(XP_MACOSX) + // Note: |aMyArgv| is already null-terminated as required by posix_spawnp. + pid_t newPid = 0; + int result = posix_spawnp(&newPid, aMyArgv[0], nullptr, nullptr, aMyArgv, + *_NSGetEnviron()); + mPid = static_cast(newPid); + + if (result != 0) { + return NS_ERROR_FAILURE; + } +#elif defined(XP_UNIX) + base::LaunchOptions options; + std::vector argvVec; + for (char** arg = aMyArgv; *arg != nullptr; ++arg) { + argvVec.push_back(*arg); + } + pid_t newPid; + if (base::LaunchApp(argvVec, std::move(options), &newPid).isOk()) { + static_assert(sizeof(pid_t) <= sizeof(int32_t), + "mPid is large enough to hold a pid"); + mPid = static_cast(newPid); + } else { + return NS_ERROR_FAILURE; + } +#else + { + PRProcess* prProcess = + PR_CreateProcess(aMyArgv[0], aMyArgv, nullptr, nullptr); + if (!prProcess) { + return NS_ERROR_FAILURE; + } + { + MutexAutoLock lock(mLock); + mProcess = prProcess; + } + struct MYProcess { + uint32_t pid; + }; + MYProcess* ptrProc = (MYProcess*)mProcess; + mPid = ptrProc->pid; + } +#endif + + NS_ADDREF_THIS(); + mBlocking = aBlocking; + if (aBlocking) { + Monitor(this); + MutexAutoLock lock(mLock); + if (mExitValue < 0) { + return NS_ERROR_FILE_EXECUTION_FAILED; + } + } else { + mThread = CreateMonitorThread(); + if (!mThread) { + NS_RELEASE_THIS(); + return NS_ERROR_FAILURE; + } + + // It isn't a failure if we just can't watch for shutdown + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->AddObserver(this, "xpcom-shutdown", false); + } + } + + return NS_OK; +} + +// We don't guarantee that monitor threads are joined before Gecko exits, which +// can cause TSAN to complain about thread leaks. We handle this with a TSAN +// suppression, and route thread creation through this helper so that the +// suppression is as narrowly-scoped as possible. +PRThread* nsProcess::CreateMonitorThread() { + return PR_CreateThread(PR_SYSTEM_THREAD, Monitor, this, PR_PRIORITY_NORMAL, + PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0); +} + +NS_IMETHODIMP +nsProcess::GetIsRunning(bool* aIsRunning) { + if (mThread) { + *aIsRunning = true; + } else { + *aIsRunning = false; + } + + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::GetStartHidden(bool* aStartHidden) { + *aStartHidden = mStartHidden; + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::SetStartHidden(bool aStartHidden) { + mStartHidden = aStartHidden; + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::GetNoShell(bool* aNoShell) { + *aNoShell = mNoShell; + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::SetNoShell(bool aNoShell) { + mNoShell = aNoShell; + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::GetPid(uint32_t* aPid) { + if (!mThread) { + return NS_ERROR_FAILURE; + } + if (mPid < 0) { + return NS_ERROR_NOT_IMPLEMENTED; + } + *aPid = mPid; + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::Kill() { + if (!mThread) { + return NS_ERROR_FAILURE; + } + + { + MutexAutoLock lock(mLock); +#if defined(PROCESSMODEL_WINAPI) + if (TerminateProcess(mProcess, 0) == 0) { + return NS_ERROR_FAILURE; + } +#elif defined(XP_UNIX) + if (kill(mPid, SIGKILL) != 0) { + return NS_ERROR_FAILURE; + } +#else + if (!mProcess || (PR_KillProcess(mProcess) != PR_SUCCESS)) { + return NS_ERROR_FAILURE; + } +#endif + } + + // We must null out mThread if we want IsRunning to return false immediately + // after this call. + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(this, "xpcom-shutdown"); + } + PR_JoinThread(mThread); + mThread = nullptr; + + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::GetExitValue(int32_t* aExitValue) { + MutexAutoLock lock(mLock); + + *aExitValue = mExitValue; + + return NS_OK; +} + +NS_IMETHODIMP +nsProcess::Observe(nsISupports* aSubject, const char* aTopic, + const char16_t* aData) { + // Shutting down, drop all references + if (mThread) { + nsCOMPtr os = mozilla::services::GetObserverService(); + if (os) { + os->RemoveObserver(this, "xpcom-shutdown"); + } + mThread = nullptr; + } + + mObserver = nullptr; + + MutexAutoLock lock(mLock); + mShutdown = true; + + return NS_OK; +} diff --git a/xpcom/threads/nsProxyRelease.cpp b/xpcom/threads/nsProxyRelease.cpp new file mode 100644 index 0000000000..86ed18c8b5 --- /dev/null +++ b/xpcom/threads/nsProxyRelease.cpp @@ -0,0 +1,30 @@ +/* -*- 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 "nsProxyRelease.h" +#include "nsThreadUtils.h" + +namespace detail { + +/* static */ nsresult ProxyReleaseChooser::ProxyReleaseISupports( + const char* aName, nsIEventTarget* aTarget, nsISupports* aDoomed, + bool aAlwaysProxy) { + return ::detail::ProxyRelease( + aName, aTarget, dont_AddRef(aDoomed), aAlwaysProxy); +} + +} // namespace detail + +extern "C" { + +// This function uses C linkage because it's exposed to Rust to support the +// `ThreadPtrHolder` wrapper in the `moz_task` crate. +void NS_ProxyReleaseISupports(const char* aName, nsIEventTarget* aTarget, + nsISupports* aDoomed, bool aAlwaysProxy) { + NS_ProxyRelease(aName, aTarget, dont_AddRef(aDoomed), aAlwaysProxy); +} + +} // extern "C" diff --git a/xpcom/threads/nsProxyRelease.h b/xpcom/threads/nsProxyRelease.h new file mode 100644 index 0000000000..c125eae2aa --- /dev/null +++ b/xpcom/threads/nsProxyRelease.h @@ -0,0 +1,380 @@ +/* -*- 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 nsProxyRelease_h__ +#define nsProxyRelease_h__ + +#include + +#include "MainThreadUtils.h" +#include "mozilla/Likely.h" +#include "mozilla/Unused.h" +#include "nsCOMPtr.h" +#include "nsIEventTarget.h" +#include "nsISerialEventTarget.h" +#include "nsIThread.h" +#include "nsPrintfCString.h" +#include "nsThreadUtils.h" + +#ifdef XPCOM_GLUE_AVOID_NSPR +# error NS_ProxyRelease implementation depends on NSPR. +#endif + +class nsIRunnable; + +namespace detail { + +template +class ProxyReleaseEvent : public mozilla::CancelableRunnable { + public: + ProxyReleaseEvent(const char* aName, already_AddRefed aDoomed) + : CancelableRunnable(aName), mDoomed(aDoomed.take()) {} + + NS_IMETHOD Run() override { + NS_IF_RELEASE(mDoomed); + return NS_OK; + } + + nsresult Cancel() override { return Run(); } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHOD GetName(nsACString& aName) override { + if (mName) { + aName.Append(nsPrintfCString("ProxyReleaseEvent for %s", mName)); + } else { + aName.AssignLiteral("ProxyReleaseEvent"); + } + return NS_OK; + } +#endif + + private: + T* MOZ_OWNING_REF mDoomed; +}; + +template +nsresult ProxyRelease(const char* aName, nsIEventTarget* aTarget, + already_AddRefed aDoomed, bool aAlwaysProxy) { + // Auto-managing release of the pointer. + RefPtr doomed = aDoomed; + nsresult rv; + + if (!doomed || !aTarget) { + return NS_ERROR_INVALID_ARG; + } + + if (!aAlwaysProxy) { + bool onCurrentThread = false; + rv = aTarget->IsOnCurrentThread(&onCurrentThread); + if (NS_SUCCEEDED(rv) && onCurrentThread) { + return NS_OK; + } + } + + nsCOMPtr ev = new ProxyReleaseEvent(aName, doomed.forget()); + + rv = aTarget->Dispatch(ev, NS_DISPATCH_NORMAL); + if (NS_FAILED(rv)) { + NS_WARNING(nsPrintfCString( + "failed to post proxy release event for %s, leaking!", aName) + .get()); + // It is better to leak the aDoomed object than risk crashing as + // a result of deleting it on the wrong thread. + } + return rv; +} + +template +struct ProxyReleaseChooser { + template + static nsresult ProxyRelease(const char* aName, nsIEventTarget* aTarget, + already_AddRefed aDoomed, bool aAlwaysProxy) { + return ::detail::ProxyRelease(aName, aTarget, std::move(aDoomed), + aAlwaysProxy); + } +}; + +template <> +struct ProxyReleaseChooser { + // We need an intermediate step for handling classes with ambiguous + // inheritance to nsISupports. + template + static nsresult ProxyRelease(const char* aName, nsIEventTarget* aTarget, + already_AddRefed aDoomed, bool aAlwaysProxy) { + return ProxyReleaseISupports(aName, aTarget, ToSupports(aDoomed.take()), + aAlwaysProxy); + } + + static nsresult ProxyReleaseISupports(const char* aName, + nsIEventTarget* aTarget, + nsISupports* aDoomed, + bool aAlwaysProxy); +}; + +} // namespace detail + +/** + * Ensures that the delete of a smart pointer occurs on the target thread. + * Note: The doomed object will be leaked if dispatch to the target thread + * fails, as releasing it on the current thread may be unsafe + * + * @param aName + * the labelling name of the runnable involved in the releasing. + * @param aTarget + * the target thread where the doomed object should be released. + * @param aDoomed + * the doomed object; the object to be released on the target thread. + * @param aAlwaysProxy + * normally, if NS_ProxyRelease is called on the target thread, then the + * doomed object will be released directly. However, if this parameter is + * true, then an event will always be posted to the target thread for + * asynchronous release. + * @return result of the task which is dispatched to delete the smart pointer + * on the target thread. + * Note: The caller should not attempt to recover from an + * error code returned by trying to perform the final ->Release() + * manually. + */ +template +inline NS_HIDDEN_(nsresult) + NS_ProxyRelease(const char* aName, nsIEventTarget* aTarget, + already_AddRefed aDoomed, bool aAlwaysProxy = false) { + return ::detail::ProxyReleaseChooser< + std::is_base_of::value>::ProxyRelease(aName, aTarget, + std::move(aDoomed), + aAlwaysProxy); +} + +/** + * Ensures that the delete of a smart pointer occurs on the main thread. + * + * @param aName + * the labelling name of the runnable involved in the releasing + * @param aDoomed + * the doomed object; the object to be released on the main thread. + * @param aAlwaysProxy + * normally, if NS_ReleaseOnMainThread is called on the main + * thread, then the doomed object will be released directly. However, if + * this parameter is true, then an event will always be posted to the + * main thread for asynchronous release. + */ +template +inline NS_HIDDEN_(void) + NS_ReleaseOnMainThread(const char* aName, already_AddRefed aDoomed, + bool aAlwaysProxy = false) { + RefPtr doomed = aDoomed; + if (!doomed) { + return; // Nothing to do. + } + + // NS_ProxyRelease treats a null event target as "the current thread". So a + // handle on the main thread is only necessary when we're not already on the + // main thread or the release must happen asynchronously. + nsCOMPtr target; + if (!NS_IsMainThread() || aAlwaysProxy) { + target = mozilla::GetMainThreadSerialEventTarget(); + + if (!target) { + MOZ_ASSERT_UNREACHABLE("Could not get main thread; leaking an object!"); + mozilla::Unused << doomed.forget().take(); + return; + } + } + + NS_ProxyRelease(aName, target, doomed.forget(), aAlwaysProxy); +} + +/** + * Class to safely handle main-thread-only pointers off the main thread. + * + * Classes like XPCWrappedJS are main-thread-only, which means that it is + * forbidden to call methods on instances of these classes off the main thread. + * For various reasons (see bug 771074), this restriction applies to + * AddRef/Release as well. + * + * This presents a problem for consumers that wish to hold a callback alive + * on non-main-thread code. A common example of this is the proxy callback + * pattern, where non-main-thread code holds a strong-reference to the callback + * object, and dispatches new Runnables (also with a strong reference) to the + * main thread in order to execute the callback. This involves several AddRef + * and Release calls on the other thread, which is verboten. + * + * The basic idea of this class is to introduce a layer of indirection. + * nsMainThreadPtrHolder is a threadsafe reference-counted class that internally + * maintains one strong reference to the main-thread-only object. It must be + * instantiated on the main thread (so that the AddRef of the underlying object + * happens on the main thread), but consumers may subsequently pass references + * to the holder anywhere they please. These references are meant to be opaque + * when accessed off-main-thread (assertions enforce this). + * + * The semantics of RefPtr> would be cumbersome, so we + * also introduce nsMainThreadPtrHandle, which is conceptually identical to + * the above (though it includes various convenience methods). The basic pattern + * is as follows. + * + * // On the main thread: + * nsCOMPtr callback = ...; + * nsMainThreadPtrHandle callbackHandle = + * new nsMainThreadPtrHolder(callback); + * // Pass callbackHandle to structs/classes that might be accessed on other + * // threads. + * + * All structs and classes that might be accessed on other threads should store + * an nsMainThreadPtrHandle rather than an nsCOMPtr. + */ +template +class MOZ_IS_SMARTPTR_TO_REFCOUNTED nsMainThreadPtrHolder final { + public: + // We can only acquire a pointer on the main thread. We want to fail fast for + // threading bugs, so by default we assert if our pointer is used or acquired + // off-main-thread. But some consumers need to use the same pointer for + // multiple classes, some of which are main-thread-only and some of which + // aren't. So we allow them to explicitly disable this strict checking. + nsMainThreadPtrHolder(const char* aName, T* aPtr, bool aStrict = true) + : mRawPtr(aPtr), + mStrict(aStrict) +#ifndef RELEASE_OR_BETA + , + mName(aName) +#endif + { + // We can only AddRef our pointer on the main thread, which means that the + // holder must be constructed on the main thread. + MOZ_ASSERT(!mStrict || NS_IsMainThread()); + NS_IF_ADDREF(mRawPtr); + } + nsMainThreadPtrHolder(const char* aName, already_AddRefed aPtr, + bool aStrict = true) + : mRawPtr(aPtr.take()), + mStrict(aStrict) +#ifndef RELEASE_OR_BETA + , + mName(aName) +#endif + { + // Since we don't need to AddRef the pointer, this constructor is safe to + // call on any thread. + } + + // Copy constructor and operator= deleted. Once constructed, the holder is + // immutable. + T& operator=(nsMainThreadPtrHolder& aOther) = delete; + nsMainThreadPtrHolder(const nsMainThreadPtrHolder& aOther) = delete; + + private: + // We can be released on any thread. + ~nsMainThreadPtrHolder() { + if (NS_IsMainThread()) { + NS_IF_RELEASE(mRawPtr); + } else if (mRawPtr) { + NS_ReleaseOnMainThread( +#ifdef RELEASE_OR_BETA + nullptr, +#else + mName, +#endif + dont_AddRef(mRawPtr)); + } + } + + public: + T* get() const { + // Nobody should be touching the raw pointer off-main-thread. + if (mStrict && MOZ_UNLIKELY(!NS_IsMainThread())) { + NS_ERROR("Can't dereference nsMainThreadPtrHolder off main thread"); + MOZ_CRASH(); + } + return mRawPtr; + } + + bool operator==(const nsMainThreadPtrHolder& aOther) const { + return mRawPtr == aOther.mRawPtr; + } + bool operator!() const { return !mRawPtr; } + + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsMainThreadPtrHolder) + + private: + // Our wrapped pointer. + T* mRawPtr = nullptr; + + // Whether to strictly enforce thread invariants in this class. + bool mStrict = true; + +#ifndef RELEASE_OR_BETA + const char* mName = nullptr; +#endif +}; + +template +class MOZ_IS_SMARTPTR_TO_REFCOUNTED nsMainThreadPtrHandle { + public: + nsMainThreadPtrHandle() : mPtr(nullptr) {} + MOZ_IMPLICIT nsMainThreadPtrHandle(decltype(nullptr)) : mPtr(nullptr) {} + explicit nsMainThreadPtrHandle(nsMainThreadPtrHolder* aHolder) + : mPtr(aHolder) {} + explicit nsMainThreadPtrHandle( + already_AddRefed> aHolder) + : mPtr(aHolder) {} + nsMainThreadPtrHandle(const nsMainThreadPtrHandle& aOther) = default; + nsMainThreadPtrHandle(nsMainThreadPtrHandle&& aOther) = default; + nsMainThreadPtrHandle& operator=(const nsMainThreadPtrHandle& aOther) = + default; + nsMainThreadPtrHandle& operator=(nsMainThreadPtrHandle&& aOther) = default; + nsMainThreadPtrHandle& operator=(nsMainThreadPtrHolder* aHolder) { + mPtr = aHolder; + return *this; + } + + // These all call through to nsMainThreadPtrHolder, and thus implicitly + // assert that we're on the main thread (if strict). Off-main-thread consumers + // must treat these handles as opaque. + T* get() const { + if (mPtr) { + return mPtr.get()->get(); + } + return nullptr; + } + + operator T*() const { return get(); } + T* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { return get(); } + + // These are safe to call on other threads with appropriate external locking. + bool operator==(const nsMainThreadPtrHandle& aOther) const { + if (!mPtr || !aOther.mPtr) { + return mPtr == aOther.mPtr; + } + return *mPtr == *aOther.mPtr; + } + bool operator!=(const nsMainThreadPtrHandle& aOther) const { + return !operator==(aOther); + } + bool operator==(decltype(nullptr)) const { return mPtr == nullptr; } + bool operator!=(decltype(nullptr)) const { return mPtr != nullptr; } + bool operator!() const { return !mPtr || !*mPtr; } + + private: + RefPtr> mPtr; +}; + +class nsCycleCollectionTraversalCallback; +template +void CycleCollectionNoteChild(nsCycleCollectionTraversalCallback& aCallback, + T* aChild, const char* aName, uint32_t aFlags); + +template +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, + nsMainThreadPtrHandle& aField, const char* aName, uint32_t aFlags = 0) { + CycleCollectionNoteChild(aCallback, aField.get(), aName, aFlags); +} + +template +inline void ImplCycleCollectionUnlink(nsMainThreadPtrHandle& aField) { + aField = nullptr; +} + +#endif diff --git a/xpcom/threads/nsThread.cpp b/xpcom/threads/nsThread.cpp new file mode 100644 index 0000000000..98542e9438 --- /dev/null +++ b/xpcom/threads/nsThread.cpp @@ -0,0 +1,1560 @@ +/* -*- 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 "nsThread.h" + +#include "base/message_loop.h" +#include "base/platform_thread.h" + +// Chromium's logging can sometimes leak through... +#ifdef LOG +# undef LOG +#endif + +#include "mozilla/ReentrantMonitor.h" +#include "nsMemoryPressure.h" +#include "nsThreadManager.h" +#include "nsIClassInfoImpl.h" +#include "nsCOMPtr.h" +#include "nsQueryObject.h" +#include "pratom.h" +#include "mozilla/BackgroundHangMonitor.h" +#include "mozilla/CycleCollectedJSContext.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/Logging.h" +#include "nsIObserverService.h" +#include "mozilla/IOInterposer.h" +#include "mozilla/ipc/MessageChannel.h" +#include "mozilla/ipc/BackgroundChild.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerRunnable.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/Services.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticLocalPtr.h" +#include "mozilla/StaticPrefs_threads.h" +#include "mozilla/TaskController.h" +#include "nsXPCOMPrivate.h" +#include "mozilla/ChaosMode.h" +#include "mozilla/Telemetry.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/DocGroup.h" +#include "mozilla/dom/ScriptSettings.h" +#include "nsThreadSyncDispatch.h" +#include "nsServiceManagerUtils.h" +#include "GeckoProfiler.h" +#include "ThreadEventQueue.h" +#include "ThreadEventTarget.h" +#include "ThreadDelay.h" + +#include + +#ifdef XP_LINUX +# ifdef __GLIBC__ +# include +# endif +# include +# include +# include +# include +# include +#endif + +#ifdef XP_WIN +# include "mozilla/DynamicallyLinkedFunctionPtr.h" + +# include + +using GetCurrentThreadStackLimitsFn = void(WINAPI*)(PULONG_PTR LowLimit, + PULONG_PTR HighLimit); +#endif + +#define HAVE_UALARM \ + _BSD_SOURCE || \ + (_XOPEN_SOURCE >= 500 || _XOPEN_SOURCE && _XOPEN_SOURCE_EXTENDED) && \ + !(_POSIX_C_SOURCE >= 200809L || _XOPEN_SOURCE >= 700) + +#if defined(XP_LINUX) && !defined(ANDROID) && defined(_GNU_SOURCE) +# define HAVE_SCHED_SETAFFINITY +#endif + +#ifdef XP_MACOSX +# include +# include +# include +#endif + +#ifdef MOZ_CANARY +# include +# include +# include +# include +# include "nsXULAppAPI.h" +#endif + +using namespace mozilla; + +extern void InitThreadLocalVariables(); + +static LazyLogModule sThreadLog("nsThread"); +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sThreadLog, mozilla::LogLevel::Debug, args) + +NS_DECL_CI_INTERFACE_GETTER(nsThread) + +Array nsThread::sMainThreadRunnableName; + +#ifdef EARLY_BETA_OR_EARLIER +const uint32_t kTelemetryWakeupCountLimit = 100; +#endif + +//----------------------------------------------------------------------------- +// Because we do not have our own nsIFactory, we have to implement nsIClassInfo +// somewhat manually. + +class nsThreadClassInfo : public nsIClassInfo { + public: + NS_DECL_ISUPPORTS_INHERITED // no mRefCnt + NS_DECL_NSICLASSINFO + + nsThreadClassInfo() = default; +}; + +NS_IMETHODIMP_(MozExternalRefCountType) +nsThreadClassInfo::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) +nsThreadClassInfo::Release() { return 1; } +NS_IMPL_QUERY_INTERFACE(nsThreadClassInfo, nsIClassInfo) + +NS_IMETHODIMP +nsThreadClassInfo::GetInterfaces(nsTArray& aArray) { + return NS_CI_INTERFACE_GETTER_NAME(nsThread)(aArray); +} + +NS_IMETHODIMP +nsThreadClassInfo::GetScriptableHelper(nsIXPCScriptable** aResult) { + *aResult = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadClassInfo::GetContractID(nsACString& aResult) { + aResult.SetIsVoid(true); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadClassInfo::GetClassDescription(nsACString& aResult) { + aResult.SetIsVoid(true); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadClassInfo::GetClassID(nsCID** aResult) { + *aResult = nullptr; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadClassInfo::GetFlags(uint32_t* aResult) { + *aResult = THREADSAFE; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadClassInfo::GetClassIDNoAlloc(nsCID* aResult) { + return NS_ERROR_NOT_AVAILABLE; +} + +//----------------------------------------------------------------------------- + +NS_IMPL_ADDREF(nsThread) +NS_IMPL_RELEASE(nsThread) +NS_INTERFACE_MAP_BEGIN(nsThread) + NS_INTERFACE_MAP_ENTRY(nsIThread) + NS_INTERFACE_MAP_ENTRY(nsIThreadInternal) + NS_INTERFACE_MAP_ENTRY(nsIEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISerialEventTarget) + NS_INTERFACE_MAP_ENTRY(nsISupportsPriority) + NS_INTERFACE_MAP_ENTRY(nsIDirectTaskDispatcher) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIThread) + if (aIID.Equals(NS_GET_IID(nsIClassInfo))) { + static nsThreadClassInfo sThreadClassInfo; + foundInterface = static_cast(&sThreadClassInfo); + } else +NS_INTERFACE_MAP_END +NS_IMPL_CI_INTERFACE_GETTER(nsThread, nsIThread, nsIThreadInternal, + nsIEventTarget, nsISerialEventTarget, + nsISupportsPriority) + +//----------------------------------------------------------------------------- + +// This event is responsible for notifying nsThread::Shutdown that it is time +// to call PR_JoinThread. It implements nsICancelableRunnable so that it can +// run on a DOM Worker thread (where all events must implement +// nsICancelableRunnable.) +class nsThreadShutdownAckEvent : public CancelableRunnable { + public: + explicit nsThreadShutdownAckEvent(NotNull aCtx) + : CancelableRunnable("nsThreadShutdownAckEvent"), + mShutdownContext(aCtx) {} + NS_IMETHOD Run() override { + mShutdownContext->mTerminatingThread->ShutdownComplete(mShutdownContext); + return NS_OK; + } + nsresult Cancel() override { return Run(); } + + private: + virtual ~nsThreadShutdownAckEvent() = default; + + NotNull> mShutdownContext; +}; + +// This event is responsible for setting mShutdownContext +class nsThreadShutdownEvent : public Runnable { + public: + nsThreadShutdownEvent(NotNull aThr, + NotNull aCtx) + : Runnable("nsThreadShutdownEvent"), + mThread(aThr), + mShutdownContext(aCtx) {} + NS_IMETHOD Run() override { + // Creates a cycle between `mThread` and the shutdown context which will be + // broken when the thread exits. + mThread->mShutdownContext = mShutdownContext; + MessageLoop::current()->Quit(); +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // Let's leave a trace that we passed here in the thread's name. + nsAutoCString threadName(PR_GetThreadName(PR_GetCurrentThread())); + threadName.Append(",SHDRCV"_ns); + NS_SetCurrentThreadName(threadName.get()); +#endif + return NS_OK; + } + + private: + NotNull> mThread; + NotNull> mShutdownContext; +}; + +//----------------------------------------------------------------------------- + +static void SetThreadAffinity(unsigned int cpu) { +#ifdef HAVE_SCHED_SETAFFINITY + cpu_set_t cpus; + CPU_ZERO(&cpus); + CPU_SET(cpu, &cpus); + sched_setaffinity(0, sizeof(cpus), &cpus); + // Don't assert sched_setaffinity's return value because it intermittently (?) + // fails with EINVAL on Linux x64 try runs. +#elif defined(XP_MACOSX) + // OS X does not provide APIs to pin threads to specific processors, but you + // can tag threads as belonging to the same "affinity set" and the OS will try + // to run them on the same processor. To run threads on different processors, + // tag them as belonging to different affinity sets. Tag 0, the default, means + // "no affinity" so let's pretend each CPU has its own tag `cpu+1`. + thread_affinity_policy_data_t policy; + policy.affinity_tag = cpu + 1; + kern_return_t kr = thread_policy_set( + mach_thread_self(), THREAD_AFFINITY_POLICY, &policy.affinity_tag, 1); + // Setting the thread affinity is not supported on ARM. + MOZ_ALWAYS_TRUE(kr == KERN_SUCCESS || kr == KERN_NOT_SUPPORTED); +#elif defined(XP_WIN) + MOZ_ALWAYS_TRUE(SetThreadIdealProcessor(GetCurrentThread(), cpu) != + (DWORD)-1); +#endif +} + +static void SetupCurrentThreadForChaosMode() { + if (!ChaosMode::isActive(ChaosFeature::ThreadScheduling)) { + return; + } + +#ifdef XP_LINUX + // PR_SetThreadPriority doesn't really work since priorities > + // PR_PRIORITY_NORMAL can't be set by non-root users. Instead we'll just use + // setpriority(2) to set random 'nice values'. In regular Linux this is only + // a dynamic adjustment so it still doesn't really do what we want, but tools + // like 'rr' can be more aggressive about honoring these values. + // Some of these calls may fail due to trying to lower the priority + // (e.g. something may have already called setpriority() for this thread). + // This makes it hard to have non-main threads with higher priority than the + // main thread, but that's hard to fix. Tools like rr can choose to honor the + // requested values anyway. + // Use just 4 priorities so there's a reasonable chance of any two threads + // having equal priority. + setpriority(PRIO_PROCESS, 0, ChaosMode::randomUint32LessThan(4)); +#else + // We should set the affinity here but NSPR doesn't provide a way to expose + // it. + uint32_t priority = ChaosMode::randomUint32LessThan(PR_PRIORITY_LAST + 1); + PR_SetThreadPriority(PR_GetCurrentThread(), PRThreadPriority(priority)); +#endif + + // Force half the threads to CPU 0 so they compete for CPU + if (ChaosMode::randomUint32LessThan(2)) { + SetThreadAffinity(0); + } +} + +namespace { + +struct ThreadInitData { + RefPtr thread; + nsCString name; +}; + +} // namespace + +void nsThread::MaybeRemoveFromThreadList() { + nsThreadManager& tm = nsThreadManager::get(); + OffTheBooksMutexAutoLock mal(tm.ThreadListMutex()); + if (isInList()) { + removeFrom(tm.ThreadList()); + } +} + +/*static*/ +void nsThread::ThreadFunc(void* aArg) { + using mozilla::ipc::BackgroundChild; + + UniquePtr initData(static_cast(aArg)); + RefPtr& self = initData->thread; + + MOZ_ASSERT(self->mEventTarget); + MOZ_ASSERT(self->mEvents); + + // Note: see the comment in nsThread::Init, where we set these same values. + DebugOnly prev = self->mThread.exchange(PR_GetCurrentThread()); + MOZ_ASSERT(!prev || prev == PR_GetCurrentThread()); + self->mEventTarget->SetCurrentThread(self->mThread); + SetupCurrentThreadForChaosMode(); + + if (!initData->name.IsEmpty()) { + NS_SetCurrentThreadName(initData->name.BeginReading()); + } + + self->InitCommon(); + + // Inform the ThreadManager + nsThreadManager::get().RegisterCurrentThread(*self); + + mozilla::IOInterposer::RegisterCurrentThread(); + + // This must come after the call to nsThreadManager::RegisterCurrentThread(), + // because that call is needed to properly set up this thread as an nsThread, + // which profiler_register_thread() requires. See bug 1347007. + const bool registerWithProfiler = !initData->name.IsEmpty(); + if (registerWithProfiler) { + PROFILER_REGISTER_THREAD(initData->name.BeginReading()); + } + + { + // Scope for MessageLoop. + MessageLoop loop( +#if defined(XP_WIN) || defined(XP_MACOSX) + self->mIsUiThread ? MessageLoop::TYPE_MOZILLA_NONMAINUITHREAD + : MessageLoop::TYPE_MOZILLA_NONMAINTHREAD, +#else + MessageLoop::TYPE_MOZILLA_NONMAINTHREAD, +#endif + self); + + // Now, process incoming events... + loop.Run(); + + self->mEvents->RunShutdownTasks(); + + BackgroundChild::CloseForCurrentThread(); + + // NB: The main thread does not shut down here! It shuts down via + // nsThreadManager::Shutdown. + + // Do NS_ProcessPendingEvents but with special handling to set + // mEventsAreDoomed atomically with the removal of the last event. The key + // invariant here is that we will never permit PutEvent to succeed if the + // event would be left in the queue after our final call to + // NS_ProcessPendingEvents. We also have to keep processing events as long + // as we have outstanding mRequestedShutdownContexts. + while (true) { + // Check and see if we're waiting on any threads. + self->WaitForAllAsynchronousShutdowns(); + + if (self->mEvents->ShutdownIfNoPendingEvents()) { + break; + } + NS_ProcessPendingEvents(self); + } + } + + mozilla::IOInterposer::UnregisterCurrentThread(); + + // Inform the threadmanager that this thread is going away + nsThreadManager::get().UnregisterCurrentThread(*self); + + // The thread should only unregister itself if it was registered above. + if (registerWithProfiler) { + PROFILER_UNREGISTER_THREAD(); + } + + NotNull> context = + WrapNotNull(self->mShutdownContext); + self->mShutdownContext = nullptr; + MOZ_ASSERT(context->mTerminatingThread == self); + + // Take the joining thread from our shutdown context. This may have been + // cleared by the joining thread if it decided to cancel waiting on us, in + // which case we won't notify our caller, and leak. + RefPtr joiningThread; + { + MutexAutoLock lock(context->mJoiningThreadMutex); + joiningThread = context->mJoiningThread.forget(); + MOZ_RELEASE_ASSERT(joiningThread || context->mThreadLeaked); + } + if (joiningThread) { + // Dispatch shutdown ACK + nsCOMPtr event = new nsThreadShutdownAckEvent(context); + nsresult dispatch_ack_rv = + joiningThread->Dispatch(event, NS_DISPATCH_NORMAL); + + // We do not expect this to ever happen, but If we cannot dispatch + // the ack event, someone probably blocks waiting on us and will + // crash with a hang later anyways. The best we can do is to tell + // the world what happened right here. + MOZ_RELEASE_ASSERT(NS_SUCCEEDED(dispatch_ack_rv)); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + // Let's leave a trace that we passed here in the thread's name. + nsAutoCString threadName(PR_GetThreadName(PR_GetCurrentThread())); + threadName.Append(",SHDACK"_ns); + NS_SetCurrentThreadName(threadName.get()); +#endif + } else { + NS_WARNING( + "nsThread exiting after StopWaitingAndLeakThread was called, thread " + "resources will be leaked!"); + } + + // Release any observer of the thread here. + self->SetObserver(nullptr); + + // The PRThread will be deleted in PR_JoinThread(), so clear references. + self->mThread = nullptr; + self->mEventTarget->ClearCurrentThread(); +} + +void nsThread::InitCommon() { + mThreadId = uint32_t(PlatformThread::CurrentId()); + + { +#if defined(XP_LINUX) + pthread_attr_t attr; + pthread_attr_init(&attr); + pthread_getattr_np(pthread_self(), &attr); + + size_t stackSize; + pthread_attr_getstack(&attr, &mStackBase, &stackSize); + + // Glibc prior to 2.27 reports the stack size and base including the guard + // region, so we need to compensate for it to get accurate accounting. + // Also, this behavior difference isn't guarded by a versioned symbol, so we + // actually need to check the runtime glibc version, not the version we were + // compiled against. + static bool sAdjustForGuardSize = ({ +# ifdef __GLIBC__ + unsigned major, minor; + sscanf(gnu_get_libc_version(), "%u.%u", &major, &minor) < 2 || + major < 2 || (major == 2 && minor < 27); +# else + false; +# endif + }); + if (sAdjustForGuardSize) { + size_t guardSize; + pthread_attr_getguardsize(&attr, &guardSize); + + // Note: This assumes that the stack grows down, as is the case on all of + // our tier 1 platforms. On platforms where the stack grows up, the + // mStackBase adjustment is unnecessary, but doesn't cause any harm other + // than under-counting stack memory usage by one page. + mStackBase = reinterpret_cast(mStackBase) + guardSize; + stackSize -= guardSize; + } + + mStackSize = stackSize; + + // This is a bit of a hack. + // + // We really do want the NOHUGEPAGE flag on our thread stacks, since we + // don't expect any of them to need anywhere near 2MB of space. But setting + // it here is too late to have an effect, since the first stack page has + // already been faulted in existence, and NSPR doesn't give us a way to set + // it beforehand. + // + // What this does get us, however, is a different set of VM flags on our + // thread stacks compared to normal heap memory. Which makes the Linux + // kernel report them as separate regions, even when they are adjacent to + // heap memory. This allows us to accurately track the actual memory + // consumption of our allocated stacks. + madvise(mStackBase, stackSize, MADV_NOHUGEPAGE); + + pthread_attr_destroy(&attr); +#elif defined(XP_WIN) + static const StaticDynamicallyLinkedFunctionPtr< + GetCurrentThreadStackLimitsFn> + sGetStackLimits(L"kernel32.dll", "GetCurrentThreadStackLimits"); + + if (sGetStackLimits) { + ULONG_PTR stackBottom, stackTop; + sGetStackLimits(&stackBottom, &stackTop); + mStackBase = reinterpret_cast(stackBottom); + mStackSize = stackTop - stackBottom; + } +#endif + } + + InitThreadLocalVariables(); +} + +//----------------------------------------------------------------------------- + +#ifdef MOZ_CANARY +int sCanaryOutputFD = -1; +#endif + +nsThread::nsThread(NotNull aQueue, + MainThreadFlag aMainThread, + nsIThreadManager::ThreadCreationOptions aOptions) + : mEvents(aQueue.get()), + mEventTarget(new ThreadEventTarget( + mEvents.get(), aMainThread == MAIN_THREAD, aOptions.blockDispatch)), + mOutstandingShutdownContexts(0), + mShutdownContext(nullptr), + mScriptObserver(nullptr), + mThreadName(""), + mStackSize(aOptions.stackSize), + mNestedEventLoopDepth(0), + mShutdownRequired(false), + mPriority(PRIORITY_NORMAL), + mIsMainThread(aMainThread == MAIN_THREAD), + mUseHangMonitor(aMainThread == MAIN_THREAD), + mIsUiThread(aOptions.isUiThread), + mIsAPoolThreadFree(nullptr), + mCanInvokeJS(false), +#ifdef EARLY_BETA_OR_EARLIER + mLastWakeupCheckTime(TimeStamp::Now()), +#endif + mPerformanceCounterState(mNestedEventLoopDepth, mIsMainThread, + aOptions.longTaskLength) { +#if !(defined(XP_WIN) || defined(XP_MACOSX)) + MOZ_ASSERT(!mIsUiThread, + "Non-main UI threads are only supported on Windows and macOS"); +#endif + if (mIsMainThread) { + MOZ_ASSERT(!mIsUiThread, + "Setting isUIThread is not supported for main threads"); + mozilla::TaskController::Get()->SetPerformanceCounterState( + &mPerformanceCounterState); + } +} + +nsThread::nsThread() + : mEvents(nullptr), + mEventTarget(nullptr), + mOutstandingShutdownContexts(0), + mShutdownContext(nullptr), + mScriptObserver(nullptr), + mThreadName(""), + mStackSize(0), + mNestedEventLoopDepth(0), + mShutdownRequired(false), + mPriority(PRIORITY_NORMAL), + mIsMainThread(false), + mUseHangMonitor(false), + mIsUiThread(false), + mCanInvokeJS(false), +#ifdef EARLY_BETA_OR_EARLIER + mLastWakeupCheckTime(TimeStamp::Now()), +#endif + mPerformanceCounterState(mNestedEventLoopDepth) { + MOZ_ASSERT(!NS_IsMainThread()); +} + +nsThread::~nsThread() { + NS_ASSERTION(mOutstandingShutdownContexts == 0, + "shouldn't be waiting on other threads to shutdown"); + + MaybeRemoveFromThreadList(); +} + +nsresult nsThread::Init(const nsACString& aName) { + MOZ_ASSERT(mEvents); + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(!mThread); + + SetThreadNameInternal(aName); + + PRThread* thread = nullptr; + + nsThreadManager& tm = nsThreadManager::get(); + { + OffTheBooksMutexAutoLock lock(tm.ThreadListMutex()); + if (!tm.AllowNewXPCOMThreadsLocked()) { + return NS_ERROR_NOT_INITIALIZED; + } + + // We need to fully start the thread while holding the thread list lock, as + // the next acquire of the lock could try to shut down this thread (e.g. + // during xpcom shutdown), which would hang if `PR_CreateThread` failed. + + UniquePtr initData( + new ThreadInitData{this, nsCString(aName)}); + + // ThreadFunc is responsible for setting mThread + if (!(thread = PR_CreateThread(PR_USER_THREAD, ThreadFunc, initData.get(), + PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, + PR_JOINABLE_THREAD, mStackSize))) { + return NS_ERROR_OUT_OF_MEMORY; + } + + // The created thread now owns initData, so release our ownership of it. + Unused << initData.release(); + + // The thread has successfully started, so we can mark it as requiring + // shutdown & add it to the thread list. + mShutdownRequired = true; + tm.ThreadList().insertBack(this); + } + + // Note: we set these both here and inside ThreadFunc, to what should be + // the same value. This is because calls within ThreadFunc need these values + // to be set, and our callers need these values to be set. + DebugOnly prev = mThread.exchange(thread); + MOZ_ASSERT(!prev || prev == thread); + + mEventTarget->SetCurrentThread(thread); + return NS_OK; +} + +nsresult nsThread::InitCurrentThread() { + mThread = PR_GetCurrentThread(); + + nsThreadManager& tm = nsThreadManager::get(); + { + OffTheBooksMutexAutoLock lock(tm.ThreadListMutex()); + // NOTE: We don't check AllowNewXPCOMThreads here, as threads initialized + // this way do not need shutdown, so are OK to create after nsThreadManager + // shutdown. In addition, the main thread is initialized this way, which + // happens before AllowNewXPCOMThreads begins to return true. + tm.ThreadList().insertBack(this); + } + + SetupCurrentThreadForChaosMode(); + InitCommon(); + + tm.RegisterCurrentThread(*this); + return NS_OK; +} + +void nsThread::GetThreadName(nsACString& aNameBuffer) { + auto lock = mThreadName.Lock(); + aNameBuffer = lock.ref(); +} + +void nsThread::SetThreadNameInternal(const nsACString& aName) { + auto lock = mThreadName.Lock(); + lock->Assign(aName); +} + +//----------------------------------------------------------------------------- +// nsIEventTarget + +NS_IMETHODIMP +nsThread::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + MOZ_ASSERT(mEventTarget); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_IMPLEMENTED); + + nsCOMPtr event(aEvent); + return mEventTarget->Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +nsThread::Dispatch(already_AddRefed aEvent, uint32_t aFlags) { + MOZ_ASSERT(mEventTarget); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_IMPLEMENTED); + + LOG(("THRD(%p) Dispatch [%p %x]\n", this, /* XXX aEvent */ nullptr, aFlags)); + + return mEventTarget->Dispatch(std::move(aEvent), aFlags); +} + +NS_IMETHODIMP +nsThread::DelayedDispatch(already_AddRefed aEvent, + uint32_t aDelayMs) { + MOZ_ASSERT(mEventTarget); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_IMPLEMENTED); + + return mEventTarget->DelayedDispatch(std::move(aEvent), aDelayMs); +} + +NS_IMETHODIMP +nsThread::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + MOZ_ASSERT(mEventTarget); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_IMPLEMENTED); + + return mEventTarget->RegisterShutdownTask(aTask); +} + +NS_IMETHODIMP +nsThread::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + MOZ_ASSERT(mEventTarget); + NS_ENSURE_TRUE(mEventTarget, NS_ERROR_NOT_IMPLEMENTED); + + return mEventTarget->UnregisterShutdownTask(aTask); +} + +NS_IMETHODIMP +nsThread::GetRunningEventDelay(TimeDuration* aDelay, TimeStamp* aStart) { + if (mIsAPoolThreadFree && *mIsAPoolThreadFree) { + // if there are unstarted threads in the pool, a new event to the + // pool would not be delayed at all (beyond thread start time) + *aDelay = TimeDuration(); + *aStart = TimeStamp(); + } else { + *aDelay = mLastEventDelay; + *aStart = mLastEventStart; + } + return NS_OK; +} + +NS_IMETHODIMP +nsThread::SetRunningEventDelay(TimeDuration aDelay, TimeStamp aStart) { + mLastEventDelay = aDelay; + mLastEventStart = aStart; + return NS_OK; +} + +NS_IMETHODIMP +nsThread::IsOnCurrentThread(bool* aResult) { + if (mEventTarget) { + return mEventTarget->IsOnCurrentThread(aResult); + } + *aResult = PR_GetCurrentThread() == mThread; + return NS_OK; +} + +NS_IMETHODIMP_(bool) +nsThread::IsOnCurrentThreadInfallible() { + // This method is only going to be called if `mThread` is null, which + // only happens when the thread has exited the event loop. Therefore, when + // we are called, we can never be on this thread. + return false; +} + +//----------------------------------------------------------------------------- +// nsIThread + +NS_IMETHODIMP +nsThread::GetPRThread(PRThread** aResult) { + PRThread* thread = mThread; // atomic load + *aResult = thread; + return thread ? NS_OK : NS_ERROR_NOT_AVAILABLE; +} + +NS_IMETHODIMP +nsThread::GetCanInvokeJS(bool* aResult) { + *aResult = mCanInvokeJS; + return NS_OK; +} + +NS_IMETHODIMP +nsThread::SetCanInvokeJS(bool aCanInvokeJS) { + mCanInvokeJS = aCanInvokeJS; + return NS_OK; +} + +NS_IMETHODIMP +nsThread::GetLastLongTaskEnd(TimeStamp* _retval) { + *_retval = mPerformanceCounterState.LastLongTaskEnd(); + return NS_OK; +} + +NS_IMETHODIMP +nsThread::GetLastLongNonIdleTaskEnd(TimeStamp* _retval) { + *_retval = mPerformanceCounterState.LastLongNonIdleTaskEnd(); + return NS_OK; +} + +NS_IMETHODIMP +nsThread::SetNameForWakeupTelemetry(const nsACString& aName) { +#ifdef EARLY_BETA_OR_EARLIER + mNameForWakeupTelemetry = aName; +#endif + return NS_OK; +} + +NS_IMETHODIMP +nsThread::AsyncShutdown() { + LOG(("THRD(%p) async shutdown\n", this)); + + nsCOMPtr shutdown; + BeginShutdown(getter_AddRefs(shutdown)); + return NS_OK; +} + +NS_IMETHODIMP +nsThread::BeginShutdown(nsIThreadShutdown** aShutdown) { + LOG(("THRD(%p) begin shutdown\n", this)); + + MOZ_ASSERT(mEvents); + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(mThread != PR_GetCurrentThread()); + if (NS_WARN_IF(mThread == PR_GetCurrentThread())) { + return NS_ERROR_UNEXPECTED; + } + + // Prevent multiple calls to this method. + if (!mShutdownRequired.compareExchange(true, false)) { + return NS_ERROR_UNEXPECTED; + } + MOZ_ASSERT(mThread); + + RefPtr currentThread = nsThreadManager::get().GetCurrentThread(); + + MOZ_DIAGNOSTIC_ASSERT(currentThread->EventQueue(), + "Shutdown() may only be called from an XPCOM thread"); + + // Allocate a shutdown context, and record that we're waiting for it. + RefPtr context = + new nsThreadShutdownContext(WrapNotNull(this), currentThread); + + ++currentThread->mOutstandingShutdownContexts; + nsCOMPtr clearOutstanding = NS_NewRunnableFunction( + "nsThread::ClearOutstandingShutdownContext", + [currentThread] { --currentThread->mOutstandingShutdownContexts; }); + context->OnCompletion(clearOutstanding); + + // Set mShutdownContext and wake up the thread in case it is waiting for + // events to process. + nsCOMPtr event = + new nsThreadShutdownEvent(WrapNotNull(this), WrapNotNull(context)); + if (!mEvents->PutEvent(event.forget(), EventQueuePriority::Normal)) { + // We do not expect this to happen. Let's collect some diagnostics. + nsAutoCString threadName; + GetThreadName(threadName); + MOZ_CRASH_UNSAFE_PRINTF("Attempt to shutdown an already dead thread: %s", + threadName.get()); + } + + // We could still end up with other events being added after the shutdown + // task, but that's okay because we process pending events in ThreadFunc + // after setting mShutdownContext just before exiting. + context.forget(aShutdown); + return NS_OK; +} + +void nsThread::ShutdownComplete(NotNull aContext) { + MOZ_ASSERT(mEvents); + MOZ_ASSERT(mEventTarget); + MOZ_ASSERT(aContext->mTerminatingThread == this); + +#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED + { + MutexAutoLock lock(aContext->mJoiningThreadMutex); + + // StopWaitingAndLeakThread is explicitely meant to not cause a + // nsThreadShutdownAckEvent on the joining thread, which is the only + // caller of ShutdownComplete. + MOZ_DIAGNOSTIC_ASSERT(!aContext->mThreadLeaked); + } +#endif + + MaybeRemoveFromThreadList(); + + // Now, it should be safe to join without fear of dead-locking. + PR_JoinThread(aContext->mTerminatingPRThread); + MOZ_ASSERT(!mThread); + +#ifdef DEBUG + nsCOMPtr obs = mEvents->GetObserver(); + MOZ_ASSERT(!obs, "Should have been cleared at shutdown!"); +#endif + + aContext->MarkCompleted(); +} + +void nsThread::WaitForAllAsynchronousShutdowns() { + // This is the motivating example for why SpinEventLoopUntil + // has the template parameter we are providing here. + SpinEventLoopUntil( + "nsThread::WaitForAllAsynchronousShutdowns"_ns, + [&]() { return mOutstandingShutdownContexts == 0; }, this); +} + +NS_IMETHODIMP +nsThread::Shutdown() { + LOG(("THRD(%p) sync shutdown\n", this)); + + nsCOMPtr context; + nsresult rv = BeginShutdown(getter_AddRefs(context)); + if (NS_FAILED(rv)) { + return NS_OK; // The thread has already shut down. + } + + // If we are going to hang here we want to see the thread's name + nsAutoCString threadName; + GetThreadName(threadName); + + // Process events on the current thread until we receive a shutdown ACK. + // Allows waiting; ensure no locks are held that would deadlock us! + SpinEventLoopUntil("nsThread::Shutdown: "_ns + threadName, + [&]() { return context->GetCompleted(); }); + + return NS_OK; +} + +NS_IMETHODIMP +nsThread::HasPendingEvents(bool* aResult) { + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + if (mIsMainThread) { + *aResult = TaskController::Get()->HasMainThreadPendingTasks(); + } else { + *aResult = mEvents->HasPendingEvent(); + } + return NS_OK; +} + +NS_IMETHODIMP +nsThread::HasPendingHighPriorityEvents(bool* aResult) { + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + // This function appears to never be called anymore. + *aResult = false; + return NS_OK; +} + +NS_IMETHODIMP +nsThread::DispatchToQueue(already_AddRefed aEvent, + EventQueuePriority aQueue) { + nsCOMPtr event = aEvent; + + if (NS_WARN_IF(!event)) { + return NS_ERROR_INVALID_ARG; + } + + if (!mEvents->PutEvent(event.forget(), aQueue)) { + NS_WARNING( + "An idle event was posted to a thread that will never run it " + "(rejected)"); + return NS_ERROR_UNEXPECTED; + } + + return NS_OK; +} + +NS_IMETHODIMP nsThread::SetThreadQoS(nsIThread::QoSPriority aPriority) { + if (!StaticPrefs::threads_use_low_power_enabled()) { + return NS_OK; + } + // The approach here is to have a thread set itself for its QoS level, + // so we assert if we aren't on the current thread. + MOZ_ASSERT(IsOnCurrentThread(), "Can only change the current thread's QoS"); + +#if defined(XP_MACOSX) + // Only arm64 macs may possess heterogeneous cores. On these, we can tell + // a thread to set its own QoS status. On intel macs things should behave + // normally, and the OS will ignore the QoS state of the thread. + if (aPriority == nsIThread::QOS_PRIORITY_LOW) { + pthread_set_qos_class_self_np(QOS_CLASS_BACKGROUND, 0); + } else if (NS_IsMainThread()) { + // MacOS documentation specifies that a main thread should be initialized at + // the USER_INTERACTIVE priority, so when we restore thread priorities the + // main thread should be setting itself to this. + pthread_set_qos_class_self_np(QOS_CLASS_USER_INTERACTIVE, 0); + } else { + pthread_set_qos_class_self_np(QOS_CLASS_DEFAULT, 0); + } +#endif + // Do nothing if an OS-specific implementation is unavailable. + return NS_OK; +} + +#ifdef MOZ_CANARY +void canary_alarm_handler(int signum); + +class Canary { + // XXX ToDo: support nested loops + public: + Canary() { + if (sCanaryOutputFD > 0 && EventLatencyIsImportant()) { + signal(SIGALRM, canary_alarm_handler); + ualarm(15000, 0); + } + } + + ~Canary() { + if (sCanaryOutputFD != 0 && EventLatencyIsImportant()) { + ualarm(0, 0); + } + } + + static bool EventLatencyIsImportant() { + return NS_IsMainThread() && XRE_IsParentProcess(); + } +}; + +void canary_alarm_handler(int signum) { + void* array[30]; + const char msg[29] = "event took too long to run:\n"; + // use write to be safe in the signal handler + write(sCanaryOutputFD, msg, sizeof(msg)); + backtrace_symbols_fd(array, backtrace(array, 30), sCanaryOutputFD); +} + +#endif + +#define NOTIFY_EVENT_OBSERVERS(observers_, func_, params_) \ + do { \ + if (!observers_.IsEmpty()) { \ + for (nsCOMPtr obs_ : observers_.ForwardRange()) { \ + obs_->func_ params_; \ + } \ + } \ + } while (0) + +size_t nsThread::ShallowSizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + if (mShutdownContext) { + n += aMallocSizeOf(mShutdownContext); + } + return aMallocSizeOf(this) + aMallocSizeOf(mThread) + n; +} + +size_t nsThread::SizeOfEventQueues(mozilla::MallocSizeOf aMallocSizeOf) const { + size_t n = 0; + if (mEventTarget) { + // The size of mEvents is reported by mEventTarget. + n += mEventTarget->SizeOfIncludingThis(aMallocSizeOf); + } + return n; +} + +size_t nsThread::SizeOfIncludingThis( + mozilla::MallocSizeOf aMallocSizeOf) const { + return ShallowSizeOfIncludingThis(aMallocSizeOf) + + SizeOfEventQueues(aMallocSizeOf); +} + +NS_IMETHODIMP +nsThread::ProcessNextEvent(bool aMayWait, bool* aResult) { + MOZ_ASSERT(mEvents); + NS_ENSURE_TRUE(mEvents, NS_ERROR_NOT_IMPLEMENTED); + + LOG(("THRD(%p) ProcessNextEvent [%u %u]\n", this, aMayWait, + mNestedEventLoopDepth)); + + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + // The toplevel event loop normally blocks waiting for the next event, but + // if we're trying to shut this thread down, we must exit the event loop + // when the event queue is empty. This only applys to the toplevel event + // loop! Nested event loops (e.g. during sync dispatch) are waiting for + // some state change and must be able to block even if something has + // requested shutdown of the thread. Otherwise we'll just busywait as we + // endlessly look for an event, fail to find one, and repeat the nested + // event loop since its state change hasn't happened yet. + bool reallyWait = aMayWait && (mNestedEventLoopDepth > 0 || !ShuttingDown()); + + Maybe noJSAPI; + + if (mUseHangMonitor && reallyWait) { + BackgroundHangMonitor().NotifyWait(); + } + + if (mIsMainThread) { + DoMainThreadSpecificProcessing(); + } + + ++mNestedEventLoopDepth; + + // We only want to create an AutoNoJSAPI on threads that actually do DOM + // stuff (including workers). Those are exactly the threads that have an + // mScriptObserver. + bool callScriptObserver = !!mScriptObserver; + if (callScriptObserver) { + noJSAPI.emplace(); + mScriptObserver->BeforeProcessTask(reallyWait); + } + + DrainDirectTasks(); + +#ifdef EARLY_BETA_OR_EARLIER + // Need to capture mayWaitForWakeup state before OnProcessNextEvent, + // since on the main thread OnProcessNextEvent ends up waiting for the new + // events. + bool mayWaitForWakeup = reallyWait && !mEvents->HasPendingEvent(); +#endif + + nsCOMPtr obs = mEvents->GetObserverOnThread(); + if (obs) { + obs->OnProcessNextEvent(this, reallyWait); + } + + NOTIFY_EVENT_OBSERVERS(EventQueue()->EventObservers(), OnProcessNextEvent, + (this, reallyWait)); + + DrainDirectTasks(); + +#ifdef MOZ_CANARY + Canary canary; +#endif + nsresult rv = NS_OK; + + { + // Scope for |event| to make sure that its destructor fires while + // mNestedEventLoopDepth has been incremented, since that destructor can + // also do work. + nsCOMPtr event; + bool usingTaskController = mIsMainThread; + if (usingTaskController) { + event = TaskController::Get()->GetRunnableForMTTask(reallyWait); + } else { + event = mEvents->GetEvent(reallyWait, &mLastEventDelay); + } + + *aResult = (event.get() != nullptr); + + if (event) { +#ifdef EARLY_BETA_OR_EARLIER + if (mayWaitForWakeup && mThread) { + ++mWakeupCount; + if (mWakeupCount == kTelemetryWakeupCountLimit) { + TimeStamp now = TimeStamp::Now(); + double ms = (now - mLastWakeupCheckTime).ToMilliseconds(); + if (ms < 0) { + ms = 0; + } + const char* name = !mNameForWakeupTelemetry.IsEmpty() + ? mNameForWakeupTelemetry.get() + : PR_GetThreadName(mThread); + if (!name) { + name = mIsMainThread ? "MainThread" : "(nameless thread)"; + } + nsDependentCString key(name); + Telemetry::Accumulate(Telemetry::THREAD_WAKEUP, key, + static_cast(ms)); + mLastWakeupCheckTime = now; + mWakeupCount = 0; + } + } +#endif + + LOG(("THRD(%p) running [%p]\n", this, event.get())); + + Maybe log; + + if (!usingTaskController) { + log.emplace(event); + } + + // Delay event processing to encourage whoever dispatched this event + // to run. + DelayForChaosMode(ChaosFeature::TaskRunning, 1000); + + mozilla::TimeStamp now = mozilla::TimeStamp::Now(); + + if (mUseHangMonitor) { + BackgroundHangMonitor().NotifyActivity(); + } + + Maybe snapshot; + if (!usingTaskController) { + snapshot.emplace(mPerformanceCounterState.RunnableWillRun(now, false)); + } + + mLastEventStart = now; + + if (!usingTaskController) { + AUTO_PROFILE_FOLLOWING_RUNNABLE(event); + event->Run(); + } else { + // Avoid generating "Runnable" profiler markers for the + // "TaskController::ExecutePendingMTTasks" runnables created + // by TaskController, which already adds "Runnable" markers + // when executing tasks. + event->Run(); + } + + if (usingTaskController) { + *aResult = TaskController::Get()->MTTaskRunnableProcessedTask(); + } else { + mPerformanceCounterState.RunnableDidRun(EmptyCString(), + std::move(snapshot.ref())); + } + + // To cover the event's destructor code inside the LogRunnable span. + event = nullptr; + } else { + mLastEventDelay = TimeDuration(); + mLastEventStart = TimeStamp(); + if (aMayWait) { + MOZ_ASSERT(ShuttingDown(), + "This should only happen when shutting down"); + rv = NS_ERROR_UNEXPECTED; + } + } + } + + DrainDirectTasks(); + + NOTIFY_EVENT_OBSERVERS(EventQueue()->EventObservers(), AfterProcessNextEvent, + (this, *aResult)); + + if (obs) { + obs->AfterProcessNextEvent(this, *aResult); + } + + // In case some EventObserver dispatched some direct tasks; process them + // now. + DrainDirectTasks(); + + if (callScriptObserver) { + if (mScriptObserver) { + mScriptObserver->AfterProcessTask(mNestedEventLoopDepth); + } + noJSAPI.reset(); + } + + --mNestedEventLoopDepth; + + return rv; +} + +//----------------------------------------------------------------------------- +// nsISupportsPriority + +NS_IMETHODIMP +nsThread::GetPriority(int32_t* aPriority) { + *aPriority = mPriority; + return NS_OK; +} + +NS_IMETHODIMP +nsThread::SetPriority(int32_t aPriority) { + if (NS_WARN_IF(!mThread)) { + return NS_ERROR_NOT_INITIALIZED; + } + + // NSPR defines the following four thread priorities: + // PR_PRIORITY_LOW + // PR_PRIORITY_NORMAL + // PR_PRIORITY_HIGH + // PR_PRIORITY_URGENT + // We map the priority values defined on nsISupportsPriority to these + // values. + + mPriority = aPriority; + + PRThreadPriority pri; + if (mPriority <= PRIORITY_HIGHEST) { + pri = PR_PRIORITY_URGENT; + } else if (mPriority < PRIORITY_NORMAL) { + pri = PR_PRIORITY_HIGH; + } else if (mPriority > PRIORITY_NORMAL) { + pri = PR_PRIORITY_LOW; + } else { + pri = PR_PRIORITY_NORMAL; + } + // If chaos mode is active, retain the randomly chosen priority + if (!ChaosMode::isActive(ChaosFeature::ThreadScheduling)) { + PR_SetThreadPriority(mThread, pri); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsThread::AdjustPriority(int32_t aDelta) { + return SetPriority(mPriority + aDelta); +} + +//----------------------------------------------------------------------------- +// nsIThreadInternal + +NS_IMETHODIMP +nsThread::GetObserver(nsIThreadObserver** aObs) { + MOZ_ASSERT(mEvents); + NS_ENSURE_TRUE(mEvents, NS_ERROR_NOT_IMPLEMENTED); + + nsCOMPtr obs = mEvents->GetObserver(); + obs.forget(aObs); + return NS_OK; +} + +NS_IMETHODIMP +nsThread::SetObserver(nsIThreadObserver* aObs) { + MOZ_ASSERT(mEvents); + NS_ENSURE_TRUE(mEvents, NS_ERROR_NOT_IMPLEMENTED); + + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + mEvents->SetObserver(aObs); + return NS_OK; +} + +uint32_t nsThread::RecursionDepth() const { + MOZ_ASSERT(PR_GetCurrentThread() == mThread); + return mNestedEventLoopDepth; +} + +NS_IMETHODIMP +nsThread::AddObserver(nsIThreadObserver* aObserver) { + MOZ_ASSERT(mEvents); + NS_ENSURE_TRUE(mEvents, NS_ERROR_NOT_IMPLEMENTED); + + if (NS_WARN_IF(!aObserver)) { + return NS_ERROR_INVALID_ARG; + } + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + EventQueue()->AddObserver(aObserver); + + return NS_OK; +} + +NS_IMETHODIMP +nsThread::RemoveObserver(nsIThreadObserver* aObserver) { + MOZ_ASSERT(mEvents); + NS_ENSURE_TRUE(mEvents, NS_ERROR_NOT_IMPLEMENTED); + + if (NS_WARN_IF(PR_GetCurrentThread() != mThread)) { + return NS_ERROR_NOT_SAME_THREAD; + } + + EventQueue()->RemoveObserver(aObserver); + + return NS_OK; +} + +void nsThread::SetScriptObserver( + mozilla::CycleCollectedJSContext* aScriptObserver) { + if (!aScriptObserver) { + mScriptObserver = nullptr; + return; + } + + MOZ_ASSERT(!mScriptObserver); + mScriptObserver = aScriptObserver; +} + +void NS_DispatchMemoryPressure(); + +void nsThread::DoMainThreadSpecificProcessing() const { + MOZ_ASSERT(mIsMainThread); + + ipc::CancelCPOWs(); + + // Fire a memory pressure notification, if one is pending. + if (!ShuttingDown()) { + NS_DispatchMemoryPressure(); + } +} + +//----------------------------------------------------------------------------- +// nsIDirectTaskDispatcher + +NS_IMETHODIMP +nsThread::DispatchDirectTask(already_AddRefed aEvent) { + if (!IsOnCurrentThread()) { + return NS_ERROR_FAILURE; + } + mDirectTasks.AddTask(std::move(aEvent)); + return NS_OK; +} + +NS_IMETHODIMP nsThread::DrainDirectTasks() { + if (!IsOnCurrentThread()) { + return NS_ERROR_FAILURE; + } + mDirectTasks.DrainTasks(); + return NS_OK; +} + +NS_IMETHODIMP nsThread::HaveDirectTasks(bool* aValue) { + if (!IsOnCurrentThread()) { + return NS_ERROR_FAILURE; + } + + *aValue = mDirectTasks.HaveTasks(); + return NS_OK; +} + +NS_IMPL_ISUPPORTS(nsThreadShutdownContext, nsIThreadShutdown) + +NS_IMETHODIMP +nsThreadShutdownContext::OnCompletion(nsIRunnable* aEvent) { + if (mCompleted) { + aEvent->Run(); + } else { + mCompletionCallbacks.AppendElement(aEvent); + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadShutdownContext::GetCompleted(bool* aCompleted) { + *aCompleted = mCompleted; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadShutdownContext::StopWaitingAndLeakThread() { + // Take the joining thread from `mJoiningThread` so that the terminating + // thread won't try to dispatch nsThreadShutdownAckEvent to us anymore. + RefPtr joiningThread; + { + MutexAutoLock lock(mJoiningThreadMutex); + if (!mJoiningThread) { + // Shutdown is already being resolved, so there's nothing for us to do. + return NS_ERROR_NOT_AVAILABLE; + } + joiningThread = mJoiningThread.forget(); + mThreadLeaked = true; + } + + MOZ_DIAGNOSTIC_ASSERT(joiningThread->IsOnCurrentThread()); + + MarkCompleted(); + + return NS_OK; +} + +void nsThreadShutdownContext::MarkCompleted() { + MOZ_ASSERT(!mCompleted); + mCompleted = true; + nsTArray> callbacks(std::move(mCompletionCallbacks)); + for (auto& callback : callbacks) { + callback->Run(); + } +} + +namespace mozilla { +PerformanceCounterState::Snapshot PerformanceCounterState::RunnableWillRun( + TimeStamp aNow, bool aIsIdleRunnable) { + if (mIsMainThread && IsNestedRunnable()) { + // Flush out any accumulated time that should be accounted to the + // current runnable before we start running a nested runnable. Don't + // do this for non-mainthread threads that may be running their own + // event loops, like SocketThread. + MaybeReportAccumulatedTime("nested runnable"_ns, aNow); + } + + Snapshot snapshot(mCurrentEventLoopDepth, mCurrentRunnableIsIdleRunnable); + + mCurrentEventLoopDepth = mNestedEventLoopDepth; + mCurrentRunnableIsIdleRunnable = aIsIdleRunnable; + mCurrentTimeSliceStart = aNow; + + return snapshot; +} + +void PerformanceCounterState::RunnableDidRun(const nsCString& aName, + Snapshot&& aSnapshot) { + // First thing: Restore our mCurrentEventLoopDepth so we can use + // IsNestedRunnable(). + mCurrentEventLoopDepth = aSnapshot.mOldEventLoopDepth; + + // We may not need the current timestamp; don't bother computing it if we + // don't. + TimeStamp now; + if (mLongTaskLength.isSome() || IsNestedRunnable()) { + now = TimeStamp::Now(); + } + if (mLongTaskLength.isSome()) { + MaybeReportAccumulatedTime(aName, now); + } + + // And now restore the rest of our state. + mCurrentRunnableIsIdleRunnable = aSnapshot.mOldIsIdleRunnable; + if (IsNestedRunnable()) { + // Reset mCurrentTimeSliceStart to right now, so our parent runnable's + // next slice can be properly accounted for. + mCurrentTimeSliceStart = now; + } else { + // We are done at the outermost level; we are no longer in a timeslice. + mCurrentTimeSliceStart = TimeStamp(); + } +} + +void PerformanceCounterState::MaybeReportAccumulatedTime(const nsCString& aName, + TimeStamp aNow) { + MOZ_ASSERT(mCurrentTimeSliceStart, + "How did we get here if we're not in a timeslice?"); + if (!mLongTaskLength.isSome()) { + return; + } + + TimeDuration duration = aNow - mCurrentTimeSliceStart; +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + if (mIsMainThread && duration.ToMilliseconds() > LONGTASK_TELEMETRY_MS) { + Telemetry::Accumulate(Telemetry::EVENT_LONGTASK, aName, + duration.ToMilliseconds()); + } +#endif + + // Long tasks only matter on the main thread. + if (duration.ToMilliseconds() >= mLongTaskLength.value()) { + // Idle events (gc...) don't *really* count here + if (!mCurrentRunnableIsIdleRunnable) { + mLastLongNonIdleTaskEnd = aNow; + } + mLastLongTaskEnd = aNow; + + if (profiler_thread_is_being_profiled_for_markers()) { + struct LongTaskMarker { + static constexpr Span MarkerTypeName() { + return MakeStringSpan("MainThreadLongTask"); + } + static void StreamJSONMarkerData( + baseprofiler::SpliceableJSONWriter& aWriter) { + aWriter.StringProperty("category", "LongTask"); + } + static MarkerSchema MarkerTypeDisplay() { + using MS = MarkerSchema; + MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable}; + schema.AddKeyLabelFormatSearchable("category", "Type", + MS::Format::String, + MS::Searchable::Searchable); + return schema; + } + }; + + profiler_add_marker(mCurrentRunnableIsIdleRunnable + ? ProfilerString8View("LongIdleTask") + : ProfilerString8View("LongTask"), + geckoprofiler::category::OTHER, + MarkerTiming::Interval(mCurrentTimeSliceStart, aNow), + LongTaskMarker{}); + } + } +} + +} // namespace mozilla diff --git a/xpcom/threads/nsThread.h b/xpcom/threads/nsThread.h new file mode 100644 index 0000000000..5b177f0c0c --- /dev/null +++ b/xpcom/threads/nsThread.h @@ -0,0 +1,364 @@ +/* -*- 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 nsThread_h__ +#define nsThread_h__ + +#include "MainThreadUtils.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/Atomics.h" +#include "mozilla/Attributes.h" +#include "mozilla/DataMutex.h" +#include "mozilla/EventQueue.h" +#include "mozilla/LinkedList.h" +#include "mozilla/MemoryReporting.h" +#include "mozilla/Mutex.h" +#include "mozilla/NotNull.h" +#include "mozilla/RefPtr.h" +#include "mozilla/TaskDispatcher.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/UniquePtr.h" +#include "nsIDirectTaskDispatcher.h" +#include "nsIEventTarget.h" +#include "nsISerialEventTarget.h" +#include "nsISupportsPriority.h" +#include "nsIThread.h" +#include "nsIThreadInternal.h" +#include "nsTArray.h" + +namespace mozilla { +class CycleCollectedJSContext; +class DelayedRunnable; +class SynchronizedEventQueue; +class ThreadEventQueue; +class ThreadEventTarget; + +template +class Array; +} // namespace mozilla + +using mozilla::NotNull; + +class nsIRunnable; +class nsThreadShutdownContext; + +// See https://www.w3.org/TR/longtasks +#define W3_LONGTASK_BUSY_WINDOW_MS 50 + +// Time a Runnable executes before we accumulate telemetry on it +#define LONGTASK_TELEMETRY_MS 30 + +// A class for managing performance counter state. +namespace mozilla { +class PerformanceCounterState { + public: + explicit PerformanceCounterState( + const uint32_t& aNestedEventLoopDepthRef, bool aIsMainThread = false, + const Maybe& aLongTaskLength = Nothing()) + : mNestedEventLoopDepth(aNestedEventLoopDepthRef), + mIsMainThread(aIsMainThread), + mLongTaskLength(aLongTaskLength), + // Does it really make sense to initialize these to "now" when we + // haven't run any tasks? + mLastLongTaskEnd(TimeStamp::Now()), + mLastLongNonIdleTaskEnd(mLastLongTaskEnd) {} + + class Snapshot { + public: + Snapshot(uint32_t aOldEventLoopDepth, bool aOldIsIdleRunnable) + : mOldEventLoopDepth(aOldEventLoopDepth), + mOldIsIdleRunnable(aOldIsIdleRunnable) {} + + Snapshot(const Snapshot&) = default; + Snapshot(Snapshot&&) = default; + + private: + friend class PerformanceCounterState; + + const uint32_t mOldEventLoopDepth; + const bool mOldIsIdleRunnable; + }; + + // Notification that a runnable is about to run. This captures a snapshot of + // our current state before we reset to prepare for the new runnable. This + // muast be called after mNestedEventLoopDepth has been incremented for the + // runnable execution. The performance counter passed in should be the one + // for the relevant runnable and may be null. aIsIdleRunnable should be true + // if and only if the runnable has idle priority. + Snapshot RunnableWillRun(TimeStamp aNow, bool aIsIdleRunnable); + + // Notification that a runnable finished executing. This must be passed the + // snapshot that RunnableWillRun returned for the same runnable. This must be + // called before mNestedEventLoopDepth is decremented after the runnable's + // execution. + void RunnableDidRun(const nsCString& aName, Snapshot&& aSnapshot); + + const TimeStamp& LastLongTaskEnd() const { return mLastLongTaskEnd; } + const TimeStamp& LastLongNonIdleTaskEnd() const { + return mLastLongNonIdleTaskEnd; + } + + private: + // Called to report accumulated time, as needed, when we're about to run a + // runnable or just finished running one. + void MaybeReportAccumulatedTime(const nsCString& aName, TimeStamp aNow); + + // Whether the runnable we are about to run, or just ran, is a nested + // runnable, in the sense that there is some other runnable up the stack + // spinning the event loop. This must be called before we change our + // mCurrentEventLoopDepth (when about to run a new event) or after we restore + // it (after we ran one). + bool IsNestedRunnable() const { + return mNestedEventLoopDepth > mCurrentEventLoopDepth; + } + + // The event loop depth of the currently running runnable. Set to the max + // value of a uint32_t when there is no runnable running, so when starting to + // run a toplevel (not nested) runnable IsNestedRunnable() will test false. + uint32_t mCurrentEventLoopDepth = std::numeric_limits::max(); + + // A reference to the nsThread's mNestedEventLoopDepth, so we can + // see what it is right now. + const uint32_t& mNestedEventLoopDepth; + + // A boolean that indicates whether the currently running runnable is an idle + // runnable. Only has a useful value between RunnableWillRun() being called + // and RunnableDidRun() returning. + bool mCurrentRunnableIsIdleRunnable = false; + + // Whether we're attached to the mainthread nsThread. + const bool mIsMainThread; + + // what is considered a LongTask (in ms) + const Maybe mLongTaskLength; + + // The timestamp from which time to be accounted for should be measured. This + // can be the start of a runnable running or the end of a nested runnable + // running. + TimeStamp mCurrentTimeSliceStart; + + // Information about when long tasks last ended. + TimeStamp mLastLongTaskEnd; + TimeStamp mLastLongNonIdleTaskEnd; +}; +} // namespace mozilla + +// A native thread +class nsThread : public nsIThreadInternal, + public nsISupportsPriority, + public nsIDirectTaskDispatcher, + private mozilla::LinkedListElement { + friend mozilla::LinkedList; + friend mozilla::LinkedListElement; + + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + NS_DECL_NSITHREAD + NS_DECL_NSITHREADINTERNAL + NS_DECL_NSISUPPORTSPRIORITY + NS_DECL_NSIDIRECTTASKDISPATCHER + + enum MainThreadFlag { MAIN_THREAD, NOT_MAIN_THREAD }; + + nsThread(NotNull aQueue, + MainThreadFlag aMainThread, + nsIThreadManager::ThreadCreationOptions aOptions); + + private: + nsThread(); + + public: + // Initialize this as a named wrapper for a new PRThread. + nsresult Init(const nsACString& aName); + + // Initialize this as a wrapper for the current PRThread. + nsresult InitCurrentThread(); + + // Get this thread's name, thread-safe. + void GetThreadName(nsACString& aNameBuffer); + + // Set this thread's name. Consider using + // NS_SetCurrentThreadName if you are not sure. + void SetThreadNameInternal(const nsACString& aName); + + private: + // Initializes the mThreadId and stack base/size members, and adds the thread + // to the ThreadList(). + void InitCommon(); + + public: + // The PRThread corresponding to this thread. + PRThread* GetPRThread() const { return mThread; } + + const void* StackBase() const { return mStackBase; } + size_t StackSize() const { return mStackSize; } + + uint32_t ThreadId() const { return mThreadId; } + + // If this flag is true, then the nsThread was created using + // nsIThreadManager::NewThread. + bool ShutdownRequired() { return mShutdownRequired; } + + // Lets GetRunningEventDelay() determine if the pool this is part + // of has an unstarted thread + void SetPoolThreadFreePtr(mozilla::Atomic* aPtr) { + mIsAPoolThreadFree = aPtr; + } + + void SetScriptObserver(mozilla::CycleCollectedJSContext* aScriptObserver); + + uint32_t RecursionDepth() const; + + void ShutdownComplete(NotNull aContext); + + void WaitForAllAsynchronousShutdowns(); + + static const uint32_t kRunnableNameBufSize = 1000; + static mozilla::Array sMainThreadRunnableName; + + mozilla::SynchronizedEventQueue* EventQueue() { return mEvents.get(); } + + bool ShuttingDown() const { return mShutdownContext != nullptr; } + + size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + // Returns the size of this object, its PRThread, and its shutdown contexts, + // but excluding its event queues. + size_t ShallowSizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const; + + size_t SizeOfEventQueues(mozilla::MallocSizeOf aMallocSizeOf) const; + + void SetUseHangMonitor(bool aValue) { + MOZ_ASSERT(IsOnCurrentThread()); + mUseHangMonitor = aValue; + } + + private: + void DoMainThreadSpecificProcessing() const; + + protected: + friend class nsThreadShutdownEvent; + + virtual ~nsThread(); + + static void ThreadFunc(void* aArg); + + // Helper + already_AddRefed GetObserver() { + nsIThreadObserver* obs; + nsThread::GetObserver(&obs); + return already_AddRefed(obs); + } + + already_AddRefed ShutdownInternal(bool aSync); + + friend class nsThreadManager; + friend class nsThreadPool; + + void MaybeRemoveFromThreadList(); + + // Whether or not these members have a value determines whether the nsThread + // is treated as a full XPCOM thread or as a thin wrapper. + // + // For full nsThreads, they will always contain valid pointers. For thin + // wrappers around non-XPCOM threads, they will be null, and event dispatch + // methods which rely on them will fail (and assert) if called. + RefPtr mEvents; + RefPtr mEventTarget; + + // The number of outstanding nsThreadShutdownContext started by this thread. + // The thread will not be allowed to exit until this number reaches 0. + uint32_t mOutstandingShutdownContexts; + // The shutdown context for ourselves. + RefPtr mShutdownContext; + + mozilla::CycleCollectedJSContext* mScriptObserver; + + // Our name. + mozilla::DataMutex mThreadName; + + void* mStackBase = nullptr; + uint32_t mStackSize; + uint32_t mThreadId; + + uint32_t mNestedEventLoopDepth; + + mozilla::Atomic mShutdownRequired; + + int8_t mPriority; + + const bool mIsMainThread; + bool mUseHangMonitor; + const bool mIsUiThread; + mozilla::Atomic* mIsAPoolThreadFree; + + // Set to true if this thread creates a JSRuntime. + bool mCanInvokeJS; + + // The time the currently running event spent in event queues, and + // when it started running. If no event is running, they are + // TimeDuration() & TimeStamp(). + mozilla::TimeDuration mLastEventDelay; + mozilla::TimeStamp mLastEventStart; + +#ifdef EARLY_BETA_OR_EARLIER + nsCString mNameForWakeupTelemetry; + mozilla::TimeStamp mLastWakeupCheckTime; + uint32_t mWakeupCount = 0; +#endif + + mozilla::PerformanceCounterState mPerformanceCounterState; + + mozilla::SimpleTaskQueue mDirectTasks; +}; + +class nsThreadShutdownContext final : public nsIThreadShutdown { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSITHREADSHUTDOWN + + private: + friend class nsThread; + friend class nsThreadShutdownEvent; + friend class nsThreadShutdownAckEvent; + + nsThreadShutdownContext(NotNull aTerminatingThread, + nsThread* aJoiningThread) + : mTerminatingThread(aTerminatingThread), + mTerminatingPRThread(aTerminatingThread->GetPRThread()), + mJoiningThreadMutex("nsThreadShutdownContext::mJoiningThreadMutex"), + mJoiningThread(aJoiningThread) {} + + ~nsThreadShutdownContext() = default; + + // Must be called on the joining thread. + void MarkCompleted(); + + // NB: This may be the last reference. + NotNull> const mTerminatingThread; + PRThread* const mTerminatingPRThread; + + // May only be accessed on the joining thread. + bool mCompleted = false; + nsTArray> mCompletionCallbacks; + + // The thread waiting for this thread to shut down. Will either be cleared by + // the joining thread if `StopWaitingAndLeakThread` is called or by the + // terminating thread upon exiting and notifying the joining thread. + mozilla::Mutex mJoiningThreadMutex; + RefPtr mJoiningThread MOZ_GUARDED_BY(mJoiningThreadMutex); + bool mThreadLeaked MOZ_GUARDED_BY(mJoiningThreadMutex) = false; +}; + +#if defined(XP_UNIX) && !defined(ANDROID) && !defined(DEBUG) && HAVE_UALARM && \ + defined(_GNU_SOURCE) +# define MOZ_CANARY + +extern int sCanaryOutputFD; +#endif + +#endif // nsThread_h__ diff --git a/xpcom/threads/nsThreadManager.cpp b/xpcom/threads/nsThreadManager.cpp new file mode 100644 index 0000000000..805be6ee64 --- /dev/null +++ b/xpcom/threads/nsThreadManager.cpp @@ -0,0 +1,841 @@ +/* -*- 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 "nsThreadManager.h" +#include "nsThread.h" +#include "nsThreadPool.h" +#include "nsThreadUtils.h" +#include "nsIClassInfoImpl.h" +#include "nsExceptionHandler.h" +#include "nsTArray.h" +#include "nsXULAppAPI.h" +#include "nsExceptionHandler.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/AppShutdown.h" +#include "mozilla/ClearOnShutdown.h" +#include "mozilla/CycleCollectedJSContext.h" // nsAutoMicroTask +#include "mozilla/EventQueue.h" +#include "mozilla/InputTaskManager.h" +#include "mozilla/Mutex.h" +#include "mozilla/NeverDestroyed.h" +#include "mozilla/Preferences.h" +#include "mozilla/ProfilerMarkers.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "mozilla/StaticPtr.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/ThreadEventQueue.h" +#include "mozilla/ThreadLocal.h" +#include "TaskController.h" +#include "ThreadEventTarget.h" +#ifdef MOZ_CANARY +# include +# include +#endif + +#include "MainThreadIdlePeriod.h" + +using namespace mozilla; + +static MOZ_THREAD_LOCAL(bool) sTLSIsMainThread; + +bool NS_IsMainThreadTLSInitialized() { return sTLSIsMainThread.initialized(); } + +class BackgroundEventTarget final : public nsIEventTarget, + public TaskQueueTracker { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIEVENTTARGET_FULL + + BackgroundEventTarget() = default; + + nsresult Init(); + + already_AddRefed CreateBackgroundTaskQueue(const char* aName); + + void BeginShutdown(nsTArray>&); + void FinishShutdown(); + + private: + ~BackgroundEventTarget() = default; + + nsCOMPtr mPool; + nsCOMPtr mIOPool; +}; + +NS_IMPL_ISUPPORTS(BackgroundEventTarget, nsIEventTarget, TaskQueueTracker) + +nsresult BackgroundEventTarget::Init() { + nsCOMPtr pool(new nsThreadPool()); + NS_ENSURE_TRUE(pool, NS_ERROR_FAILURE); + + nsresult rv = pool->SetName("BackgroundThreadPool"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Use potentially more conservative stack size. + rv = pool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize); + NS_ENSURE_SUCCESS(rv, rv); + + // Thread limit of 2 makes deadlock during synchronous dispatch less likely. + rv = pool->SetThreadLimit(2); + NS_ENSURE_SUCCESS(rv, rv); + + rv = pool->SetIdleThreadLimit(1); + NS_ENSURE_SUCCESS(rv, rv); + + // Leave threads alive for up to 5 minutes + rv = pool->SetIdleThreadTimeout(300000); + NS_ENSURE_SUCCESS(rv, rv); + + // Initialize the background I/O event target. + nsCOMPtr ioPool(new nsThreadPool()); + NS_ENSURE_TRUE(pool, NS_ERROR_FAILURE); + + // The io pool spends a lot of its time blocking on io, so we want to offload + // these jobs on a lower priority if available. + rv = ioPool->SetQoSForThreads(nsIThread::QOS_PRIORITY_LOW); + NS_ENSURE_SUCCESS( + rv, rv); // note: currently infallible, keeping this for brevity. + + rv = ioPool->SetName("BgIOThreadPool"_ns); + NS_ENSURE_SUCCESS(rv, rv); + + // Use potentially more conservative stack size. + rv = ioPool->SetThreadStackSize(nsIThreadManager::kThreadPoolStackSize); + NS_ENSURE_SUCCESS(rv, rv); + + // Thread limit of 4 makes deadlock during synchronous dispatch less likely. + rv = ioPool->SetThreadLimit(4); + NS_ENSURE_SUCCESS(rv, rv); + + rv = ioPool->SetIdleThreadLimit(1); + NS_ENSURE_SUCCESS(rv, rv); + + // Leave threads alive for up to 5 minutes + rv = ioPool->SetIdleThreadTimeout(300000); + NS_ENSURE_SUCCESS(rv, rv); + + pool.swap(mPool); + ioPool.swap(mIOPool); + + return NS_OK; +} + +NS_IMETHODIMP_(bool) +BackgroundEventTarget::IsOnCurrentThreadInfallible() { + return mPool->IsOnCurrentThread() || mIOPool->IsOnCurrentThread(); +} + +NS_IMETHODIMP +BackgroundEventTarget::IsOnCurrentThread(bool* aValue) { + bool value = false; + if (NS_SUCCEEDED(mPool->IsOnCurrentThread(&value)) && value) { + *aValue = value; + return NS_OK; + } + return mIOPool->IsOnCurrentThread(aValue); +} + +NS_IMETHODIMP +BackgroundEventTarget::Dispatch(already_AddRefed aRunnable, + uint32_t aFlags) { + // We need to be careful here, because if an event is getting dispatched here + // from within TaskQueue::Runner::Run, it will be dispatched with + // NS_DISPATCH_AT_END, but we might not be running the event on the same + // pool, depending on which pool we were on and the dispatch flags. If we + // dispatch an event with NS_DISPATCH_AT_END to the wrong pool, the pool + // may not process the event in a timely fashion, which can lead to deadlock. + uint32_t flags = aFlags & ~NS_DISPATCH_EVENT_MAY_BLOCK; + bool mayBlock = bool(aFlags & NS_DISPATCH_EVENT_MAY_BLOCK); + nsCOMPtr& pool = mayBlock ? mIOPool : mPool; + + // If we're already running on the pool we want to dispatch to, we can + // unconditionally add NS_DISPATCH_AT_END to indicate that we shouldn't spin + // up a new thread. + // + // Otherwise, we should remove NS_DISPATCH_AT_END so we don't run into issues + // like those in the above comment. + if (pool->IsOnCurrentThread()) { + flags |= NS_DISPATCH_AT_END; + } else { + flags &= ~NS_DISPATCH_AT_END; + } + + return pool->Dispatch(std::move(aRunnable), flags); +} + +NS_IMETHODIMP +BackgroundEventTarget::DispatchFromScript(nsIRunnable* aRunnable, + uint32_t aFlags) { + nsCOMPtr runnable(aRunnable); + return Dispatch(runnable.forget(), aFlags); +} + +NS_IMETHODIMP +BackgroundEventTarget::DelayedDispatch(already_AddRefed aRunnable, + uint32_t) { + nsCOMPtr dropRunnable(aRunnable); + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +BackgroundEventTarget::RegisterShutdownTask(nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +BackgroundEventTarget::UnregisterShutdownTask(nsITargetShutdownTask* aTask) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +void BackgroundEventTarget::BeginShutdown( + nsTArray>& promises) { + auto queues = GetAllTrackedTaskQueues(); + for (auto& queue : queues) { + promises.AppendElement(queue->BeginShutdown()); + } +} + +void BackgroundEventTarget::FinishShutdown() { + mPool->Shutdown(); + mIOPool->Shutdown(); +} + +already_AddRefed BackgroundEventTarget::CreateBackgroundTaskQueue( + const char* aName) { + return TaskQueue::Create(do_AddRef(this), aName).forget(); +} + +extern "C" { +// This uses the C language linkage because it's exposed to Rust +// via the xpcom/rust/moz_task crate. +bool NS_IsMainThread() { return sTLSIsMainThread.get(); } +} + +void NS_SetMainThread() { + if (!sTLSIsMainThread.init()) { + MOZ_CRASH(); + } + sTLSIsMainThread.set(true); + MOZ_ASSERT(NS_IsMainThread()); + // We initialize the SerialEventTargetGuard's TLS here for simplicity as it + // needs to be initialized around the same time you would initialize + // sTLSIsMainThread. + SerialEventTargetGuard::InitTLS(); + nsThreadPool::InitTLS(); +} + +#ifdef DEBUG + +namespace mozilla { + +void AssertIsOnMainThread() { MOZ_ASSERT(NS_IsMainThread(), "Wrong thread!"); } + +} // namespace mozilla + +#endif + +//----------------------------------------------------------------------------- + +/* static */ +void nsThreadManager::ReleaseThread(void* aData) { + static_cast(aData)->Release(); +} + +// statically allocated instance +NS_IMETHODIMP_(MozExternalRefCountType) +nsThreadManager::AddRef() { return 2; } +NS_IMETHODIMP_(MozExternalRefCountType) +nsThreadManager::Release() { return 1; } +NS_IMPL_CLASSINFO(nsThreadManager, nullptr, + nsIClassInfo::THREADSAFE | nsIClassInfo::SINGLETON, + NS_THREADMANAGER_CID) +NS_IMPL_QUERY_INTERFACE_CI(nsThreadManager, nsIThreadManager) +NS_IMPL_CI_INTERFACE_GETTER(nsThreadManager, nsIThreadManager) + +//----------------------------------------------------------------------------- + +/*static*/ nsThreadManager& nsThreadManager::get() { + static NeverDestroyed sInstance; + return *sInstance; +} + +nsThreadManager::nsThreadManager() + : mCurThreadIndex(0), + mMutex("nsThreadManager::mMutex"), + mState(State::eUninit) {} + +nsThreadManager::~nsThreadManager() = default; + +nsresult nsThreadManager::Init() { + // Child processes need to initialize the thread manager before they + // initialize XPCOM in order to set up the crash reporter. This leads to + // situations where we get initialized twice. + { + OffTheBooksMutexAutoLock lock(mMutex); + if (mState > State::eUninit) { + return NS_OK; + } + } + + if (PR_NewThreadPrivateIndex(&mCurThreadIndex, ReleaseThread) == PR_FAILURE) { + return NS_ERROR_FAILURE; + } + +#ifdef MOZ_CANARY + const int flags = O_WRONLY | O_APPEND | O_CREAT | O_NONBLOCK; + const mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH; + char* env_var_flag = getenv("MOZ_KILL_CANARIES"); + sCanaryOutputFD = + env_var_flag + ? (env_var_flag[0] ? open(env_var_flag, flags, mode) : STDERR_FILENO) + : 0; +#endif + + TaskController::Initialize(); + + // Initialize idle handling. + nsCOMPtr idlePeriod = new MainThreadIdlePeriod(); + TaskController::Get()->SetIdleTaskManager( + new IdleTaskManager(idlePeriod.forget())); + + // Create main thread queue that forwards events to TaskController and + // construct main thread. + UniquePtr queue = MakeUnique(true); + + RefPtr synchronizedQueue = + new ThreadEventQueue(std::move(queue), true); + + mMainThread = + new nsThread(WrapNotNull(synchronizedQueue), nsThread::MAIN_THREAD, + {0, false, false, Some(W3_LONGTASK_BUSY_WINDOW_MS)}); + + nsresult rv = mMainThread->InitCurrentThread(); + if (NS_FAILED(rv)) { + mMainThread = nullptr; + return rv; + } +#ifdef MOZ_MEMORY + jemalloc_set_main_thread(); +#endif + + // Init AbstractThread. + AbstractThread::InitTLS(); + AbstractThread::InitMainThread(); + + // Initialize the background event target. + RefPtr target(new BackgroundEventTarget()); + + rv = target->Init(); + NS_ENSURE_SUCCESS(rv, rv); + + { + OffTheBooksMutexAutoLock lock(mMutex); + + mBackgroundEventTarget = std::move(target); + + mState = State::eActive; + } + + return NS_OK; +} + +void nsThreadManager::ShutdownNonMainThreads() { + MOZ_ASSERT(NS_IsMainThread(), "shutdown not called from main thread"); + + // Empty the main thread event queue before we begin shutting down threads. + NS_ProcessPendingEvents(mMainThread); + + mMainThread->mEvents->RunShutdownTasks(); + + RefPtr backgroundEventTarget; + { + OffTheBooksMutexAutoLock lock(mMutex); + MOZ_ASSERT(mState == State::eActive, "shutdown called multiple times"); + backgroundEventTarget = mBackgroundEventTarget; + } + + nsTArray> promises; + backgroundEventTarget->BeginShutdown(promises); + + bool taskQueuesShutdown = false; + // It's fine to capture everything by reference in the Then handler since it + // runs before we exit the nested event loop, thanks to the SpinEventLoopUntil + // below. + ShutdownPromise::All(mMainThread, promises)->Then(mMainThread, __func__, [&] { + backgroundEventTarget->FinishShutdown(); + taskQueuesShutdown = true; + }); + + // Wait for task queues to shutdown, so we don't shut down the underlying + // threads of the background event target in the block below, thereby + // preventing the task queues from emptying, preventing the shutdown promises + // from resolving, and prevent anything checking `taskQueuesShutdown` from + // working. + mozilla::SpinEventLoopUntil( + "nsThreadManager::Shutdown"_ns, [&]() { return taskQueuesShutdown; }, + mMainThread); + + { + // Prevent new nsThreads from being created, and collect a list of threads + // which need to be shut down. + // + // We don't prevent new thread creation until we've shut down background + // task queues, to ensure that they are able to start thread pool threads + // for shutdown tasks. + nsTArray> threadsToShutdown; + { + OffTheBooksMutexAutoLock lock(mMutex); + mState = State::eShutdown; + + for (auto* thread : mThreadList) { + if (thread->ShutdownRequired()) { + threadsToShutdown.AppendElement(thread); + } + } + } + + // It's tempting to walk the list of threads here and tell them each to stop + // accepting new events, but that could lead to badness if one of those + // threads is stuck waiting for a response from another thread. To do it + // right, we'd need some way to interrupt the threads. + // + // Instead, we process events on the current thread while waiting for + // threads to shutdown. This means that we have to preserve a mostly + // functioning world until such time as the threads exit. + + // As we're going to be waiting for all asynchronous shutdowns below, we + // can begin asynchronously shutting down all XPCOM threads here, rather + // than shutting each thread down one-at-a-time. + for (const auto& thread : threadsToShutdown) { + thread->AsyncShutdown(); + } + } + + // NB: It's possible that there are events in the queue that want to *start* + // an asynchronous shutdown. But we have already started async shutdown of + // the threads above, so there's no need to worry about them. We only have to + // wait for all in-flight asynchronous thread shutdowns to complete. + mMainThread->WaitForAllAsynchronousShutdowns(); + + // There are no more background threads at this point. +} + +void nsThreadManager::ShutdownMainThread() { +#ifdef DEBUG + { + OffTheBooksMutexAutoLock lock(mMutex); + MOZ_ASSERT(mState == State::eShutdown, "Must have called BeginShutdown"); + } +#endif + + // Do NS_ProcessPendingEvents but with special handling to set + // mEventsAreDoomed atomically with the removal of the last event. This means + // that PutEvent cannot succeed if the event would be left in the main thread + // queue after our final call to NS_ProcessPendingEvents. + // See comments in `nsThread::ThreadFunc` for a more detailed explanation. + while (true) { + if (mMainThread->mEvents->ShutdownIfNoPendingEvents()) { + break; + } + NS_ProcessPendingEvents(mMainThread); + } + + // Normally thread shutdown clears the observer for the thread, but since the + // main thread is special we do it manually here after we're sure all events + // have been processed. + mMainThread->SetObserver(nullptr); + + OffTheBooksMutexAutoLock lock(mMutex); + mBackgroundEventTarget = nullptr; +} + +void nsThreadManager::ReleaseMainThread() { +#ifdef DEBUG + { + OffTheBooksMutexAutoLock lock(mMutex); + MOZ_ASSERT(mState == State::eShutdown, "Must have called BeginShutdown"); + MOZ_ASSERT(!mBackgroundEventTarget, "Must have called ShutdownMainThread"); + } +#endif + MOZ_ASSERT(mMainThread); + + // Release main thread object. + mMainThread = nullptr; + + // Remove the TLS entry for the main thread. + PR_SetThreadPrivate(mCurThreadIndex, nullptr); +} + +void nsThreadManager::RegisterCurrentThread(nsThread& aThread) { + MOZ_ASSERT(aThread.GetPRThread() == PR_GetCurrentThread(), "bad aThread"); + + aThread.AddRef(); // for TLS entry + PR_SetThreadPrivate(mCurThreadIndex, &aThread); + +#ifdef DEBUG + { + OffTheBooksMutexAutoLock lock(mMutex); + MOZ_ASSERT(aThread.isInList(), + "Thread was not added to the thread list before registering!"); + } +#endif +} + +void nsThreadManager::UnregisterCurrentThread(nsThread& aThread) { + MOZ_ASSERT(aThread.GetPRThread() == PR_GetCurrentThread(), "bad aThread"); + + PR_SetThreadPrivate(mCurThreadIndex, nullptr); + // Ref-count balanced via ReleaseThread +} + +// Not to be used for MainThread! +nsThread* nsThreadManager::CreateCurrentThread(SynchronizedEventQueue* aQueue) { + // Make sure we don't have an nsThread yet. + MOZ_ASSERT(!PR_GetThreadPrivate(mCurThreadIndex)); + + if (!AllowNewXPCOMThreads()) { + return nullptr; + } + + RefPtr thread = new nsThread( + WrapNotNull(aQueue), nsThread::NOT_MAIN_THREAD, {.stackSize = 0}); + if (NS_FAILED(thread->InitCurrentThread())) { + return nullptr; + } + + return thread.get(); // reference held in TLS +} + +nsresult nsThreadManager::DispatchToBackgroundThread(nsIRunnable* aEvent, + uint32_t aDispatchFlags) { + RefPtr backgroundTarget; + { + OffTheBooksMutexAutoLock lock(mMutex); + if (!AllowNewXPCOMThreadsLocked() || !mBackgroundEventTarget) { + return NS_ERROR_FAILURE; + } + backgroundTarget = mBackgroundEventTarget; + } + + return backgroundTarget->Dispatch(aEvent, aDispatchFlags); +} + +already_AddRefed nsThreadManager::CreateBackgroundTaskQueue( + const char* aName) { + RefPtr backgroundTarget; + { + OffTheBooksMutexAutoLock lock(mMutex); + if (!AllowNewXPCOMThreadsLocked() || !mBackgroundEventTarget) { + return nullptr; + } + backgroundTarget = mBackgroundEventTarget; + } + + return backgroundTarget->CreateBackgroundTaskQueue(aName); +} + +nsThread* nsThreadManager::GetCurrentThread() { + // read thread local storage + void* data = PR_GetThreadPrivate(mCurThreadIndex); + if (data) { + return static_cast(data); + } + + // Keep this function working early during startup or late during shutdown on + // the main thread. + if (!AllowNewXPCOMThreads() || NS_IsMainThread()) { + return nullptr; + } + + // OK, that's fine. We'll dynamically create one :-) + // + // We assume that if we're implicitly creating a thread here that it doesn't + // want an event queue. Any thread which wants an event queue should + // explicitly create its nsThread wrapper. + // + // nsThread::InitCurrentThread() will check AllowNewXPCOMThreads, and return + // an error if we're too late in shutdown to create new XPCOM threads. + RefPtr thread = new nsThread(); + if (NS_FAILED(thread->InitCurrentThread())) { + return nullptr; + } + + return thread.get(); // reference held in TLS +} + +bool nsThreadManager::IsNSThread() const { + { + OffTheBooksMutexAutoLock lock(mMutex); + if (mState == State::eUninit) { + return false; + } + } + if (auto* thread = (nsThread*)PR_GetThreadPrivate(mCurThreadIndex)) { + return thread->EventQueue(); + } + return false; +} + +NS_IMETHODIMP +nsThreadManager::NewNamedThread( + const nsACString& aName, nsIThreadManager::ThreadCreationOptions aOptions, + nsIThread** aResult) { + // Note: can be called from arbitrary threads + + [[maybe_unused]] TimeStamp startTime = TimeStamp::Now(); + + RefPtr queue = + new ThreadEventQueue(MakeUnique()); + RefPtr thr = + new nsThread(WrapNotNull(queue), nsThread::NOT_MAIN_THREAD, aOptions); + + // Note: nsThread::Init() will check AllowNewXPCOMThreads, and return an + // error if we're too late in shutdown to create new XPCOM threads. If we + // aren't, the thread will be synchronously added to mThreadList. + nsresult rv = thr->Init(aName); + if (NS_FAILED(rv)) { + return rv; + } + + PROFILER_MARKER_TEXT( + "NewThread", OTHER, + MarkerOptions(MarkerStack::Capture(), + MarkerTiming::IntervalUntilNowFrom(startTime)), + aName); + if (!NS_IsMainThread()) { + PROFILER_MARKER_TEXT( + "NewThread (non-main thread)", OTHER, + MarkerOptions(MarkerStack::Capture(), MarkerThreadId::MainThread(), + MarkerTiming::IntervalUntilNowFrom(startTime)), + aName); + } + + thr.forget(aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadManager::GetMainThread(nsIThread** aResult) { + // Keep this functioning during Shutdown + if (!mMainThread) { + if (!NS_IsMainThread()) { + NS_WARNING( + "Called GetMainThread but there isn't a main thread and " + "we're not the main thread."); + } + return NS_ERROR_NOT_INITIALIZED; + } + NS_ADDREF(*aResult = mMainThread); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadManager::GetCurrentThread(nsIThread** aResult) { + // Keep this functioning during Shutdown + if (!mMainThread) { + return NS_ERROR_NOT_INITIALIZED; + } + *aResult = GetCurrentThread(); + if (!*aResult) { + return NS_ERROR_OUT_OF_MEMORY; + } + NS_ADDREF(*aResult); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadManager::SpinEventLoopUntil(const nsACString& aVeryGoodReasonToDoThis, + nsINestedEventLoopCondition* aCondition) { + return SpinEventLoopUntilInternal(aVeryGoodReasonToDoThis, aCondition, + ShutdownPhase::NotInShutdown); +} + +NS_IMETHODIMP +nsThreadManager::SpinEventLoopUntilOrQuit( + const nsACString& aVeryGoodReasonToDoThis, + nsINestedEventLoopCondition* aCondition) { + return SpinEventLoopUntilInternal(aVeryGoodReasonToDoThis, aCondition, + ShutdownPhase::AppShutdownConfirmed); +} + +// statics from SpinEventLoopUntil.h +AutoNestedEventLoopAnnotation* AutoNestedEventLoopAnnotation::sCurrent = + nullptr; +StaticMutex AutoNestedEventLoopAnnotation::sStackMutex; + +// static from SpinEventLoopUntil.h +void AutoNestedEventLoopAnnotation::AnnotateXPCOMSpinEventLoopStack( + const nsACString& aStack) { + if (aStack.Length() > 0) { + nsCString prefixedStack(XRE_GetProcessTypeString()); + prefixedStack += ": "_ns + aStack; + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::XPCOMSpinEventLoopStack, prefixedStack); + } else { + CrashReporter::AnnotateCrashReport( + CrashReporter::Annotation::XPCOMSpinEventLoopStack, ""_ns); + } +} + +nsresult nsThreadManager::SpinEventLoopUntilInternal( + const nsACString& aVeryGoodReasonToDoThis, + nsINestedEventLoopCondition* aCondition, + ShutdownPhase aShutdownPhaseToCheck) { + // XXX: We would want to AssertIsOnMainThread(); but that breaks some GTest. + nsCOMPtr condition(aCondition); + nsresult rv = NS_OK; + + if (!mozilla::SpinEventLoopUntil(aVeryGoodReasonToDoThis, [&]() -> bool { + // Check if an ongoing shutdown reached our limits. + if (aShutdownPhaseToCheck > ShutdownPhase::NotInShutdown && + AppShutdown::GetCurrentShutdownPhase() >= aShutdownPhaseToCheck) { + return true; + } + + bool isDone = false; + rv = condition->IsDone(&isDone); + // JS failure should be unusual, but we need to stop and propagate + // the error back to the caller. + if (NS_FAILED(rv)) { + return true; + } + + return isDone; + })) { + // We stopped early for some reason, which is unexpected. + return NS_ERROR_UNEXPECTED; + } + + // If we exited when the condition told us to, we need to return whether + // the condition encountered failure when executing. + return rv; +} + +NS_IMETHODIMP +nsThreadManager::SpinEventLoopUntilEmpty() { + nsIThread* thread = NS_GetCurrentThread(); + + while (NS_HasPendingEvents(thread)) { + (void)NS_ProcessNextEvent(thread, false); + } + + return NS_OK; +} + +NS_IMETHODIMP +nsThreadManager::GetMainThreadEventTarget(nsIEventTarget** aTarget) { + nsCOMPtr target = GetMainThreadSerialEventTarget(); + target.forget(aTarget); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadManager::DispatchToMainThread(nsIRunnable* aEvent, uint32_t aPriority, + uint8_t aArgc) { + // Note: C++ callers should instead use NS_DispatchToMainThread. + MOZ_ASSERT(NS_IsMainThread()); + + // Keep this functioning during Shutdown + if (NS_WARN_IF(!mMainThread)) { + return NS_ERROR_NOT_INITIALIZED; + } + // If aPriority wasn't explicitly passed, that means it should be treated as + // PRIORITY_NORMAL. + if (aArgc > 0 && aPriority != nsIRunnablePriority::PRIORITY_NORMAL) { + nsCOMPtr event(aEvent); + return mMainThread->DispatchFromScript( + new PrioritizableRunnable(event.forget(), aPriority), 0); + } + return mMainThread->DispatchFromScript(aEvent, 0); +} + +class AutoMicroTaskWrapperRunnable final : public Runnable { + public: + explicit AutoMicroTaskWrapperRunnable(nsIRunnable* aEvent) + : Runnable("AutoMicroTaskWrapperRunnable"), mEvent(aEvent) { + MOZ_ASSERT(aEvent); + } + + private: + ~AutoMicroTaskWrapperRunnable() = default; + + NS_IMETHOD Run() override { + nsAutoMicroTask mt; + + return mEvent->Run(); + } + + RefPtr mEvent; +}; + +NS_IMETHODIMP +nsThreadManager::DispatchToMainThreadWithMicroTask(nsIRunnable* aEvent, + uint32_t aPriority, + uint8_t aArgc) { + RefPtr runnable = + new AutoMicroTaskWrapperRunnable(aEvent); + + return DispatchToMainThread(runnable, aPriority, aArgc); +} + +void nsThreadManager::EnableMainThreadEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + InputTaskManager::Get()->EnableInputEventPrioritization(); +} + +void nsThreadManager::FlushInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + InputTaskManager::Get()->FlushInputEventPrioritization(); +} + +void nsThreadManager::SuspendInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + InputTaskManager::Get()->SuspendInputEventPrioritization(); +} + +void nsThreadManager::ResumeInputEventPrioritization() { + MOZ_ASSERT(NS_IsMainThread()); + InputTaskManager::Get()->ResumeInputEventPrioritization(); +} + +// static +bool nsThreadManager::MainThreadHasPendingHighPriorityEvents() { + MOZ_ASSERT(NS_IsMainThread()); + bool retVal = false; + if (get().mMainThread) { + get().mMainThread->HasPendingHighPriorityEvents(&retVal); + } + return retVal; +} + +NS_IMETHODIMP +nsThreadManager::IdleDispatchToMainThread(nsIRunnable* aEvent, + uint32_t aTimeout) { + // Note: C++ callers should instead use NS_DispatchToThreadQueue or + // NS_DispatchToCurrentThreadQueue. + MOZ_ASSERT(NS_IsMainThread()); + + nsCOMPtr event(aEvent); + if (aTimeout) { + return NS_DispatchToThreadQueue(event.forget(), aTimeout, mMainThread, + EventQueuePriority::Idle); + } + + return NS_DispatchToThreadQueue(event.forget(), mMainThread, + EventQueuePriority::Idle); +} + +NS_IMETHODIMP +nsThreadManager::DispatchDirectTaskToCurrentThread(nsIRunnable* aEvent) { + NS_ENSURE_STATE(aEvent); + nsCOMPtr runnable = aEvent; + return GetCurrentThread()->DispatchDirectTask(runnable.forget()); +} + +bool nsThreadManager::AllowNewXPCOMThreads() { + mozilla::OffTheBooksMutexAutoLock lock(mMutex); + return AllowNewXPCOMThreadsLocked(); +} diff --git a/xpcom/threads/nsThreadManager.h b/xpcom/threads/nsThreadManager.h new file mode 100644 index 0000000000..46e061a1ad --- /dev/null +++ b/xpcom/threads/nsThreadManager.h @@ -0,0 +1,156 @@ +/* -*- 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 nsThreadManager_h__ +#define nsThreadManager_h__ + +#include "nsIThreadManager.h" +#include "nsThread.h" +#include "mozilla/ShutdownPhase.h" + +class nsIRunnable; +class nsIThread; + +namespace mozilla { +class IdleTaskManager; +class SynchronizedEventQueue; +class TaskQueue; + +template +class NeverDestroyed; +} // namespace mozilla + +class BackgroundEventTarget; + +class nsThreadManager : public nsIThreadManager { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITHREADMANAGER + + static nsThreadManager& get(); + + nsresult Init(); + + // Shutdown all threads other than the main thread. This function should only + // be called on the main thread of the application process. + void ShutdownNonMainThreads(); + + // Finish shutting down all threads. This function must be called after + // ShutdownNonMainThreads and will delete the BackgroundEventTarget and + // take the main thread event target out of commission, but without + // releasing the underlying nsThread object. + void ShutdownMainThread(); + + // Release the underlying main thread nsThread object. + void ReleaseMainThread(); + + // Called by nsThread to inform the ThreadManager it exists. This method + // must be called when the given thread is the current thread. + void RegisterCurrentThread(nsThread& aThread); + + // Called by nsThread to inform the ThreadManager it is going away. This + // method must be called when the given thread is the current thread. + void UnregisterCurrentThread(nsThread& aThread); + + // Returns the current thread. Returns null if OOM or if ThreadManager isn't + // initialized. Creates the nsThread if one does not exist yet. + nsThread* GetCurrentThread(); + + // Returns true iff the currently running thread has an nsThread associated + // with it (ie; whether this is a thread that we can dispatch runnables to). + bool IsNSThread() const; + + // CreateCurrentThread sets up an nsThread for the current thread. It uses the + // event queue and main thread flags passed in. It should only be called once + // for the current thread. After it returns, GetCurrentThread() will return + // the thread that was created. GetCurrentThread() will also create a thread + // (lazily), but it doesn't allow the queue or main-thread attributes to be + // specified. + nsThread* CreateCurrentThread(mozilla::SynchronizedEventQueue* aQueue); + + nsresult DispatchToBackgroundThread(nsIRunnable* aEvent, + uint32_t aDispatchFlags); + + already_AddRefed CreateBackgroundTaskQueue( + const char* aName); + + ~nsThreadManager(); + + void EnableMainThreadEventPrioritization(); + void FlushInputEventPrioritization(); + void SuspendInputEventPrioritization(); + void ResumeInputEventPrioritization(); + + static bool MainThreadHasPendingHighPriorityEvents(); + + nsIThread* GetMainThreadWeak() { return mMainThread; } + + // Low level methods for interacting with the global thread list. Be very + // careful when holding `ThreadListMutex()` as no new threads can be started + // while it is held. + mozilla::OffTheBooksMutex& ThreadListMutex() MOZ_RETURN_CAPABILITY(mMutex) { + return mMutex; + } + + bool AllowNewXPCOMThreads() MOZ_EXCLUDES(mMutex); + bool AllowNewXPCOMThreadsLocked() MOZ_REQUIRES(mMutex) { + return mState == State::eActive; + } + + mozilla::LinkedList& ThreadList() MOZ_REQUIRES(mMutex) { + return mThreadList; + } + + private: + friend class mozilla::NeverDestroyed; + + nsThreadManager(); + + nsresult SpinEventLoopUntilInternal( + const nsACString& aVeryGoodReasonToDoThis, + nsINestedEventLoopCondition* aCondition, + mozilla::ShutdownPhase aShutdownPhaseToCheck); + + static void ReleaseThread(void* aData); + + enum class State : uint8_t { + // The thread manager has yet to be initialized. + eUninit, + // The thread manager is active, and operating normally. + eActive, + // The thread manager is in XPCOM shutdown. New calls to NS_NewNamedThread + // will fail, as all XPCOM threads are required to be shutting down. + eShutdown, + }; + + unsigned mCurThreadIndex; // thread-local-storage index + RefPtr mMainThread; + + mutable mozilla::OffTheBooksMutex mMutex; + + // Current state in the thread manager's lifecycle. See docs above. + State mState MOZ_GUARDED_BY(mMutex); + + // Global list of active nsThread instances, including both explicitly and + // implicitly created threads. + // + // NOTE: New entries to this list _may_ be added after mAllowNewThreads has + // been cleared, but only for implicitly created thread wrappers which are + // not shut down during XPCOM shutdown. + mozilla::LinkedList mThreadList MOZ_GUARDED_BY(mMutex); + + // Shared event target used for background runnables. + RefPtr mBackgroundEventTarget MOZ_GUARDED_BY(mMutex); +}; + +#define NS_THREADMANAGER_CID \ + { /* 7a4204c6-e45a-4c37-8ebb-6709a22c917c */ \ + 0x7a4204c6, 0xe45a, 0x4c37, { \ + 0x8e, 0xbb, 0x67, 0x09, 0xa2, 0x2c, 0x91, 0x7c \ + } \ + } + +#endif // nsThreadManager_h__ diff --git a/xpcom/threads/nsThreadPool.cpp b/xpcom/threads/nsThreadPool.cpp new file mode 100644 index 0000000000..c2ef022035 --- /dev/null +++ b/xpcom/threads/nsThreadPool.cpp @@ -0,0 +1,610 @@ +/* -*- 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 "nsThreadPool.h" + +#include "nsCOMArray.h" +#include "ThreadDelay.h" +#include "nsThreadManager.h" +#include "nsThread.h" +#include "nsThreadUtils.h" +#include "prinrval.h" +#include "mozilla/Logging.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ProfilerRunnable.h" +#include "mozilla/SchedulerGroup.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/SpinEventLoopUntil.h" +#include "nsThreadSyncDispatch.h" + +#include + +using namespace mozilla; + +static LazyLogModule sThreadPoolLog("nsThreadPool"); +#ifdef LOG +# undef LOG +#endif +#define LOG(args) MOZ_LOG(sThreadPoolLog, mozilla::LogLevel::Debug, args) + +static MOZ_THREAD_LOCAL(nsThreadPool*) gCurrentThreadPool; + +void nsThreadPool::InitTLS() { gCurrentThreadPool.infallibleInit(); } + +// DESIGN: +// o Allocate anonymous threads. +// o Use nsThreadPool::Run as the main routine for each thread. +// o Each thread waits on the event queue's monitor, checking for +// pending events and rescheduling itself as an idle thread. + +#define DEFAULT_THREAD_LIMIT 4 +#define DEFAULT_IDLE_THREAD_LIMIT 1 +#define DEFAULT_IDLE_THREAD_TIMEOUT PR_SecondsToInterval(60) + +NS_IMPL_ISUPPORTS_INHERITED(nsThreadPool, Runnable, nsIThreadPool, + nsIEventTarget) + +nsThreadPool* nsThreadPool::GetCurrentThreadPool() { + return gCurrentThreadPool.get(); +} + +nsThreadPool::nsThreadPool() + : Runnable("nsThreadPool"), + mMutex("[nsThreadPool.mMutex]"), + mEventsAvailable(mMutex, "[nsThreadPool.mEventsAvailable]"), + mThreadLimit(DEFAULT_THREAD_LIMIT), + mIdleThreadLimit(DEFAULT_IDLE_THREAD_LIMIT), + mIdleThreadTimeout(DEFAULT_IDLE_THREAD_TIMEOUT), + mIdleCount(0), + mQoSPriority(nsIThread::QOS_PRIORITY_NORMAL), + mStackSize(nsIThreadManager::DEFAULT_STACK_SIZE), + mShutdown(false), + mRegressiveMaxIdleTime(false), + mIsAPoolThreadFree(true) { + LOG(("THRD-P(%p) constructor!!!\n", this)); +} + +nsThreadPool::~nsThreadPool() { + // Threads keep a reference to the nsThreadPool until they return from Run() + // after removing themselves from mThreads. + MOZ_ASSERT(mThreads.IsEmpty()); +} + +nsresult nsThreadPool::PutEvent(nsIRunnable* aEvent) { + nsCOMPtr event(aEvent); + return PutEvent(event.forget(), 0); +} + +nsresult nsThreadPool::PutEvent(already_AddRefed aEvent, + uint32_t aFlags) { + // Avoid spawning a new thread while holding the event queue lock... + + bool spawnThread = false; + uint32_t stackSize = 0; + nsCString name; + { + MutexAutoLock lock(mMutex); + + if (NS_WARN_IF(mShutdown)) { + return NS_ERROR_NOT_AVAILABLE; + } + LOG(("THRD-P(%p) put [%d %d %d]\n", this, mIdleCount, mThreads.Count(), + mThreadLimit)); + MOZ_ASSERT(mIdleCount <= (uint32_t)mThreads.Count(), "oops"); + + // Make sure we have a thread to service this event. + if (mThreads.Count() < (int32_t)mThreadLimit && + !(aFlags & NS_DISPATCH_AT_END) && + // Spawn a new thread if we don't have enough idle threads to serve + // pending events immediately. + mEvents.Count(lock) >= mIdleCount) { + spawnThread = true; + } + + nsCOMPtr event(aEvent); + LogRunnable::LogDispatch(event); + mEvents.PutEvent(event.forget(), EventQueuePriority::Normal, lock); + mEventsAvailable.Notify(); + stackSize = mStackSize; + name = mName; + } + + auto delay = MakeScopeExit([&]() { + // Delay to encourage the receiving task to run before we do work. + DelayForChaosMode(ChaosFeature::TaskDispatching, 1000); + }); + + LOG(("THRD-P(%p) put [spawn=%d]\n", this, spawnThread)); + if (!spawnThread) { + return NS_OK; + } + + nsCOMPtr thread; + nsresult rv = NS_NewNamedThread( + mThreadNaming.GetNextThreadName(name), getter_AddRefs(thread), nullptr, + {.stackSize = stackSize, .blockDispatch = true}); + if (NS_WARN_IF(NS_FAILED(rv))) { + return NS_ERROR_UNEXPECTED; + } + + bool killThread = false; + { + MutexAutoLock lock(mMutex); + if (mShutdown) { + killThread = true; + } else if (mThreads.Count() < (int32_t)mThreadLimit) { + mThreads.AppendObject(thread); + if (mThreads.Count() >= (int32_t)mThreadLimit) { + mIsAPoolThreadFree = false; + } + } else { + // Someone else may have also been starting a thread + killThread = true; // okay, we don't need this thread anymore + } + } + LOG(("THRD-P(%p) put [%p kill=%d]\n", this, thread.get(), killThread)); + if (killThread) { + // We never dispatched any events to the thread, so we can shut it down + // asynchronously without worrying about anything. + ShutdownThread(thread); + } else { + thread->Dispatch(this, NS_DISPATCH_IGNORE_BLOCK_DISPATCH); + } + + return NS_OK; +} + +void nsThreadPool::ShutdownThread(nsIThread* aThread) { + LOG(("THRD-P(%p) shutdown async [%p]\n", this, aThread)); + + // This is either called by a threadpool thread that is out of work, or + // a thread that attempted to create a threadpool thread and raced in + // such a way that the newly created thread is no longer necessary. + // In the first case, we must go to another thread to shut aThread down + // (because it is the current thread). In the second case, we cannot + // synchronously shut down the current thread (because then Dispatch() would + // spin the event loop, and that could blow up the world), and asynchronous + // shutdown requires this thread have an event loop (and it may not, see bug + // 10204784). The simplest way to cover all cases is to asynchronously + // shutdown aThread from the main thread. + SchedulerGroup::Dispatch(NewRunnableMethod( + "nsIThread::AsyncShutdown", aThread, &nsIThread::AsyncShutdown)); +} + +NS_IMETHODIMP +nsThreadPool::SetQoSForThreads(nsIThread::QoSPriority aPriority) { + MutexAutoLock lock(mMutex); + mQoSPriority = aPriority; + + // We don't notify threads here to observe the change, because we don't want + // to create spurious wakeups during idle. Rather, we want threads to simply + // observe the change on their own if they wake up to do some task. + + return NS_OK; +} + +// This event 'runs' for the lifetime of the worker thread. The actual +// eventqueue is mEvents, and is shared by all the worker threads. This +// means that the set of threads together define the delay seen by a new +// event sent to the pool. +// +// To model the delay experienced by the pool, we can have each thread in +// the pool report 0 if it's idle OR if the pool is below the threadlimit; +// or otherwise the current event's queuing delay plus current running +// time. +// +// To reconstruct the delays for the pool, the profiler can look at all the +// threads that are part of a pool (pools have defined naming patterns that +// can be user to connect them). If all threads have delays at time X, +// that means that all threads saturated at that point and any event +// dispatched to the pool would get a delay. +// +// The delay experienced by an event dispatched when all pool threads are +// busy is based on the calculations shown in platform.cpp. Run that +// algorithm for each thread in the pool, and the delay at time X is the +// longest value for time X of any of the threads, OR the time from X until +// any one of the threads reports 0 (i.e. it's not busy), whichever is +// shorter. + +// In order to record this when the profiler samples threads in the pool, +// each thread must (effectively) override GetRunnningEventDelay, by +// resetting the mLastEventDelay/Start values in the nsThread when we start +// to run an event (or when we run out of events to run). Note that handling +// the shutdown of a thread may be a little tricky. + +NS_IMETHODIMP +nsThreadPool::Run() { + nsCOMPtr current; + nsThreadManager::get().GetCurrentThread(getter_AddRefs(current)); + + bool shutdownThreadOnExit = false; + bool exitThread = false; + bool wasIdle = false; + TimeStamp idleSince; + nsIThread::QoSPriority threadPriority = nsIThread::QOS_PRIORITY_NORMAL; + + // This thread is an nsThread created below with NS_NewNamedThread() + static_cast(current.get()) + ->SetPoolThreadFreePtr(&mIsAPoolThreadFree); + + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + listener = mListener; + LOG(("THRD-P(%p) enter %s\n", this, mName.BeginReading())); + + // Go ahead and check for thread priority. If priority is normal, do nothing + // because threads are created with default priority. + if (threadPriority != mQoSPriority) { + current->SetThreadQoS(threadPriority); + threadPriority = mQoSPriority; + } + } + + if (listener) { + listener->OnThreadCreated(); + } + + MOZ_ASSERT(!gCurrentThreadPool.get()); + gCurrentThreadPool.set(this); + + do { + nsCOMPtr event; + TimeDuration delay; + { + MutexAutoLock lock(mMutex); + + // Before getting the next event, we can adjust priority as needed. + if (threadPriority != mQoSPriority) { + current->SetThreadQoS(threadPriority); + threadPriority = mQoSPriority; + } + + event = mEvents.GetEvent(lock, &delay); + if (!event) { + TimeStamp now = TimeStamp::Now(); + uint32_t idleTimeoutDivider = + (mIdleCount && mRegressiveMaxIdleTime) ? mIdleCount : 1; + TimeDuration timeout = TimeDuration::FromMilliseconds( + static_cast(mIdleThreadTimeout) / idleTimeoutDivider); + + // If we are shutting down, then don't keep any idle threads. + if (mShutdown) { + exitThread = true; + } else { + if (wasIdle) { + // if too many idle threads or idle for too long, then bail. + if (mIdleCount > mIdleThreadLimit || + (mIdleThreadTimeout != UINT32_MAX && + (now - idleSince) >= timeout)) { + exitThread = true; + } + } else { + // if would be too many idle threads... + if (mIdleCount == mIdleThreadLimit) { + exitThread = true; + } else { + ++mIdleCount; + idleSince = now; + wasIdle = true; + } + } + } + + if (exitThread) { + if (wasIdle) { + --mIdleCount; + } + shutdownThreadOnExit = mThreads.RemoveObject(current); + + // keep track if there are threads available to start + mIsAPoolThreadFree = (mThreads.Count() < (int32_t)mThreadLimit); + } else { + current->SetRunningEventDelay(TimeDuration(), TimeStamp()); + + AUTO_PROFILER_LABEL("nsThreadPool::Run::Wait", IDLE); + + TimeDuration delta = timeout - (now - idleSince); + LOG(("THRD-P(%p) %s waiting [%f]\n", this, mName.BeginReading(), + delta.ToMilliseconds())); + mEventsAvailable.Wait(delta); + LOG(("THRD-P(%p) done waiting\n", this)); + } + } else if (wasIdle) { + wasIdle = false; + --mIdleCount; + } + } + if (event) { + if (MOZ_LOG_TEST(sThreadPoolLog, mozilla::LogLevel::Debug)) { + MutexAutoLock lock(mMutex); + LOG(("THRD-P(%p) %s running [%p]\n", this, mName.BeginReading(), + event.get())); + } + + // Delay event processing to encourage whoever dispatched this event + // to run. + DelayForChaosMode(ChaosFeature::TaskRunning, 1000); + + if (profiler_thread_is_being_profiled( + ThreadProfilingFeatures::Sampling)) { + // We'll handle the case of unstarted threads available + // when we sample. + current->SetRunningEventDelay(delay, TimeStamp::Now()); + } + + LogRunnable::Run log(event); + AUTO_PROFILE_FOLLOWING_RUNNABLE(event); + event->Run(); + // To cover the event's destructor code in the LogRunnable span + event = nullptr; + } + } while (!exitThread); + + if (listener) { + listener->OnThreadShuttingDown(); + } + + MOZ_ASSERT(gCurrentThreadPool.get() == this); + gCurrentThreadPool.set(nullptr); + + if (shutdownThreadOnExit) { + ShutdownThread(current); + } + + LOG(("THRD-P(%p) leave\n", this)); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { + nsCOMPtr event(aEvent); + return Dispatch(event.forget(), aFlags); +} + +NS_IMETHODIMP +nsThreadPool::Dispatch(already_AddRefed aEvent, uint32_t aFlags) { + LOG(("THRD-P(%p) dispatch [%p %x]\n", this, /* XXX aEvent*/ nullptr, aFlags)); + + if (NS_WARN_IF(mShutdown)) { + nsCOMPtr event(aEvent); + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ASSERTION(aFlags == NS_DISPATCH_NORMAL || aFlags == NS_DISPATCH_AT_END, + "unexpected dispatch flags"); + PutEvent(std::move(aEvent), aFlags); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::DelayedDispatch(already_AddRefed, uint32_t) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsThreadPool::RegisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP +nsThreadPool::UnregisterShutdownTask(nsITargetShutdownTask*) { + return NS_ERROR_NOT_IMPLEMENTED; +} + +NS_IMETHODIMP_(bool) +nsThreadPool::IsOnCurrentThreadInfallible() { + return gCurrentThreadPool.get() == this; +} + +NS_IMETHODIMP +nsThreadPool::IsOnCurrentThread(bool* aResult) { + MutexAutoLock lock(mMutex); + if (NS_WARN_IF(mShutdown)) { + return NS_ERROR_NOT_AVAILABLE; + } + + *aResult = IsOnCurrentThreadInfallible(); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::Shutdown() { return ShutdownWithTimeout(-1); } + +NS_IMETHODIMP +nsThreadPool::ShutdownWithTimeout(int32_t aTimeoutMs) { + nsCOMArray threads; + nsCOMPtr listener; + { + MutexAutoLock lock(mMutex); + if (mShutdown) { + return NS_ERROR_ILLEGAL_DURING_SHUTDOWN; + } + mShutdown = true; + mEventsAvailable.NotifyAll(); + + threads.AppendObjects(mThreads); + mThreads.Clear(); + + // Swap in a null listener so that we release the listener at the end of + // this method. The listener will be kept alive as long as the other threads + // that were created when it was set. + mListener.swap(listener); + } + + nsTArray> contexts; + for (int32_t i = 0; i < threads.Count(); ++i) { + nsCOMPtr context; + if (NS_SUCCEEDED(threads[i]->BeginShutdown(getter_AddRefs(context)))) { + contexts.AppendElement(std::move(context)); + } + } + + // Start a timer which will stop waiting & leak the thread, forcing + // onCompletion to be called when it expires. + nsCOMPtr timer; + if (aTimeoutMs >= 0) { + NS_NewTimerWithCallback( + getter_AddRefs(timer), + [&](nsITimer*) { + for (auto& context : contexts) { + context->StopWaitingAndLeakThread(); + } + }, + aTimeoutMs, nsITimer::TYPE_ONE_SHOT, + "nsThreadPool::ShutdownWithTimeout"); + } + + // Start a counter and register a callback to decrement outstandingThreads + // when the threads finish exiting. We'll spin an event loop until + // outstandingThreads reaches 0. + uint32_t outstandingThreads = contexts.Length(); + RefPtr onCompletion = NS_NewCancelableRunnableFunction( + "nsThreadPool thread completion", [&] { --outstandingThreads; }); + for (auto& context : contexts) { + context->OnCompletion(onCompletion); + } + + mozilla::SpinEventLoopUntil("nsThreadPool::ShutdownWithTimeout"_ns, + [&] { return outstandingThreads == 0; }); + + if (timer) { + timer->Cancel(); + } + onCompletion->Cancel(); + + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetThreadLimit(uint32_t* aValue) { + MutexAutoLock lock(mMutex); + *aValue = mThreadLimit; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetThreadLimit(uint32_t aValue) { + MutexAutoLock lock(mMutex); + LOG(("THRD-P(%p) thread limit [%u]\n", this, aValue)); + mThreadLimit = aValue; + if (mIdleThreadLimit > mThreadLimit) { + mIdleThreadLimit = mThreadLimit; + } + + if (static_cast(mThreads.Count()) > mThreadLimit) { + mEventsAvailable + .NotifyAll(); // wake up threads so they observe this change + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetIdleThreadLimit(uint32_t* aValue) { + MutexAutoLock lock(mMutex); + *aValue = mIdleThreadLimit; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetIdleThreadLimit(uint32_t aValue) { + MutexAutoLock lock(mMutex); + LOG(("THRD-P(%p) idle thread limit [%u]\n", this, aValue)); + mIdleThreadLimit = aValue; + if (mIdleThreadLimit > mThreadLimit) { + mIdleThreadLimit = mThreadLimit; + } + + // Do we need to kill some idle threads? + if (mIdleCount > mIdleThreadLimit) { + mEventsAvailable + .NotifyAll(); // wake up threads so they observe this change + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetIdleThreadTimeout(uint32_t* aValue) { + MutexAutoLock lock(mMutex); + *aValue = mIdleThreadTimeout; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetIdleThreadTimeout(uint32_t aValue) { + MutexAutoLock lock(mMutex); + uint32_t oldTimeout = mIdleThreadTimeout; + mIdleThreadTimeout = aValue; + + // Do we need to notify any idle threads that their sleep time has shortened? + if (mIdleThreadTimeout < oldTimeout && mIdleCount > 0) { + mEventsAvailable + .NotifyAll(); // wake up threads so they observe this change + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetIdleThreadTimeoutRegressive(bool* aValue) { + MutexAutoLock lock(mMutex); + *aValue = mRegressiveMaxIdleTime; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetIdleThreadTimeoutRegressive(bool aValue) { + MutexAutoLock lock(mMutex); + bool oldRegressive = mRegressiveMaxIdleTime; + mRegressiveMaxIdleTime = aValue; + + // Would setting regressive timeout effect idle threads? + if (mRegressiveMaxIdleTime > oldRegressive && mIdleCount > 1) { + mEventsAvailable + .NotifyAll(); // wake up threads so they observe this change + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetThreadStackSize(uint32_t* aValue) { + MutexAutoLock lock(mMutex); + *aValue = mStackSize; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetThreadStackSize(uint32_t aValue) { + MutexAutoLock lock(mMutex); + mStackSize = aValue; + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::GetListener(nsIThreadPoolListener** aListener) { + MutexAutoLock lock(mMutex); + NS_IF_ADDREF(*aListener = mListener); + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetListener(nsIThreadPoolListener* aListener) { + nsCOMPtr swappedListener(aListener); + { + MutexAutoLock lock(mMutex); + mListener.swap(swappedListener); + } + return NS_OK; +} + +NS_IMETHODIMP +nsThreadPool::SetName(const nsACString& aName) { + MutexAutoLock lock(mMutex); + if (mThreads.Count()) { + return NS_ERROR_NOT_AVAILABLE; + } + mName = aName; + return NS_OK; +} diff --git a/xpcom/threads/nsThreadPool.h b/xpcom/threads/nsThreadPool.h new file mode 100644 index 0000000000..b10a2f5265 --- /dev/null +++ b/xpcom/threads/nsThreadPool.h @@ -0,0 +1,68 @@ +/* -*- 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 nsThreadPool_h__ +#define nsThreadPool_h__ + +#include "nsIThreadPool.h" +#include "nsIRunnable.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsThreadUtils.h" +#include "mozilla/Atomics.h" +#include "mozilla/AlreadyAddRefed.h" +#include "mozilla/CondVar.h" +#include "mozilla/EventQueue.h" +#include "mozilla/Mutex.h" + +class nsIThread; + +class nsThreadPool final : public mozilla::Runnable, public nsIThreadPool { + public: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIEVENTTARGET_FULL + NS_DECL_NSITHREADPOOL + NS_DECL_NSIRUNNABLE + + nsThreadPool(); + + static void InitTLS(); + static nsThreadPool* GetCurrentThreadPool(); + + private: + ~nsThreadPool(); + + void ShutdownThread(nsIThread* aThread); + nsresult PutEvent(nsIRunnable* aEvent); + nsresult PutEvent(already_AddRefed aEvent, uint32_t aFlags); + + mozilla::Mutex mMutex; + nsCOMArray mThreads MOZ_GUARDED_BY(mMutex); + mozilla::CondVar mEventsAvailable MOZ_GUARDED_BY(mMutex); + mozilla::EventQueue mEvents MOZ_GUARDED_BY(mMutex); + uint32_t mThreadLimit MOZ_GUARDED_BY(mMutex); + uint32_t mIdleThreadLimit MOZ_GUARDED_BY(mMutex); + uint32_t mIdleThreadTimeout MOZ_GUARDED_BY(mMutex); + uint32_t mIdleCount MOZ_GUARDED_BY(mMutex); + nsIThread::QoSPriority mQoSPriority MOZ_GUARDED_BY(mMutex); + uint32_t mStackSize MOZ_GUARDED_BY(mMutex); + nsCOMPtr mListener MOZ_GUARDED_BY(mMutex); + mozilla::Atomic mShutdown; + bool mRegressiveMaxIdleTime MOZ_GUARDED_BY(mMutex); + mozilla::Atomic mIsAPoolThreadFree; + // set once before we start threads + nsCString mName MOZ_GUARDED_BY(mMutex); + nsThreadPoolNaming mThreadNaming; // all data inside this is atomic +}; + +#define NS_THREADPOOL_CID \ + { /* 547ec2a8-315e-4ec4-888e-6e4264fe90eb */ \ + 0x547ec2a8, 0x315e, 0x4ec4, { \ + 0x88, 0x8e, 0x6e, 0x42, 0x64, 0xfe, 0x90, 0xeb \ + } \ + } + +#endif // nsThreadPool_h__ diff --git a/xpcom/threads/nsThreadSyncDispatch.h b/xpcom/threads/nsThreadSyncDispatch.h new file mode 100644 index 0000000000..1673453f9d --- /dev/null +++ b/xpcom/threads/nsThreadSyncDispatch.h @@ -0,0 +1,65 @@ +/* -*- 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 nsThreadSyncDispatch_h_ +#define nsThreadSyncDispatch_h_ + +#include "mozilla/Atomics.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/SpinEventLoopUntil.h" + +#include "nsThreadUtils.h" +#include "LeakRefPtr.h" + +class nsThreadSyncDispatch : public mozilla::Runnable { + public: + nsThreadSyncDispatch(already_AddRefed aOrigin, + already_AddRefed&& aTask) + : Runnable("nsThreadSyncDispatch"), + mOrigin(aOrigin), + mSyncTask(std::move(aTask)), + mIsPending(true) {} + + bool IsPending() { + // This is an atomic acquire on the origin thread. + return mIsPending; + } + + void SpinEventLoopUntilComplete(const nsACString& aVeryGoodReasonToDoThis) { + mozilla::SpinEventLoopUntil(aVeryGoodReasonToDoThis, + [&]() -> bool { return !IsPending(); }); + } + + private: + NS_IMETHOD Run() override { + if (nsCOMPtr task = mSyncTask.take()) { + MOZ_ASSERT(!mSyncTask); + + mozilla::DebugOnly result = task->Run(); + MOZ_ASSERT(NS_SUCCEEDED(result), "task in sync dispatch should not fail"); + + // We must release the task here to ensure that when the original + // thread is unblocked, this task has been released. + task = nullptr; + + // This is an atomic release on the target thread. + mIsPending = false; + + // unblock the origin thread + mOrigin->Dispatch(this, NS_DISPATCH_IGNORE_BLOCK_DISPATCH); + } + + return NS_OK; + } + + nsCOMPtr mOrigin; + // The task is leaked by default when Run() is not called, because + // otherwise we may release it in an incorrect thread. + mozilla::LeakRefPtr mSyncTask; + mozilla::Atomic mIsPending; +}; + +#endif // nsThreadSyncDispatch_h_ diff --git a/xpcom/threads/nsThreadUtils.cpp b/xpcom/threads/nsThreadUtils.cpp new file mode 100644 index 0000000000..68d36605b3 --- /dev/null +++ b/xpcom/threads/nsThreadUtils.cpp @@ -0,0 +1,769 @@ +/* -*- 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 "nsThreadUtils.h" + +#include "chrome/common/ipc_message.h" // for IPC::Message +#include "LeakRefPtr.h" +#include "mozilla/Attributes.h" +#include "mozilla/Likely.h" +#include "mozilla/TaskQueue.h" +#include "mozilla/TimeStamp.h" +#include "nsComponentManagerUtils.h" +#include "nsExceptionHandler.h" +#include "nsIEventTarget.h" +#include "nsITimer.h" +#include "nsString.h" +#include "nsThreadSyncDispatch.h" +#include "nsTimerImpl.h" +#include "prsystem.h" + +#include "nsThreadManager.h" +#include "nsThreadPool.h" +#include "TaskController.h" + +#ifdef XP_WIN +# include +#elif defined(XP_MACOSX) +# include +#endif + +#if defined(ANDROID) +# include +#endif + +static mozilla::LazyLogModule sEventDispatchAndRunLog("events"); +#ifdef LOG1 +# undef LOG1 +#endif +#define LOG1(args) \ + MOZ_LOG(sEventDispatchAndRunLog, mozilla::LogLevel::Error, args) +#define LOG1_ENABLED() \ + MOZ_LOG_TEST(sEventDispatchAndRunLog, mozilla::LogLevel::Error) + +using namespace mozilla; + +#ifndef XPCOM_GLUE_AVOID_NSPR + +NS_IMPL_ISUPPORTS(IdlePeriod, nsIIdlePeriod) + +NS_IMETHODIMP +IdlePeriod::GetIdlePeriodHint(TimeStamp* aIdleDeadline) { + *aIdleDeadline = TimeStamp(); + return NS_OK; +} + +// NS_IMPL_NAMED_* relies on the mName field, which is not present on +// release or beta. Instead, fall back to using "Runnable" for all +// runnables. +# ifndef MOZ_COLLECTING_RUNNABLE_TELEMETRY +NS_IMPL_ISUPPORTS(Runnable, nsIRunnable) +# else +NS_IMPL_NAMED_ADDREF(Runnable, mName) +NS_IMPL_NAMED_RELEASE(Runnable, mName) +NS_IMPL_QUERY_INTERFACE(Runnable, nsIRunnable, nsINamed) +# endif + +NS_IMETHODIMP +Runnable::Run() { + // Do nothing + return NS_OK; +} + +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY +NS_IMETHODIMP +Runnable::GetName(nsACString& aName) { + if (mName) { + aName.AssignASCII(mName); + } else { + aName.Truncate(); + } + return NS_OK; +} +# endif + +NS_IMPL_ISUPPORTS_INHERITED(DiscardableRunnable, Runnable, + nsIDiscardableRunnable) + +NS_IMPL_ISUPPORTS_INHERITED(CancelableRunnable, DiscardableRunnable, + nsICancelableRunnable) + +void CancelableRunnable::OnDiscard() { + // Tasks that implement Cancel() can be safely cleaned up if it turns out + // that the task will not run. + (void)NS_WARN_IF(NS_FAILED(Cancel())); +} + +NS_IMPL_ISUPPORTS_INHERITED(IdleRunnable, DiscardableRunnable, nsIIdleRunnable) + +NS_IMPL_ISUPPORTS_INHERITED(CancelableIdleRunnable, CancelableRunnable, + nsIIdleRunnable) + +NS_IMPL_ISUPPORTS_INHERITED(PrioritizableRunnable, Runnable, + nsIRunnablePriority) + +PrioritizableRunnable::PrioritizableRunnable( + already_AddRefed&& aRunnable, uint32_t aPriority) + // Real runnable name is managed by overridding the GetName function. + : Runnable("PrioritizableRunnable"), + mRunnable(std::move(aRunnable)), + mPriority(aPriority) { +# if DEBUG + nsCOMPtr runnablePrio = do_QueryInterface(mRunnable); + MOZ_ASSERT(!runnablePrio); +# endif +} + +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY +NS_IMETHODIMP +PrioritizableRunnable::GetName(nsACString& aName) { + // Try to get a name from the underlying runnable. + nsCOMPtr named = do_QueryInterface(mRunnable); + if (named) { + named->GetName(aName); + } + return NS_OK; +} +# endif + +NS_IMETHODIMP +PrioritizableRunnable::Run() { + MOZ_RELEASE_ASSERT(NS_IsMainThread()); + return mRunnable->Run(); +} + +NS_IMETHODIMP +PrioritizableRunnable::GetPriority(uint32_t* aPriority) { + *aPriority = mPriority; + return NS_OK; +} + +already_AddRefed mozilla::CreateRenderBlockingRunnable( + already_AddRefed&& aRunnable) { + nsCOMPtr runnable = new PrioritizableRunnable( + std::move(aRunnable), nsIRunnablePriority::PRIORITY_RENDER_BLOCKING); + return runnable.forget(); +} + +NS_IMPL_ISUPPORTS_INHERITED(PrioritizableCancelableRunnable, CancelableRunnable, + nsIRunnablePriority) + +NS_IMETHODIMP +PrioritizableCancelableRunnable::GetPriority(uint32_t* aPriority) { + *aPriority = mPriority; + return NS_OK; +} + +#endif // XPCOM_GLUE_AVOID_NSPR + +//----------------------------------------------------------------------------- + +nsresult NS_NewNamedThread(const nsACString& aName, nsIThread** aResult, + nsIRunnable* aInitialEvent, + nsIThreadManager::ThreadCreationOptions aOptions) { + nsCOMPtr event = aInitialEvent; + return NS_NewNamedThread(aName, aResult, event.forget(), aOptions); +} + +nsresult NS_NewNamedThread(const nsACString& aName, nsIThread** aResult, + already_AddRefed aInitialEvent, + nsIThreadManager::ThreadCreationOptions aOptions) { + nsCOMPtr event = std::move(aInitialEvent); + nsCOMPtr thread; + nsresult rv = nsThreadManager::get().nsThreadManager::NewNamedThread( + aName, aOptions, getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + if (event) { + rv = thread->Dispatch(event.forget(), NS_DISPATCH_IGNORE_BLOCK_DISPATCH); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + *aResult = nullptr; + thread.swap(*aResult); + return NS_OK; +} + +nsresult NS_GetCurrentThread(nsIThread** aResult) { + return nsThreadManager::get().nsThreadManager::GetCurrentThread(aResult); +} + +nsresult NS_GetMainThread(nsIThread** aResult) { + return nsThreadManager::get().nsThreadManager::GetMainThread(aResult); +} + +nsresult NS_DispatchToCurrentThread(already_AddRefed&& aEvent) { + nsresult rv; + nsCOMPtr event(aEvent); + // XXX: Consider using GetCurrentSerialEventTarget() to support TaskQueues. + nsISerialEventTarget* thread = NS_GetCurrentThread(); + if (!thread) { + return NS_ERROR_UNEXPECTED; + } + // To keep us from leaking the runnable if dispatch method fails, + // we grab the reference on failures and release it. + nsIRunnable* temp = event.get(); + rv = thread->Dispatch(event.forget(), NS_DISPATCH_NORMAL); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Dispatch() leaked the reference to the event, but due to caller's + // assumptions, we shouldn't leak here. And given we are on the same + // thread as the dispatch target, it's mostly safe to do it here. + NS_RELEASE(temp); + } + return rv; +} + +// It is common to call NS_DispatchToCurrentThread with a newly +// allocated runnable with a refcount of zero. To keep us from leaking +// the runnable if the dispatch method fails, we take a death grip. +nsresult NS_DispatchToCurrentThread(nsIRunnable* aEvent) { + nsCOMPtr event(aEvent); + return NS_DispatchToCurrentThread(event.forget()); +} + +nsresult NS_DispatchToMainThread(already_AddRefed&& aEvent, + uint32_t aDispatchFlags) { + LeakRefPtr event(std::move(aEvent)); + nsCOMPtr thread; + nsresult rv = NS_GetMainThread(getter_AddRefs(thread)); + if (NS_WARN_IF(NS_FAILED(rv))) { + NS_ASSERTION(false, + "Failed NS_DispatchToMainThread() in shutdown; leaking"); + // NOTE: if you stop leaking here, adjust Promise::MaybeReportRejected(), + // which assumes a leak here, or split into leaks and no-leaks versions + return rv; + } + return thread->Dispatch(event.take(), aDispatchFlags); +} + +// In the case of failure with a newly allocated runnable with a +// refcount of zero, we intentionally leak the runnable, because it is +// likely that the runnable is being dispatched to the main thread +// because it owns main thread only objects, so it is not safe to +// release them here. +nsresult NS_DispatchToMainThread(nsIRunnable* aEvent, uint32_t aDispatchFlags) { + nsCOMPtr event(aEvent); + return NS_DispatchToMainThread(event.forget(), aDispatchFlags); +} + +nsresult NS_DelayedDispatchToCurrentThread( + already_AddRefed&& aEvent, uint32_t aDelayMs) { + nsCOMPtr event(aEvent); + + // XXX: Consider using GetCurrentSerialEventTarget() to support TaskQueues. + nsISerialEventTarget* thread = NS_GetCurrentThread(); + if (!thread) { + return NS_ERROR_UNEXPECTED; + } + + return thread->DelayedDispatch(event.forget(), aDelayMs); +} + +nsresult NS_DispatchToThreadQueue(already_AddRefed&& aEvent, + nsIThread* aThread, + EventQueuePriority aQueue) { + nsresult rv; + nsCOMPtr event(aEvent); + NS_ENSURE_TRUE(event, NS_ERROR_INVALID_ARG); + if (!aThread) { + return NS_ERROR_UNEXPECTED; + } + // To keep us from leaking the runnable if dispatch method fails, + // we grab the reference on failures and release it. + nsIRunnable* temp = event.get(); + rv = aThread->DispatchToQueue(event.forget(), aQueue); + if (NS_WARN_IF(NS_FAILED(rv))) { + // Dispatch() leaked the reference to the event, but due to caller's + // assumptions, we shouldn't leak here. And given we are on the same + // thread as the dispatch target, it's mostly safe to do it here. + NS_RELEASE(temp); + } + + return rv; +} + +nsresult NS_DispatchToCurrentThreadQueue(already_AddRefed&& aEvent, + EventQueuePriority aQueue) { + return NS_DispatchToThreadQueue(std::move(aEvent), NS_GetCurrentThread(), + aQueue); +} + +extern nsresult NS_DispatchToMainThreadQueue( + already_AddRefed&& aEvent, EventQueuePriority aQueue) { + nsCOMPtr mainThread; + nsresult rv = NS_GetMainThread(getter_AddRefs(mainThread)); + if (NS_SUCCEEDED(rv)) { + return NS_DispatchToThreadQueue(std::move(aEvent), mainThread, aQueue); + } + return rv; +} + +class IdleRunnableWrapper final : public Runnable, + public nsIDiscardableRunnable, + public nsIIdleRunnable { + public: + explicit IdleRunnableWrapper(already_AddRefed&& aEvent) + : Runnable("IdleRunnableWrapper"), + mRunnable(std::move(aEvent)), + mDiscardable(do_QueryInterface(mRunnable)) {} + + NS_DECL_ISUPPORTS_INHERITED + + NS_IMETHOD Run() override { + if (!mRunnable) { + return NS_OK; + } + CancelTimer(); + // Don't clear mDiscardable because that would cause QueryInterface to + // change behavior during the lifetime of an instance. + nsCOMPtr runnable = std::move(mRunnable); + return runnable->Run(); + } + + // nsIDiscardableRunnable + void OnDiscard() override { + if (!mRunnable) { + // Run() was already called from TimedOut(). + return; + } + mDiscardable->OnDiscard(); + mRunnable = nullptr; + } + + static void TimedOut(nsITimer* aTimer, void* aClosure) { + RefPtr runnable = + static_cast(aClosure); + LogRunnable::Run log(runnable); + runnable->Run(); + runnable = nullptr; + } + + void SetTimer(uint32_t aDelay, nsIEventTarget* aTarget) override { + MOZ_ASSERT(aTarget); + MOZ_ASSERT(!mTimer); + NS_NewTimerWithFuncCallback(getter_AddRefs(mTimer), TimedOut, this, aDelay, + nsITimer::TYPE_ONE_SHOT, + "IdleRunnableWrapper::SetTimer", aTarget); + } + +#ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHOD GetName(nsACString& aName) override { + aName.AssignLiteral("IdleRunnableWrapper"); + if (nsCOMPtr named = do_QueryInterface(mRunnable)) { + nsAutoCString name; + named->GetName(name); + if (!name.IsEmpty()) { + aName.AppendLiteral(" for "); + aName.Append(name); + } + } + return NS_OK; + } +#endif + + private: + ~IdleRunnableWrapper() { CancelTimer(); } + + void CancelTimer() { + if (mTimer) { + mTimer->Cancel(); + } + } + + nsCOMPtr mTimer; + nsCOMPtr mRunnable; + nsCOMPtr mDiscardable; +}; + +NS_IMPL_ADDREF_INHERITED(IdleRunnableWrapper, Runnable) +NS_IMPL_RELEASE_INHERITED(IdleRunnableWrapper, Runnable) + +NS_INTERFACE_MAP_BEGIN(IdleRunnableWrapper) + NS_INTERFACE_MAP_ENTRY(nsIIdleRunnable) + NS_INTERFACE_MAP_ENTRY_CONDITIONAL(nsIDiscardableRunnable, mDiscardable) +NS_INTERFACE_MAP_END_INHERITING(Runnable) + +extern nsresult NS_DispatchToThreadQueue(already_AddRefed&& aEvent, + uint32_t aTimeout, nsIThread* aThread, + EventQueuePriority aQueue) { + nsCOMPtr event(std::move(aEvent)); + NS_ENSURE_TRUE(event, NS_ERROR_INVALID_ARG); + MOZ_ASSERT(aQueue == EventQueuePriority::Idle || + aQueue == EventQueuePriority::DeferredTimers); + if (!aThread) { + return NS_ERROR_UNEXPECTED; + } + + nsCOMPtr idleEvent = do_QueryInterface(event); + + if (!idleEvent) { + idleEvent = new IdleRunnableWrapper(event.forget()); + event = do_QueryInterface(idleEvent); + MOZ_DIAGNOSTIC_ASSERT(event); + } + idleEvent->SetTimer(aTimeout, aThread); + + nsresult rv = NS_DispatchToThreadQueue(event.forget(), aThread, aQueue); + if (NS_SUCCEEDED(rv)) { + // This is intended to bind with the "DISP" log made from inside + // NS_DispatchToThreadQueue for the `event`. There is no possibly to inject + // another "DISP" for a different event on this thread. + LOG1(("TIMEOUT %u", aTimeout)); + } + + return rv; +} + +extern nsresult NS_DispatchToCurrentThreadQueue( + already_AddRefed&& aEvent, uint32_t aTimeout, + EventQueuePriority aQueue) { + return NS_DispatchToThreadQueue(std::move(aEvent), aTimeout, + NS_GetCurrentThread(), aQueue); +} + +#ifndef XPCOM_GLUE_AVOID_NSPR +nsresult NS_ProcessPendingEvents(nsIThread* aThread, PRIntervalTime aTimeout) { + nsresult rv = NS_OK; + + if (!aThread) { + aThread = NS_GetCurrentThread(); + if (NS_WARN_IF(!aThread)) { + return NS_ERROR_UNEXPECTED; + } + } + + PRIntervalTime start = PR_IntervalNow(); + for (;;) { + bool processedEvent; + rv = aThread->ProcessNextEvent(false, &processedEvent); + if (NS_FAILED(rv) || !processedEvent) { + break; + } + if (PR_IntervalNow() - start > aTimeout) { + break; + } + } + return rv; +} +#endif // XPCOM_GLUE_AVOID_NSPR + +inline bool hasPendingEvents(nsIThread* aThread) { + bool val; + return NS_SUCCEEDED(aThread->HasPendingEvents(&val)) && val; +} + +bool NS_HasPendingEvents(nsIThread* aThread) { + if (!aThread) { + aThread = NS_GetCurrentThread(); + if (NS_WARN_IF(!aThread)) { + return false; + } + } + return hasPendingEvents(aThread); +} + +bool NS_ProcessNextEvent(nsIThread* aThread, bool aMayWait) { + if (!aThread) { + aThread = NS_GetCurrentThread(); + if (NS_WARN_IF(!aThread)) { + return false; + } + } + bool val; + return NS_SUCCEEDED(aThread->ProcessNextEvent(aMayWait, &val)) && val; +} + +void NS_SetCurrentThreadName(const char* aName) { + PR_SetCurrentThreadName(aName); +#if defined(ANDROID) && defined(DEBUG) + // Check nspr does the right thing on Android. + char buffer[16] = {'\0'}; + prctl(PR_GET_NAME, buffer); + MOZ_ASSERT(0 == strncmp(buffer, aName, 15)); +#endif + if (nsThreadManager::get().IsNSThread()) { + nsThread* thread = nsThreadManager::get().GetCurrentThread(); + thread->SetThreadNameInternal(nsDependentCString(aName)); + } +} + +nsIThread* NS_GetCurrentThread() { + return nsThreadManager::get().GetCurrentThread(); +} + +nsIThread* NS_GetCurrentThreadNoCreate() { + if (nsThreadManager::get().IsNSThread()) { + return NS_GetCurrentThread(); + } + return nullptr; +} + +// nsThreadPoolNaming +nsCString nsThreadPoolNaming::GetNextThreadName(const nsACString& aPoolName) { + nsCString name(aPoolName); + name.AppendLiteral(" #"); + name.AppendInt(++mCounter, 10); // The counter is declared as atomic + return name; +} + +nsresult NS_DispatchBackgroundTask(already_AddRefed aEvent, + uint32_t aDispatchFlags) { + nsCOMPtr event(aEvent); + return nsThreadManager::get().DispatchToBackgroundThread(event, + aDispatchFlags); +} + +// nsAutoLowPriorityIO +nsAutoLowPriorityIO::nsAutoLowPriorityIO() { +#if defined(XP_WIN) + lowIOPrioritySet = + SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_BEGIN); +#elif defined(XP_MACOSX) + oldPriority = getiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_THREAD); + lowIOPrioritySet = + oldPriority != -1 && + setiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_THREAD, IOPOL_THROTTLE) != -1; +#else + lowIOPrioritySet = false; +#endif +} + +nsAutoLowPriorityIO::~nsAutoLowPriorityIO() { +#if defined(XP_WIN) + if (MOZ_LIKELY(lowIOPrioritySet)) { + // On Windows the old thread priority is automatically restored + SetThreadPriority(GetCurrentThread(), THREAD_MODE_BACKGROUND_END); + } +#elif defined(XP_MACOSX) + if (MOZ_LIKELY(lowIOPrioritySet)) { + setiopolicy_np(IOPOL_TYPE_DISK, IOPOL_SCOPE_THREAD, oldPriority); + } +#endif +} + +namespace mozilla { + +nsISerialEventTarget* GetCurrentSerialEventTarget() { + if (nsISerialEventTarget* current = + SerialEventTargetGuard::GetCurrentSerialEventTarget()) { + return current; + } + + MOZ_DIAGNOSTIC_ASSERT(!nsThreadPool::GetCurrentThreadPool(), + "Call to GetCurrentSerialEventTarget() from thread " + "pool without an active TaskQueue"); + + nsCOMPtr thread; + nsresult rv = NS_GetCurrentThread(getter_AddRefs(thread)); + if (NS_FAILED(rv)) { + return nullptr; + } + + return thread; +} + +nsISerialEventTarget* GetMainThreadSerialEventTarget() { + return static_cast(nsThreadManager::get().GetMainThreadWeak()); +} + +size_t GetNumberOfProcessors() { +#if defined(XP_LINUX) && defined(MOZ_SANDBOX) + static const PRInt32 procs = PR_GetNumberOfProcessors(); +#else + PRInt32 procs = PR_GetNumberOfProcessors(); +#endif + MOZ_ASSERT(procs > 0); + return static_cast(procs); +} + +template +void LogTaskBase::LogDispatch(T* aEvent) { + LOG1(("DISP %p", aEvent)); +} +template +void LogTaskBase::LogDispatch(T* aEvent, void* aContext) { + LOG1(("DISP %p (%p)", aEvent, aContext)); +} + +template <> +void LogTaskBase::LogDispatchWithPid(IPC::Message* aEvent, + int32_t aPid) { + if (aEvent->seqno() && aPid > 0) { + LOG1(("SEND %p %d %d", aEvent, aEvent->seqno(), aPid)); + } +} + +template +LogTaskBase::Run::Run(T* aEvent, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + // Logging address of this RAII so that we can use it to identify the DONE log + // while not keeping any ref to the event that could be invalid at the dtor + // time. + LOG1(("EXEC %p %p", aEvent, this)); +} +template +LogTaskBase::Run::Run(T* aEvent, void* aContext, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + LOG1(("EXEC %p (%p) %p", aEvent, aContext, this)); +} + +template <> +LogTaskBase::Run::Run(nsIRunnable* aEvent, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + if (!LOG1_ENABLED()) { + return; + } + + nsCOMPtr named(do_QueryInterface(aEvent)); + if (!named) { + LOG1(("EXEC %p %p", aEvent, this)); + return; + } + + nsAutoCString name; + named->GetName(name); + LOG1(("EXEC %p %p [%s]", aEvent, this, name.BeginReading())); +} + +template <> +LogTaskBase::Run::Run(Task* aTask, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + if (!LOG1_ENABLED()) { + return; + } + + nsAutoCString name; + if (!aTask->GetName(name)) { + LOG1(("EXEC %p %p", aTask, this)); + return; + } + + LOG1(("EXEC %p %p [%s]", aTask, this, name.BeginReading())); +} + +template <> +LogTaskBase::Run::Run(IPC::Message* aMessage, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + LOG1(("RECV %p %p %d [%s]", aMessage, this, aMessage->seqno(), + aMessage->name())); +} + +template <> +LogTaskBase::Run::Run(nsTimerImpl* aEvent, bool aWillRunAgain) + : mWillRunAgain(aWillRunAgain) { + // The name of the timer will be logged when running it on the target thread. + // Logging it here (on the `Timer` thread) would be redundant. + LOG1(("EXEC %p %p [nsTimerImpl]", aEvent, this)); +} + +template +LogTaskBase::Run::~Run() { + LOG1((mWillRunAgain ? "INTERRUPTED %p" : "DONE %p", this)); +} + +template class LogTaskBase; +template class LogTaskBase; +template class LogTaskBase; +template class LogTaskBase; +template class LogTaskBase; +template class LogTaskBase; +template class LogTaskBase; + +MOZ_THREAD_LOCAL(nsISerialEventTarget*) +SerialEventTargetGuard::sCurrentThreadTLS; +void SerialEventTargetGuard::InitTLS() { + MOZ_ASSERT(NS_IsMainThread()); + if (!sCurrentThreadTLS.init()) { + MOZ_CRASH(); + } +} + +} // namespace mozilla + +bool nsIEventTarget::IsOnCurrentThread() { + if (mThread) { + return mThread == PR_GetCurrentThread(); + } + return IsOnCurrentThreadInfallible(); +} + +extern "C" { +// These functions use the C language linkage because they're exposed to Rust +// via the xpcom/rust/moz_task crate, which wraps them in safe Rust functions +// that enable Rust code to get/create threads and dispatch runnables on them. + +nsresult NS_GetCurrentThreadRust(nsIThread** aResult) { + return NS_GetCurrentThread(aResult); +} + +nsresult NS_GetMainThreadRust(nsIThread** aResult) { + return NS_GetMainThread(aResult); +} + +// NS_NewNamedThread's aStackSize parameter has the default argument +// nsIThreadManager::DEFAULT_STACK_SIZE, but we can't omit default arguments +// when calling a C++ function from Rust, and we can't access +// nsIThreadManager::DEFAULT_STACK_SIZE in Rust to pass it explicitly, +// since it is defined in a %{C++ ... %} block within nsIThreadManager.idl. +// So we indirect through this function. +nsresult NS_NewNamedThreadWithDefaultStackSize(const nsACString& aName, + nsIThread** aResult, + nsIRunnable* aEvent) { + return NS_NewNamedThread(aName, aResult, aEvent); +} + +bool NS_IsOnCurrentThread(nsIEventTarget* aTarget) { + return aTarget->IsOnCurrentThread(); +} + +nsresult NS_DispatchBackgroundTask(nsIRunnable* aEvent, + uint32_t aDispatchFlags) { + return nsThreadManager::get().DispatchToBackgroundThread(aEvent, + aDispatchFlags); +} + +nsresult NS_CreateBackgroundTaskQueue(const char* aName, + nsISerialEventTarget** aTarget) { + nsCOMPtr target = + nsThreadManager::get().CreateBackgroundTaskQueue(aName); + if (!target) { + return NS_ERROR_FAILURE; + } + + target.forget(aTarget); + return NS_OK; +} + +} // extern "C" + +nsresult NS_DispatchAndSpinEventLoopUntilComplete( + const nsACString& aVeryGoodReasonToDoThis, nsIEventTarget* aEventTarget, + already_AddRefed aEvent) { + // NOTE: Get the current thread specifically, as `SpinEventLoopUntil` can + // only spin that event target's loop. The reply will specify + // NS_DISPATCH_IGNORE_BLOCK_DISPATCH to ensure the reply is received even if + // the caller is a threadpool thread. + nsCOMPtr current = NS_GetCurrentThread(); + if (NS_WARN_IF(!current)) { + return NS_ERROR_NOT_AVAILABLE; + } + + RefPtr wrapper = + new nsThreadSyncDispatch(current.forget(), std::move(aEvent)); + nsresult rv = aEventTarget->Dispatch(do_AddRef(wrapper)); + if (NS_WARN_IF(NS_FAILED(rv))) { + // FIXME: Consider avoiding leaking the `nsThreadSyncDispatch` as well by + // using a fallible version of `Dispatch` once that is added. + return rv; + } + + wrapper->SpinEventLoopUntilComplete(aVeryGoodReasonToDoThis); + return NS_OK; +} diff --git a/xpcom/threads/nsThreadUtils.h b/xpcom/threads/nsThreadUtils.h new file mode 100644 index 0000000000..72041da295 --- /dev/null +++ b/xpcom/threads/nsThreadUtils.h @@ -0,0 +1,1925 @@ +/* -*- 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 nsThreadUtils_h__ +#define nsThreadUtils_h__ + +#include +#include +#include + +#include "MainThreadUtils.h" +#include "mozilla/EventQueue.h" +#include "mozilla/AbstractThread.h" +#include "mozilla/Atomics.h" +#include "mozilla/Likely.h" +#include "mozilla/Maybe.h" +#include "mozilla/ThreadLocal.h" +#include "mozilla/TimeStamp.h" + +#include "nsCOMPtr.h" +#include "nsICancelableRunnable.h" +#include "nsIDiscardableRunnable.h" +#include "nsIIdlePeriod.h" +#include "nsIIdleRunnable.h" +#include "nsINamed.h" +#include "nsIRunnable.h" +#include "nsIThreadManager.h" +#include "nsITimer.h" +#include "nsString.h" +#include "prinrval.h" +#include "prthread.h" + +class MessageLoop; +class nsIThread; + +//----------------------------------------------------------------------------- +// These methods are alternatives to the methods on nsIThreadManager, provided +// for convenience. + +/** + * Create a new thread, and optionally provide an initial event for the thread. + * + * @param aName + * The name of the thread. + * @param aResult + * The resulting nsIThread object. + * @param aInitialEvent + * The initial event to run on this thread. This parameter may be null. + * @param aOptions + * Options used to configure thread creation. + * Options are documented in nsIThreadManager.idl. + * + * @returns NS_ERROR_INVALID_ARG + * Indicates that the given name is not unique. + */ + +extern nsresult NS_NewNamedThread( + const nsACString& aName, nsIThread** aResult, + nsIRunnable* aInitialEvent = nullptr, + nsIThreadManager::ThreadCreationOptions aOptions = {}); + +extern nsresult NS_NewNamedThread( + const nsACString& aName, nsIThread** aResult, + already_AddRefed aInitialEvent, + nsIThreadManager::ThreadCreationOptions aOptions = {}); + +template +inline nsresult NS_NewNamedThread( + const char (&aName)[LEN], nsIThread** aResult, + already_AddRefed aInitialEvent, + nsIThreadManager::ThreadCreationOptions aOptions = {}) { + static_assert(LEN <= 16, "Thread name must be no more than 16 characters"); + return NS_NewNamedThread(nsDependentCString(aName, LEN - 1), aResult, + std::move(aInitialEvent), aOptions); +} + +template +inline nsresult NS_NewNamedThread( + const char (&aName)[LEN], nsIThread** aResult, + nsIRunnable* aInitialEvent = nullptr, + nsIThreadManager::ThreadCreationOptions aOptions = {}) { + nsCOMPtr event = aInitialEvent; + static_assert(LEN <= 16, "Thread name must be no more than 16 characters"); + return NS_NewNamedThread(nsDependentCString(aName, LEN - 1), aResult, + event.forget(), aOptions); +} + +/** + * Get a reference to the current thread, creating it if it does not exist yet. + * + * @param aResult + * The resulting nsIThread object. + */ +extern nsresult NS_GetCurrentThread(nsIThread** aResult); + +/** + * Dispatch the given event to the current thread. + * + * @param aEvent + * The event to dispatch. + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + */ +extern nsresult NS_DispatchToCurrentThread(nsIRunnable* aEvent); +extern nsresult NS_DispatchToCurrentThread( + already_AddRefed&& aEvent); + +/** + * Dispatch the given event to the main thread. + * + * @param aEvent + * The event to dispatch. + * @param aDispatchFlags + * The flags to pass to the main thread's dispatch method. + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + */ +extern nsresult NS_DispatchToMainThread( + nsIRunnable* aEvent, uint32_t aDispatchFlags = NS_DISPATCH_NORMAL); +extern nsresult NS_DispatchToMainThread( + already_AddRefed&& aEvent, + uint32_t aDispatchFlags = NS_DISPATCH_NORMAL); + +extern nsresult NS_DelayedDispatchToCurrentThread( + already_AddRefed&& aEvent, uint32_t aDelayMs); + +/** + * Dispatch the given event to the specified queue of the current thread. + * + * @param aEvent The event to dispatch. + * @param aQueue The event queue for the thread to use + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + * @returns NS_ERROR_UNEXPECTED + * If the thread is shutting down. + */ +extern nsresult NS_DispatchToCurrentThreadQueue( + already_AddRefed&& aEvent, mozilla::EventQueuePriority aQueue); + +/** + * Dispatch the given event to the specified queue of the main thread. + * + * @param aEvent The event to dispatch. + * @param aQueue The event queue for the thread to use + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + * @returns NS_ERROR_UNEXPECTED + * If the thread is shutting down. + */ +extern nsresult NS_DispatchToMainThreadQueue( + already_AddRefed&& aEvent, mozilla::EventQueuePriority aQueue); + +/** + * Dispatch the given event to an idle queue of the current thread. + * + * @param aEvent The event to dispatch. If the event implements + * nsIIdleRunnable, it will receive a call on + * nsIIdleRunnable::SetTimer when dispatched, with the value of + * aTimeout. + * + * @param aTimeout The time in milliseconds until the event should be + * moved from an idle queue to the regular queue, if it hasn't been + * executed. If aEvent is also an nsIIdleRunnable, it is expected + * that it should handle the timeout itself, after a call to + * nsIIdleRunnable::SetTimer. + * + * @param aQueue + * The event queue for the thread to use. Must be an idle queue + * (Idle or DeferredTimers) + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + * @returns NS_ERROR_UNEXPECTED + * If the thread is shutting down. + */ +extern nsresult NS_DispatchToCurrentThreadQueue( + already_AddRefed&& aEvent, uint32_t aTimeout, + mozilla::EventQueuePriority aQueue); + +/** + * Dispatch the given event to a queue of a thread. + * + * @param aEvent The event to dispatch. + * @param aThread The target thread for the dispatch. + * @param aQueue The event queue for the thread to use. + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + * @returns NS_ERROR_UNEXPECTED + * If the thread is shutting down. + */ +extern nsresult NS_DispatchToThreadQueue(already_AddRefed&& aEvent, + nsIThread* aThread, + mozilla::EventQueuePriority aQueue); + +/** + * Dispatch the given event to an idle queue of a thread. + * + * @param aEvent The event to dispatch. If the event implements + * nsIIdleRunnable, it will receive a call on + * nsIIdleRunnable::SetTimer when dispatched, with the value of + * aTimeout. + * + * @param aTimeout The time in milliseconds until the event should be + * moved from an idle queue to the regular queue, if it hasn't been + * executed. If aEvent is also an nsIIdleRunnable, it is expected + * that it should handle the timeout itself, after a call to + * nsIIdleRunnable::SetTimer. + * + * @param aThread The target thread for the dispatch. + * + * @param aQueue + * The event queue for the thread to use. Must be an idle queue + * (Idle or DeferredTimers) + * + * @returns NS_ERROR_INVALID_ARG + * If event is null. + * @returns NS_ERROR_UNEXPECTED + * If the thread is shutting down. + */ +extern nsresult NS_DispatchToThreadQueue(already_AddRefed&& aEvent, + uint32_t aTimeout, nsIThread* aThread, + mozilla::EventQueuePriority aQueue); + +#ifndef XPCOM_GLUE_AVOID_NSPR +/** + * Process all pending events for the given thread before returning. This + * method simply calls ProcessNextEvent on the thread while HasPendingEvents + * continues to return true and the time spent in NS_ProcessPendingEvents + * does not exceed the given timeout value. + * + * @param aThread + * The thread object for which to process pending events. If null, then + * events will be processed for the current thread. + * @param aTimeout + * The maximum number of milliseconds to spend processing pending events. + * Events are not pre-empted to honor this timeout. Rather, the timeout + * value is simply used to determine whether or not to process another event. + * Pass PR_INTERVAL_NO_TIMEOUT to specify no timeout. + */ +extern nsresult NS_ProcessPendingEvents( + nsIThread* aThread, PRIntervalTime aTimeout = PR_INTERVAL_NO_TIMEOUT); +#endif + +/** + * Shortcut for nsIThread::HasPendingEvents. + * + * It is an error to call this function when the given thread is not the + * current thread. This function will return false if called from some + * other thread. + * + * @param aThread + * The current thread or null. + * + * @returns + * A boolean value that if "true" indicates that there are pending events + * in the current thread's event queue. + */ +extern bool NS_HasPendingEvents(nsIThread* aThread = nullptr); + +/** + * Shortcut for nsIThread::ProcessNextEvent. + * + * It is an error to call this function when the given thread is not the + * current thread. This function will simply return false if called + * from some other thread. + * + * @param aThread + * The current thread or null. + * @param aMayWait + * A boolean parameter that if "true" indicates that the method may block + * the calling thread to wait for a pending event. + * + * @returns + * A boolean value that if "true" indicates that an event from the current + * thread's event queue was processed. + */ +extern bool NS_ProcessNextEvent(nsIThread* aThread = nullptr, + bool aMayWait = true); + +/** + * Returns true if we're in the compositor thread. + * + * We declare this here because the headers required to invoke + * CompositorThreadHolder::IsInCompositorThread() also pull in a bunch of system + * headers that #define various tokens in a way that can break the build. + */ +extern bool NS_IsInCompositorThread(); + +extern bool NS_IsInCanvasThreadOrWorker(); + +extern bool NS_IsInVRThread(); + +//----------------------------------------------------------------------------- +// Helpers that work with nsCOMPtr: + +inline already_AddRefed do_GetCurrentThread() { + nsIThread* thread = nullptr; + NS_GetCurrentThread(&thread); + return already_AddRefed(thread); +} + +inline already_AddRefed do_GetMainThread() { + nsIThread* thread = nullptr; + NS_GetMainThread(&thread); + return already_AddRefed(thread); +} + +//----------------------------------------------------------------------------- + +// Fast access to the current thread. Will create an nsIThread if one does not +// exist already! Do not release the returned pointer! If you want to use this +// pointer from some other thread, then you will need to AddRef it. Otherwise, +// you should only consider this pointer valid from code running on the current +// thread. +extern nsIThread* NS_GetCurrentThread(); + +// Exactly the same as NS_GetCurrentThread, except it will not create an +// nsThread if one does not exist yet. This is useful in cases where you have +// code that runs on threads that may or may not not be driven by an nsThread +// event loop, and wish to avoid inadvertently creating a superfluous nsThread. +extern nsIThread* NS_GetCurrentThreadNoCreate(); + +/** + * Set the name of the current thread. Prefer this function over + * PR_SetCurrentThreadName() if possible. The name will also be included in the + * crash report. + * + * @param aName + * Name of the thread. A C language null-terminated string. + */ +extern void NS_SetCurrentThreadName(const char* aName); + +//----------------------------------------------------------------------------- + +#ifndef XPCOM_GLUE_AVOID_NSPR + +namespace mozilla { + +// This class is designed to be subclassed. +class IdlePeriod : public nsIIdlePeriod { + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIIDLEPERIOD + + IdlePeriod() = default; + + protected: + virtual ~IdlePeriod() = default; + + private: + IdlePeriod(const IdlePeriod&) = delete; + IdlePeriod& operator=(const IdlePeriod&) = delete; + IdlePeriod& operator=(const IdlePeriod&&) = delete; +}; + +// Cancelable runnable methods implement nsICancelableRunnable, and +// Idle and IdleWithTimer also nsIIdleRunnable. +enum class RunnableKind { Standard, Cancelable, Idle, IdleWithTimer }; + +// Implementing nsINamed on Runnable bloats vtables for the hundreds of +// Runnable subclasses that we have, so we want to avoid that overhead +// when we're not using nsINamed for anything. +# ifndef RELEASE_OR_BETA +# define MOZ_COLLECTING_RUNNABLE_TELEMETRY +# endif + +// This class is designed to be subclassed. +class Runnable : public nsIRunnable +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + , + public nsINamed +# endif +{ + public: + NS_DECL_THREADSAFE_ISUPPORTS + NS_DECL_NSIRUNNABLE +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_DECL_NSINAMED +# endif + + Runnable() = delete; + +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + explicit Runnable(const char* aName) : mName(aName) {} +# else + explicit Runnable(const char* aName) {} +# endif + + protected: + virtual ~Runnable() = default; + +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + const char* mName = nullptr; +# endif + + private: + Runnable(const Runnable&) = delete; + Runnable& operator=(const Runnable&) = delete; + Runnable& operator=(const Runnable&&) = delete; +}; + +// This is a base class for tasks that might not be run, such as those that may +// be dispatched to workers. +// The owner of an event target will call either Run() or OnDiscard() +// exactly once. +// Derived classes should override Run(). An OnDiscard() override may +// provide cleanup when Run() will not be called. +class DiscardableRunnable : public Runnable, public nsIDiscardableRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + // nsIDiscardableRunnable + void OnDiscard() override {} + + DiscardableRunnable() = delete; + explicit DiscardableRunnable(const char* aName) : Runnable(aName) {} + + protected: + virtual ~DiscardableRunnable() = default; + + private: + DiscardableRunnable(const DiscardableRunnable&) = delete; + DiscardableRunnable& operator=(const DiscardableRunnable&) = delete; + DiscardableRunnable& operator=(const DiscardableRunnable&&) = delete; +}; + +// This class is designed to be subclassed. +// Derived classes should override Run() and Cancel() to provide that +// calling Run() after Cancel() is a no-op. +class CancelableRunnable : public DiscardableRunnable, + public nsICancelableRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + // nsIDiscardableRunnable + void OnDiscard() override; + // nsICancelableRunnable + virtual nsresult Cancel() override = 0; + + CancelableRunnable() = delete; + explicit CancelableRunnable(const char* aName) : DiscardableRunnable(aName) {} + + protected: + virtual ~CancelableRunnable() = default; + + private: + CancelableRunnable(const CancelableRunnable&) = delete; + CancelableRunnable& operator=(const CancelableRunnable&) = delete; + CancelableRunnable& operator=(const CancelableRunnable&&) = delete; +}; + +// This class is designed to be subclassed. +class IdleRunnable : public DiscardableRunnable, public nsIIdleRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + + explicit IdleRunnable(const char* aName) : DiscardableRunnable(aName) {} + + protected: + virtual ~IdleRunnable() = default; + + private: + IdleRunnable(const IdleRunnable&) = delete; + IdleRunnable& operator=(const IdleRunnable&) = delete; + IdleRunnable& operator=(const IdleRunnable&&) = delete; +}; + +// This class is designed to be subclassed. +class CancelableIdleRunnable : public CancelableRunnable, + public nsIIdleRunnable { + public: + NS_DECL_ISUPPORTS_INHERITED + + CancelableIdleRunnable() : CancelableRunnable("CancelableIdleRunnable") {} + explicit CancelableIdleRunnable(const char* aName) + : CancelableRunnable(aName) {} + + protected: + virtual ~CancelableIdleRunnable() = default; + + private: + CancelableIdleRunnable(const CancelableIdleRunnable&) = delete; + CancelableIdleRunnable& operator=(const CancelableIdleRunnable&) = delete; + CancelableIdleRunnable& operator=(const CancelableIdleRunnable&&) = delete; +}; + +// This class is designed to be a wrapper of a real runnable to support event +// prioritizable. +class PrioritizableRunnable : public Runnable, public nsIRunnablePriority { + public: + PrioritizableRunnable(already_AddRefed&& aRunnable, + uint32_t aPriority); + +# ifdef MOZ_COLLECTING_RUNNABLE_TELEMETRY + NS_IMETHOD GetName(nsACString& aName) override; +# endif + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLE + NS_DECL_NSIRUNNABLEPRIORITY + + protected: + virtual ~PrioritizableRunnable() = default; + + nsCOMPtr mRunnable; + uint32_t mPriority; +}; + +class PrioritizableCancelableRunnable : public CancelableRunnable, + public nsIRunnablePriority { + public: + PrioritizableCancelableRunnable(uint32_t aPriority, const char* aName) + : CancelableRunnable(aName), mPriority(aPriority) {} + + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_NSIRUNNABLEPRIORITY + + protected: + virtual ~PrioritizableCancelableRunnable() = default; + + const uint32_t mPriority; +}; + +extern already_AddRefed CreateRenderBlockingRunnable( + already_AddRefed&& aRunnable); + +namespace detail { + +// An event that can be used to call a C++11 functions or function objects, +// including lambdas. The function must have no required arguments, and must +// return void. +template +class RunnableFunction : public Runnable { + public: + template + explicit RunnableFunction(const char* aName, F&& aFunction) + : Runnable(aName), mFunction(std::forward(aFunction)) {} + + NS_IMETHOD Run() override { + static_assert(std::is_void_v, + "The lambda must return void!"); + mFunction(); + return NS_OK; + } + + private: + StoredFunction mFunction; +}; + +// Type alias for NS_NewRunnableFunction +template +using RunnableFunctionImpl = + // Make sure we store a non-reference in nsRunnableFunction. + typename detail::RunnableFunction>; +} // namespace detail + +namespace detail { + +template +struct IsRefcountedSmartPointerHelper : std::false_type {}; + +template +struct IsRefcountedSmartPointerHelper> : std::true_type {}; + +template +struct IsRefcountedSmartPointerHelper> : std::true_type {}; + +} // namespace detail + +template +struct IsRefcountedSmartPointer + : detail::IsRefcountedSmartPointerHelper> {}; + +namespace detail { + +template +struct RemoveSmartPointerHelper { + typedef T Type; +}; + +template +struct RemoveSmartPointerHelper> { + typedef Pointee Type; +}; + +template +struct RemoveSmartPointerHelper> { + typedef Pointee Type; +}; + +} // namespace detail + +template +struct RemoveSmartPointer + : detail::RemoveSmartPointerHelper> {}; + +namespace detail { + +template +struct RemoveRawOrSmartPointerHelper { + typedef T Type; +}; + +template +struct RemoveRawOrSmartPointerHelper { + typedef Pointee Type; +}; + +template +struct RemoveRawOrSmartPointerHelper> { + typedef Pointee Type; +}; + +template +struct RemoveRawOrSmartPointerHelper> { + typedef Pointee Type; +}; + +} // namespace detail + +template +struct RemoveRawOrSmartPointer + : detail::RemoveRawOrSmartPointerHelper> {}; + +} // namespace mozilla + +inline nsISupports* ToSupports(mozilla::Runnable* p) { + return static_cast(p); +} + +template +already_AddRefed NS_NewRunnableFunction( + const char* aName, Function&& aFunction) { + // We store a non-reference in RunnableFunction, but still forward aFunction + // to move if possible. + return do_AddRef(new mozilla::detail::RunnableFunctionImpl( + aName, std::forward(aFunction))); +} + +// Creates a new object implementing nsIRunnable and nsICancelableRunnable, +// which runs a given function on Run and clears the stored function object on a +// call to `Cancel` (and thus destroys all objects it holds). +template +already_AddRefed NS_NewCancelableRunnableFunction( + const char* aName, Function&& aFunc) { + class FuncCancelableRunnable final : public mozilla::CancelableRunnable { + public: + static_assert( + std::is_void_v< + decltype(std::declval>()())>); + + NS_INLINE_DECL_REFCOUNTING_INHERITED(FuncCancelableRunnable, + CancelableRunnable) + + explicit FuncCancelableRunnable(const char* aName, Function&& aFunc) + : CancelableRunnable{aName}, + mFunc{mozilla::Some(std::forward(aFunc))} {} + + NS_IMETHOD Run() override { + if (mFunc) { + (*mFunc)(); + } + + return NS_OK; + } + + nsresult Cancel() override { + mFunc.reset(); + return NS_OK; + } + + private: + ~FuncCancelableRunnable() = default; + + mozilla::Maybe> mFunc; + }; + + return mozilla::MakeAndAddRef( + aName, std::forward(aFunc)); +} + +namespace mozilla { +namespace detail { + +template +class TimerBehaviour { + public: + nsITimer* GetTimer() { return nullptr; } + void CancelTimer() {} + + protected: + ~TimerBehaviour() = default; +}; + +template <> +class TimerBehaviour { + public: + nsITimer* GetTimer() { + if (!mTimer) { + mTimer = NS_NewTimer(); + } + + return mTimer; + } + + void CancelTimer() { + if (mTimer) { + mTimer->Cancel(); + } + } + + protected: + ~TimerBehaviour() { CancelTimer(); } + + private: + nsCOMPtr mTimer; +}; + +} // namespace detail +} // namespace mozilla + +// An event that can be used to call a method on a class. The class type must +// support reference counting. This event supports Revoke for use +// with nsRevocableEventPtr. +template +class nsRunnableMethod + : public std::conditional_t< + Kind == mozilla::RunnableKind::Standard, mozilla::Runnable, + std::conditional_t>, + protected mozilla::detail::TimerBehaviour { + using BaseType = std::conditional_t< + Kind == mozilla::RunnableKind::Standard, mozilla::Runnable, + std::conditional_t>; + + public: + nsRunnableMethod(const char* aName) : BaseType(aName) {} + + virtual void Revoke() = 0; + + // These ReturnTypeEnforcer classes disallow return types that + // we know are not safe. The default ReturnTypeEnforcer compiles just fine but + // already_AddRefed will not. + template + class ReturnTypeEnforcer { + public: + typedef int ReturnTypeIsSafe; + }; + + template + class ReturnTypeEnforcer> { + // No ReturnTypeIsSafe makes this illegal! + }; + + // Make sure this return type is safe. + typedef typename ReturnTypeEnforcer::ReturnTypeIsSafe check; +}; + +template +struct nsRunnableMethodReceiver { + RefPtr mObj; + explicit nsRunnableMethodReceiver(ClassType* aObj) : mObj(aObj) {} + explicit nsRunnableMethodReceiver(RefPtr&& aObj) + : mObj(std::move(aObj)) {} + ~nsRunnableMethodReceiver() { Revoke(); } + ClassType* Get() const { return mObj.get(); } + void Revoke() { mObj = nullptr; } +}; + +template +struct nsRunnableMethodReceiver { + ClassType* MOZ_NON_OWNING_REF mObj; + explicit nsRunnableMethodReceiver(ClassType* aObj) : mObj(aObj) {} + ClassType* Get() const { return mObj; } + void Revoke() { mObj = nullptr; } +}; + +static inline constexpr bool IsIdle(mozilla::RunnableKind aKind) { + return aKind == mozilla::RunnableKind::Idle || + aKind == mozilla::RunnableKind::IdleWithTimer; +} + +template +struct nsRunnableMethodTraits; + +template +struct nsRunnableMethodTraits { + typedef typename mozilla::RemoveRawOrSmartPointer::Type class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; + +template +struct nsRunnableMethodTraits { + typedef const typename mozilla::RemoveRawOrSmartPointer::Type + class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; + +# ifdef NS_HAVE_STDCALL +template +struct nsRunnableMethodTraits { + typedef typename mozilla::RemoveRawOrSmartPointer::Type class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; + +template +struct nsRunnableMethodTraits { + typedef typename mozilla::RemoveRawOrSmartPointer::Type class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; + +template +struct nsRunnableMethodTraits { + typedef const typename mozilla::RemoveRawOrSmartPointer::Type + class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; + +template +struct nsRunnableMethodTraits { + typedef const typename mozilla::RemoveRawOrSmartPointer::Type + class_type; + static_assert(std::is_base_of::value, + "Stored class must inherit from method's class"); + typedef R return_type; + typedef nsRunnableMethod base_type; + static const bool can_cancel = Kind == mozilla::RunnableKind::Cancelable; +}; +# endif + +// IsParameterStorageClass::value is true if T is a parameter-storage class +// that will be recognized by NS_New[NonOwning]RunnableMethodWithArg[s] to +// force a specific storage&passing strategy (instead of inferring one, +// see ParameterStorage). +// When creating a new storage class, add a specialization for it to be +// recognized. +template +struct IsParameterStorageClass : public std::false_type {}; + +// StoreXPassByY structs used to inform nsRunnableMethodArguments how to +// store arguments, and how to pass them to the target method. + +template +struct StoreCopyPassByValue { + using stored_type = std::decay_t; + typedef stored_type passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByValue(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StoreCopyPassByConstLRef { + using stored_type = std::decay_t; + typedef const stored_type& passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByConstLRef(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StoreCopyPassByLRef { + using stored_type = std::decay_t; + typedef stored_type& passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByLRef(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> : public std::true_type { +}; + +template +struct StoreCopyPassByRRef { + using stored_type = std::decay_t; + typedef stored_type&& passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByRRef(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return std::move(m); } +}; +template +struct IsParameterStorageClass> : public std::true_type { +}; + +template +struct StoreRefPassByLRef { + typedef T& stored_type; + typedef T& passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreRefPassByLRef(A& a) : m(a) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> : public std::true_type { +}; + +template +struct StoreConstRefPassByConstLRef { + typedef const T& stored_type; + typedef const T& passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreConstRefPassByConstLRef(const A& a) : m(a) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StoreRefPtrPassByPtr { + typedef RefPtr stored_type; + typedef T* passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreRefPtrPassByPtr(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return m.get(); } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StorePtrPassByPtr { + typedef T* stored_type; + typedef T* passed_type; + stored_type m; + template + MOZ_IMPLICIT StorePtrPassByPtr(A a) : m(a) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> : public std::true_type {}; + +template +struct StoreConstPtrPassByConstPtr { + typedef const T* stored_type; + typedef const T* passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreConstPtrPassByConstPtr(A a) : m(a) {} + passed_type PassAsParameter() { return m; } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StoreCopyPassByConstPtr { + typedef T stored_type; + typedef const T* passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByConstPtr(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return &m; } +}; +template +struct IsParameterStorageClass> + : public std::true_type {}; + +template +struct StoreCopyPassByPtr { + typedef T stored_type; + typedef T* passed_type; + stored_type m; + template + MOZ_IMPLICIT StoreCopyPassByPtr(A&& a) : m(std::forward(a)) {} + passed_type PassAsParameter() { return &m; } +}; +template +struct IsParameterStorageClass> : public std::true_type { +}; + +namespace detail { + +template +struct SFINAE1True : std::true_type {}; + +template +static auto HasRefCountMethodsTest(int) + -> SFINAE1True().AddRef(), + std::declval().Release())>; +template +static auto HasRefCountMethodsTest(long) -> std::false_type; + +template +struct HasRefCountMethods : decltype(HasRefCountMethodsTest(0)) {}; + +template +struct NonnsISupportsPointerStorageClass + : std::conditional< + std::is_const_v, + StoreConstPtrPassByConstPtr>, + StorePtrPassByPtr> { + using Type = typename NonnsISupportsPointerStorageClass::conditional::type; +}; + +template +struct PointerStorageClass + : std::conditional< + HasRefCountMethods::value, + StoreRefPtrPassByPtr, + typename NonnsISupportsPointerStorageClass::Type> { + using Type = typename PointerStorageClass::conditional::type; +}; + +template +struct LValueReferenceStorageClass + : std::conditional< + std::is_const_v, + StoreConstRefPassByConstLRef>, + StoreRefPassByLRef> { + using Type = typename LValueReferenceStorageClass::conditional::type; +}; + +template +struct SmartPointerStorageClass + : std::conditional< + mozilla::IsRefcountedSmartPointer::value, + StoreRefPtrPassByPtr::Type>, + StoreCopyPassByConstLRef> { + using Type = typename SmartPointerStorageClass::conditional::type; +}; + +template +struct NonLValueReferenceStorageClass + : std::conditional, + StoreCopyPassByRRef>, + typename SmartPointerStorageClass::Type> { + using Type = typename NonLValueReferenceStorageClass::conditional::type; +}; + +template +struct NonPointerStorageClass + : std::conditional, + typename LValueReferenceStorageClass< + std::remove_reference_t>::Type, + typename NonLValueReferenceStorageClass::Type> { + using Type = typename NonPointerStorageClass::conditional::type; +}; + +template +struct NonParameterStorageClass + : std::conditional< + std::is_pointer_v, + typename PointerStorageClass>::Type, + typename NonPointerStorageClass::Type> { + using Type = typename NonParameterStorageClass::conditional::type; +}; + +// Choose storage&passing strategy based on preferred storage type: +// - If IsParameterStorageClass::value is true, use as-is. +// - RC* -> StoreRefPtrPassByPtr :Store RefPtr, pass RC* +// ^^ RC quacks like a ref-counted type (i.e., has AddRef and Release methods) +// - const T* -> StoreConstPtrPassByConstPtr :Store const T*, pass const T* +// - T* -> StorePtrPassByPtr :Store T*, pass T*. +// - const T& -> StoreConstRefPassByConstLRef:Store const T&, pass const T&. +// - T& -> StoreRefPassByLRef :Store T&, pass T&. +// - T&& -> StoreCopyPassByRRef :Store T, pass std::move(T). +// - RefPtr, nsCOMPtr +// -> StoreRefPtrPassByPtr :Store RefPtr, pass T* +// - Other T -> StoreCopyPassByConstLRef :Store T, pass const T&. +// Other available explicit options: +// - StoreCopyPassByValue :Store T, pass T. +// - StoreCopyPassByLRef :Store T, pass T& (of copy!) +// - StoreCopyPassByConstPtr :Store T, pass const T* +// - StoreCopyPassByPtr :Store T, pass T* (of copy!) +// Or create your own class with PassAsParameter() method, optional +// clean-up in destructor, and with associated IsParameterStorageClass<>. +template +struct ParameterStorage + : std::conditional::value, T, + typename NonParameterStorageClass::Type> { + using Type = typename ParameterStorage::conditional::type; +}; + +template +static auto HasSetDeadlineTest(int) + -> SFINAE1True().SetDeadline( + std::declval()))>; + +template +static auto HasSetDeadlineTest(long) -> std::false_type; + +template +struct HasSetDeadline : decltype(HasSetDeadlineTest(0)) {}; + +template +std::enable_if_t<::detail::HasSetDeadline::value> SetDeadlineImpl( + T* aObj, mozilla::TimeStamp aTimeStamp) { + aObj->SetDeadline(aTimeStamp); +} + +template +std::enable_if_t::value> SetDeadlineImpl( + T* aObj, mozilla::TimeStamp aTimeStamp) {} +} /* namespace detail */ + +namespace mozilla { +namespace detail { + +// struct used to store arguments and later apply them to a method. +template +struct RunnableMethodArguments final { + std::tuple::Type...> mArguments; + template + explicit RunnableMethodArguments(As&&... aArguments) + : mArguments(std::forward(aArguments)...) {} + template + decltype(auto) apply(C* o, M m) { + return std::apply( + [&o, m](auto&&... args) { + return ((*o).*m)(args.PassAsParameter()...); + }, + mArguments); + } +}; + +template +class RunnableMethodImpl final + : public ::nsRunnableMethodTraits::base_type { + typedef typename ::nsRunnableMethodTraits + Traits; + + typedef typename Traits::class_type ClassType; + typedef typename Traits::base_type BaseType; + ::nsRunnableMethodReceiver mReceiver; + Method mMethod; + RunnableMethodArguments mArgs; + using BaseType::CancelTimer; + using BaseType::GetTimer; + + private: + virtual ~RunnableMethodImpl() { Revoke(); }; + static void TimedOut(nsITimer* aTimer, void* aClosure) { + static_assert(IsIdle(Kind), "Don't use me!"); + RefPtr r = + static_cast(aClosure); + r->SetDeadline(TimeStamp()); + r->Run(); + r->Cancel(); + } + + public: + template + explicit RunnableMethodImpl(const char* aName, ForwardedPtrType&& aObj, + Method aMethod, Args&&... aArgs) + : BaseType(aName), + mReceiver(std::forward(aObj)), + mMethod(aMethod), + mArgs(std::forward(aArgs)...) { + static_assert(sizeof...(Storages) == sizeof...(Args), + "Storages and Args should have equal sizes"); + } + + NS_IMETHOD Run() { + CancelTimer(); + + if (MOZ_LIKELY(mReceiver.Get())) { + mArgs.apply(mReceiver.Get(), mMethod); + } + + return NS_OK; + } + + nsresult Cancel() { + static_assert(Kind >= RunnableKind::Cancelable, "Don't use me!"); + Revoke(); + return NS_OK; + } + + void Revoke() { + CancelTimer(); + mReceiver.Revoke(); + } + + void SetDeadline(TimeStamp aDeadline) { + if (MOZ_LIKELY(mReceiver.Get())) { + ::detail::SetDeadlineImpl(mReceiver.Get(), aDeadline); + } + } + + void SetTimer(uint32_t aDelay, nsIEventTarget* aTarget) { + MOZ_ASSERT(aTarget); + + if (nsCOMPtr timer = GetTimer()) { + timer->Cancel(); + timer->SetTarget(aTarget); + timer->InitWithNamedFuncCallback(TimedOut, this, aDelay, + nsITimer::TYPE_ONE_SHOT, + "detail::RunnableMethodImpl::SetTimer"); + } + } +}; + +// Type aliases for NewRunnableMethod. +template +using OwningRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + true, RunnableKind::Standard>::base_type; +template +using OwningRunnableMethodImpl = + RunnableMethodImpl, Method, true, + RunnableKind::Standard, Storages...>; + +// Type aliases for NewCancelableRunnableMethod. +template +using CancelableRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + true, + RunnableKind::Cancelable>::base_type; +template +using CancelableRunnableMethodImpl = + RunnableMethodImpl, Method, true, + RunnableKind::Cancelable, Storages...>; + +// Type aliases for NewIdleRunnableMethod. +template +using IdleRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + true, RunnableKind::Idle>::base_type; +template +using IdleRunnableMethodImpl = + RunnableMethodImpl, Method, true, + RunnableKind::Idle, Storages...>; + +// Type aliases for NewIdleRunnableMethodWithTimer. +template +using IdleRunnableMethodWithTimer = + typename ::nsRunnableMethodTraits, Method, + true, + RunnableKind::IdleWithTimer>::base_type; +template +using IdleRunnableMethodWithTimerImpl = + RunnableMethodImpl, Method, true, + RunnableKind::IdleWithTimer, Storages...>; + +// Type aliases for NewNonOwningRunnableMethod. +template +using NonOwningRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + false, RunnableKind::Standard>::base_type; +template +using NonOwningRunnableMethodImpl = + RunnableMethodImpl, Method, false, + RunnableKind::Standard, Storages...>; + +// Type aliases for NonOwningCancelableRunnableMethod +template +using NonOwningCancelableRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + false, + RunnableKind::Cancelable>::base_type; +template +using NonOwningCancelableRunnableMethodImpl = + RunnableMethodImpl, Method, false, + RunnableKind::Cancelable, Storages...>; + +// Type aliases for NonOwningIdleRunnableMethod +template +using NonOwningIdleRunnableMethod = + typename ::nsRunnableMethodTraits, Method, + false, RunnableKind::Idle>::base_type; +template +using NonOwningIdleRunnableMethodImpl = + RunnableMethodImpl, Method, false, + RunnableKind::Idle, Storages...>; + +// Type aliases for NewIdleRunnableMethodWithTimer. +template +using NonOwningIdleRunnableMethodWithTimer = + typename ::nsRunnableMethodTraits, Method, + false, + RunnableKind::IdleWithTimer>::base_type; +template +using NonOwningIdleRunnableMethodWithTimerImpl = + RunnableMethodImpl, Method, false, + RunnableKind::IdleWithTimer, Storages...>; + +} // namespace detail + +// NewRunnableMethod and friends +// +// Very often in Gecko, you'll find yourself in a situation where you want +// to invoke a method (with or without arguments) asynchronously. You +// could write a small helper class inheriting from nsRunnable to handle +// all these details, or you could let NewRunnableMethod take care of all +// those details for you. +// +// The simplest use of NewRunnableMethod looks like: +// +// nsCOMPtr event = +// mozilla::NewRunnableMethod("description", myObject, +// &MyClass::HandleEvent); +// NS_DispatchToCurrentThread(event); +// +// Statically enforced constraints: +// - myObject must be of (or implicitly convertible to) type MyClass +// - MyClass must define AddRef and Release methods +// +// The "description" string should specify a human-readable name for the +// runnable; the provided string is used by various introspection tools +// in the browser. +// +// The created runnable will take a strong reference to `myObject`. For +// non-refcounted objects, or refcounted objects with unusual refcounting +// requirements, and if and only if you are 110% certain that `myObject` +// will live long enough, you can use NewNonOwningRunnableMethod instead, +// which will, as its name implies, take a non-owning reference. If you +// find yourself having to use this function, you should accompany your use +// with a proof comment describing why the runnable will not lead to +// use-after-frees. +// +// (If you find yourself writing contorted code to Release() an object +// asynchronously on a different thread, you should use the +// NS_ProxyRelease function.) +// +// Invoking a method with arguments takes a little more care. The +// natural extension of the above: +// +// nsCOMPtr event = +// mozilla::NewRunnableMethod("description", myObject, +// &MyClass::HandleEvent, +// arg1, arg2, ...); +// +// can lead to security hazards (e.g. passing in raw pointers to refcounted +// objects and storing those raw pointers in the runnable). We therefore +// require you to specify the storage types used by the runnable, just as +// you would if you were writing out the class by hand: +// +// nsCOMPtr event = +// mozilla::NewRunnableMethod, nsTArray> +// ("description", myObject, &MyClass::HandleEvent, arg1, arg2); +// +// Please note that you do not have to pass the same argument type as you +// specify in the template arguments. For example, if you want to transfer +// ownership to a runnable, you can write: +// +// RefPtr ptr = ...; +// nsTArray array = ...; +// nsCOMPtr event = +// mozilla::NewRunnableMethod, nsTArray> +// ("description", myObject, &MyClass::DoSomething, +// std::move(ptr), std::move(array)); +// +// and there will be no extra AddRef/Release traffic, or copying of the array. +// +// Each type that you specify as a template argument to NewRunnableMethod +// comes with its own style of storage in the runnable and its own style +// of argument passing to the invoked method. See the comment for +// ParameterStorage above for more details. +// +// If you need to customize the storage type and/or argument passing type, +// you can write your own class to use as a template argument to +// NewRunnableMethod. If you find yourself having to do that frequently, +// please file a bug in Core::XPCOM about adding the custom type to the +// core code in this file, and/or for custom rules for ParameterStorage +// to select that strategy. +// +// For places that require you to use cancelable runnables, such as +// workers, there's also NewCancelableRunnableMethod and its non-owning +// counterpart. The runnables returned by these methods additionally +// implement nsICancelableRunnable. +// +// Finally, all of the functions discussed above have additional overloads +// that do not take a `const char*` as their first parameter; you may see +// these in older code. The `const char*` overload is preferred and +// should be used in new code exclusively. + +template +already_AddRefed> +NewRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod) { + return do_AddRef(new detail::OwningRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewCancelableRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod) { + return do_AddRef(new detail::CancelableRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewIdleRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod) { + return do_AddRef(new detail::IdleRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewIdleRunnableMethodWithTimer(const char* aName, PtrType&& aPtr, + Method aMethod) { + return do_AddRef(new detail::IdleRunnableMethodWithTimerImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewNonOwningRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod) { + return do_AddRef(new detail::NonOwningRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewNonOwningCancelableRunnableMethod(const char* aName, PtrType&& aPtr, + Method aMethod) { + return do_AddRef( + new detail::NonOwningCancelableRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewNonOwningIdleRunnableMethod(const char* aName, PtrType&& aPtr, + Method aMethod) { + return do_AddRef(new detail::NonOwningIdleRunnableMethodImpl( + aName, std::forward(aPtr), aMethod)); +} + +template +already_AddRefed> +NewNonOwningIdleRunnableMethodWithTimer(const char* aName, PtrType&& aPtr, + Method aMethod) { + return do_AddRef( + new detail::NonOwningIdleRunnableMethodWithTimerImpl( + aName, std::forward(aPtr), aMethod)); +} + +// Similar to NewRunnableMethod. Call like so: +// nsCOMPtr event = +// NewRunnableMethod(myObject, &MyClass::HandleEvent, myArg1,...); +// 'Types' are the stored type for each argument, see ParameterStorage for +// details. +template +already_AddRefed> +NewRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod, + Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::OwningRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +template +already_AddRefed> +NewNonOwningRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod, + Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::NonOwningRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +template +already_AddRefed> +NewCancelableRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod, + Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::CancelableRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +template +already_AddRefed> +NewNonOwningCancelableRunnableMethod(const char* aName, PtrType&& aPtr, + Method aMethod, Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::NonOwningCancelableRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +template +already_AddRefed> +NewIdleRunnableMethod(const char* aName, PtrType&& aPtr, Method aMethod, + Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::IdleRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +template +already_AddRefed> +NewNonOwningIdleRunnableMethod(const char* aName, PtrType&& aPtr, + Method aMethod, Args&&... aArgs) { + static_assert(sizeof...(Storages) == sizeof...(Args), + " size should be equal to number of arguments"); + return do_AddRef( + new detail::NonOwningIdleRunnableMethodImpl( + aName, std::forward(aPtr), aMethod, + std::forward(aArgs)...)); +} + +} // namespace mozilla + +#endif // XPCOM_GLUE_AVOID_NSPR + +// This class is designed to be used when you have an event class E that has a +// pointer back to resource class R. If R goes away while E is still pending, +// then it is important to "revoke" E so that it does not try use R after R has +// been destroyed. nsRevocableEventPtr makes it easy for R to manage such +// situations: +// +// class R; +// +// class E : public mozilla::Runnable { +// public: +// void Revoke() { +// mResource = nullptr; +// } +// private: +// R *mResource; +// }; +// +// class R { +// public: +// void EventHandled() { +// mEvent.Forget(); +// } +// private: +// nsRevocableEventPtr mEvent; +// }; +// +// void R::PostEvent() { +// // Make sure any pending event is revoked. +// mEvent->Revoke(); +// +// nsCOMPtr event = new E(); +// if (NS_SUCCEEDED(NS_DispatchToCurrentThread(event))) { +// // Keep pointer to event so we can revoke it. +// mEvent = event; +// } +// } +// +// NS_IMETHODIMP E::Run() { +// if (!mResource) +// return NS_OK; +// ... +// mResource->EventHandled(); +// return NS_OK; +// } +// +template +class nsRevocableEventPtr { + public: + nsRevocableEventPtr() : mEvent(nullptr) {} + ~nsRevocableEventPtr() { Revoke(); } + + const nsRevocableEventPtr& operator=(RefPtr&& aEvent) { + if (mEvent != aEvent) { + Revoke(); + mEvent = std::move(aEvent); + } + return *this; + } + + void Revoke() { + if (mEvent) { + mEvent->Revoke(); + mEvent = nullptr; + } + } + + void Forget() { mEvent = nullptr; } + bool IsPending() { return mEvent != nullptr; } + T* get() { return mEvent; } + + private: + // Not implemented + nsRevocableEventPtr(const nsRevocableEventPtr&); + nsRevocableEventPtr& operator=(const nsRevocableEventPtr&); + + RefPtr mEvent; +}; + +template +inline already_AddRefed do_AddRef(nsRevocableEventPtr& aObj) { + return do_AddRef(aObj.get()); +} + +/** + * A simple helper to suffix thread pool name + * with incremental numbers. + */ +class nsThreadPoolNaming { + public: + nsThreadPoolNaming() = default; + + /** + * Returns a thread name as " #" and increments the counter. + */ + nsCString GetNextThreadName(const nsACString& aPoolName); + + template + nsCString GetNextThreadName(const char (&aPoolName)[LEN]) { + return GetNextThreadName(nsDependentCString(aPoolName, LEN - 1)); + } + + private: + mozilla::Atomic mCounter{0}; + + nsThreadPoolNaming(const nsThreadPoolNaming&) = delete; + void operator=(const nsThreadPoolNaming&) = delete; +}; + +/** + * Thread priority in most operating systems affect scheduling, not IO. This + * helper is used to set the current thread to low IO priority for the lifetime + * of the created object. You can only use this low priority IO setting within + * the context of the current thread. + */ +class MOZ_STACK_CLASS nsAutoLowPriorityIO { + public: + nsAutoLowPriorityIO(); + ~nsAutoLowPriorityIO(); + + private: + bool lowIOPrioritySet; +#if defined(XP_MACOSX) + int oldPriority; +#endif +}; + +void NS_SetMainThread(); + +// Used only on cooperatively scheduled "main" threads. Causes the thread to be +// considered a main thread and also causes GetCurrentVirtualThread to return +// aVirtualThread. +void NS_SetMainThread(PRThread* aVirtualThread); + +// Used only on cooperatively scheduled "main" threads. Causes the thread to no +// longer be considered a main thread. Also causes GetCurrentVirtualThread() to +// return a unique value. +void NS_UnsetMainThread(); + +/** + * Return the expiration time of the next timer to run on the current + * thread. If that expiration time is greater than aDefault, then + * return aDefault. aSearchBound specifies a maximum number of timers + * to examine to find a timer on the current thread. If no timer that + * will run on the current thread is found after examining + * aSearchBound timers, return the highest seen expiration time as a + * best effort guess. + * + * Timers with either the type nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY or + * nsITIMER::TYPE_REPEATING_SLACK_LOW_PRIORITY will be skipped when + * searching for the next expiration time. This enables timers to + * have lower priority than callbacks dispatched from + * nsIThread::IdleDispatch. + */ +extern mozilla::TimeStamp NS_GetTimerDeadlineHintOnCurrentThread( + mozilla::TimeStamp aDefault, uint32_t aSearchBound); + +/** + * Dispatches the given event to a background thread. The primary benefit of + * this API is that you do not have to manage the lifetime of your own thread + * for running your own events; the thread manager will take care of the + * background thread's lifetime. Not having to manage your own thread also + * means less resource usage, as the underlying implementation here can manage + * spinning up and shutting down threads appropriately. + * + * NOTE: there is no guarantee that events dispatched via these APIs are run + * serially, in dispatch order; several dispatched events may run in parallel. + * If you depend on serial execution of dispatched events, you should use + * NS_CreateBackgroundTaskQueue instead, and dispatch events to the returned + * event target. + */ +extern nsresult NS_DispatchBackgroundTask( + already_AddRefed aEvent, + uint32_t aDispatchFlags = NS_DISPATCH_NORMAL); +extern "C" nsresult NS_DispatchBackgroundTask( + nsIRunnable* aEvent, uint32_t aDispatchFlags = NS_DISPATCH_NORMAL); + +/** + * Obtain a new serial event target that dispatches runnables to a background + * thread. In many cases, this is a straight replacement for creating your + * own, private thread, and is generally preferred to creating your own, + * private thread. + */ +extern "C" nsresult NS_CreateBackgroundTaskQueue( + const char* aName, nsISerialEventTarget** aTarget); + +/** + * Dispatch the given runnable to the given event target, spinning the current + * thread's event loop until the runnable has finished executing. + * + * This is roughly equivalent to the previously-supported `NS_DISPATCH_SYNC` + * flag. + */ +extern nsresult NS_DispatchAndSpinEventLoopUntilComplete( + const nsACString& aVeryGoodReasonToDoThis, nsIEventTarget* aEventTarget, + already_AddRefed aEvent); + +// Predeclaration for logging function below +namespace IPC { +class Message; +class MessageReader; +class MessageWriter; +} // namespace IPC + +class nsTimerImpl; + +namespace mozilla { + +// RAII class that will set the TLS entry to return the currently running +// nsISerialEventTarget. +// It should be used from inner event loop implementation. +class SerialEventTargetGuard { + public: + explicit SerialEventTargetGuard(nsISerialEventTarget* aThread) + : mLastCurrentThread(sCurrentThreadTLS.get()) { + Set(aThread); + } + + ~SerialEventTargetGuard() { sCurrentThreadTLS.set(mLastCurrentThread); } + + static void InitTLS(); + static nsISerialEventTarget* GetCurrentSerialEventTarget() { + return sCurrentThreadTLS.get(); + } + + protected: + friend class ::MessageLoop; + static void Set(nsISerialEventTarget* aThread) { + MOZ_ASSERT(aThread->IsOnCurrentThread()); + sCurrentThreadTLS.set(aThread); + } + + private: + static MOZ_THREAD_LOCAL(nsISerialEventTarget*) sCurrentThreadTLS; + nsISerialEventTarget* mLastCurrentThread; +}; + +// Get the serial event target corresponding to the currently executing task +// queue or thread. This method will assert if called on a thread pool without +// an active task queue. +// +// This function should generally be preferred over NS_GetCurrentThread since it +// will return a more useful answer when called from a task queue running on a +// thread pool or on a non-xpcom thread which accepts runnable dispatches. +// +// NOTE: The returned nsISerialEventTarget may not accept runnable dispatches +// (e.g. if it corresponds to a non-xpcom thread), however it may still be used +// to check if you're on the given thread/queue using IsOnCurrentThread(). + +nsISerialEventTarget* GetCurrentSerialEventTarget(); + +// Get a weak reference to a serial event target which can be used to dispatch +// runnables to the main thread. +// +// NOTE: While this is currently a weak pointer to the nsIThread* returned from +// NS_GetMainThread(), this may change in the future. + +nsISerialEventTarget* GetMainThreadSerialEventTarget(); + +// Returns the number of CPUs, like PR_GetNumberOfProcessors, except +// that it can return a cached value on platforms where sandboxing +// would prevent reading the current value (currently Linux). CPU +// hotplugging is uncommon, so this is unlikely to make a difference +// in practice. +size_t GetNumberOfProcessors(); + +/** + * A helper class to log tasks dispatch and run with "MOZ_LOG=events:1". The + * output is more machine readable and creates a link between dispatch and run. + * + * Usage example for the concrete template type nsIRunnable. + * To log a dispatch, which means putting an event to a queue: + * LogRunnable::LogDispatch(event); + * theQueue.putEvent(event); + * + * To log execution (running) of the event: + * nsCOMPtr event = theQueue.popEvent(); + * { + * LogRunnable::Run log(event); + * event->Run(); + * event = null; // to include the destructor code in the span + * } + * + * The class is a template so that we can support various specific super-types + * of tasks in the future. We can't use void* because it may cast differently + * and tracking the pointer in logs would then be impossible. + */ +template +class LogTaskBase { + public: + LogTaskBase() = delete; + + // Adds a simple log about dispatch of this runnable. + static void LogDispatch(T* aEvent); + // The `aContext` pointer adds another uniqe identifier, nothing more + static void LogDispatch(T* aEvent, void* aContext); + + // Logs dispatch of the message and along that also the PID of the target + // proccess, purposed for uniquely identifying IPC messages. + static void LogDispatchWithPid(T* aEvent, int32_t aPid); + + // This is designed to surround a call to `Run()` or any code representing + // execution of the task body. + // The constructor adds a simple log about start of the runnable execution and + // the destructor adds a log about ending the execution. + class MOZ_RAII Run { + public: + Run() = delete; + explicit Run(T* aEvent, bool aWillRunAgain = false); + explicit Run(T* aEvent, void* aContext, bool aWillRunAgain = false); + ~Run(); + + // When this is called, the log in this RAII dtor will only say + // "interrupted" expecting that the event will run again. + void WillRunAgain() { mWillRunAgain = true; } + + private: + bool mWillRunAgain = false; + }; +}; + +class MicroTaskRunnable; +class Task; // TaskController +class PresShell; +namespace dom { +class FrameRequestCallback; +} // namespace dom + +// Specialized methods must be explicitly predeclared. +template <> +LogTaskBase::Run::Run(nsIRunnable* aEvent, bool aWillRunAgain); +template <> +LogTaskBase::Run::Run(Task* aTask, bool aWillRunAgain); +template <> +void LogTaskBase::LogDispatchWithPid(IPC::Message* aEvent, + int32_t aPid); +template <> +LogTaskBase::Run::Run(IPC::Message* aMessage, bool aWillRunAgain); +template <> +LogTaskBase::Run::Run(nsTimerImpl* aEvent, bool aWillRunAgain); + +typedef LogTaskBase LogRunnable; +typedef LogTaskBase LogMicroTaskRunnable; +typedef LogTaskBase LogIPCMessage; +typedef LogTaskBase LogTimerEvent; +typedef LogTaskBase LogTask; +typedef LogTaskBase LogPresShellObserver; +typedef LogTaskBase LogFrameRequestCallback; +// If you add new types don't forget to add: +// `template class LogTaskBase;` to nsThreadUtils.cpp + +} // namespace mozilla + +#endif // nsThreadUtils_h__ diff --git a/xpcom/threads/nsTimerImpl.cpp b/xpcom/threads/nsTimerImpl.cpp new file mode 100644 index 0000000000..b350e9c0a9 --- /dev/null +++ b/xpcom/threads/nsTimerImpl.cpp @@ -0,0 +1,822 @@ +/* -*- 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 "nsTimerImpl.h" + +#include + +#include "TimerThread.h" +#include "mozilla/Atomics.h" +#include "mozilla/IntegerPrintfMacros.h" +#include "mozilla/Logging.h" +#include "mozilla/Mutex.h" +#include "mozilla/ProfilerLabels.h" +#include "mozilla/ResultExtensions.h" +#include "mozilla/Sprintf.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/Try.h" +#include "nsThreadManager.h" +#include "nsThreadUtils.h" +#include "pratom.h" + +#ifdef XP_WIN +# include +# ifndef getpid +# define getpid _getpid +# endif +#else +# include +#endif + +using mozilla::Atomic; +using mozilla::LogLevel; +using mozilla::MakeRefPtr; +using mozilla::MutexAutoLock; +using mozilla::TimeDuration; +using mozilla::TimeStamp; + +// Holds the timer thread and manages all interactions with it +// under a locked mutex. This wrapper is not destroyed until after +// nsThreadManager shutdown to ensure we don't UAF during an offthread access to +// the timer thread. +class TimerThreadWrapper { + public: + constexpr TimerThreadWrapper() : mThread(nullptr){}; + ~TimerThreadWrapper() = default; + + nsresult Init(); + void Shutdown(); + + nsresult AddTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(aTimer->mMutex); + nsresult RemoveTimer(nsTimerImpl* aTimer, const MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(aTimer->mMutex); + TimeStamp FindNextFireTimeForCurrentThread(TimeStamp aDefault, + uint32_t aSearchBound); + uint32_t AllowedEarlyFiringMicroseconds(); + nsresult GetTimers(nsTArray>& aRetVal); + + private: + static mozilla::StaticMutex sMutex; + TimerThread* mThread MOZ_GUARDED_BY(sMutex); +}; + +mozilla::StaticMutex TimerThreadWrapper::sMutex; + +nsresult TimerThreadWrapper::Init() { + mozilla::StaticMutexAutoLock lock(sMutex); + mThread = new TimerThread(); + + NS_ADDREF(mThread); + + return NS_OK; +} + +void TimerThreadWrapper::Shutdown() { + RefPtr thread; + + { + mozilla::StaticMutexAutoLock lock(sMutex); + if (!mThread) { + return; + } + thread = mThread; + } + // Shutdown calls |nsTimerImpl::Cancel| which needs to make a call into + // |RemoveTimer|. This can't be done under the lock. + thread->Shutdown(); + + { + mozilla::StaticMutexAutoLock lock(sMutex); + NS_RELEASE(mThread); + } +} + +nsresult TimerThreadWrapper::AddTimer(nsTimerImpl* aTimer, + const MutexAutoLock& aProofOfLock) { + mozilla::StaticMutexAutoLock lock(sMutex); + if (mThread) { + return mThread->AddTimer(aTimer, aProofOfLock); + } + return NS_ERROR_NOT_AVAILABLE; +} + +nsresult TimerThreadWrapper::RemoveTimer(nsTimerImpl* aTimer, + const MutexAutoLock& aProofOfLock) { + mozilla::StaticMutexAutoLock lock(sMutex); + if (mThread) { + return mThread->RemoveTimer(aTimer, aProofOfLock); + } + return NS_ERROR_NOT_AVAILABLE; +} + +TimeStamp TimerThreadWrapper::FindNextFireTimeForCurrentThread( + TimeStamp aDefault, uint32_t aSearchBound) { + mozilla::StaticMutexAutoLock lock(sMutex); + return mThread + ? mThread->FindNextFireTimeForCurrentThread(aDefault, aSearchBound) + : TimeStamp(); +} + +uint32_t TimerThreadWrapper::AllowedEarlyFiringMicroseconds() { + mozilla::StaticMutexAutoLock lock(sMutex); + return mThread ? mThread->AllowedEarlyFiringMicroseconds() : 0; +} + +nsresult TimerThreadWrapper::GetTimers(nsTArray>& aRetVal) { + RefPtr thread; + { + mozilla::StaticMutexAutoLock lock(sMutex); + if (!mThread) { + return NS_ERROR_NOT_AVAILABLE; + } + thread = mThread; + } + return thread->GetTimers(aRetVal); +} + +static TimerThreadWrapper gThreadWrapper; + +// This module prints info about the precision of timers. +static mozilla::LazyLogModule sTimerLog("nsTimerImpl"); + +mozilla::LogModule* GetTimerLog() { return sTimerLog; } + +TimeStamp NS_GetTimerDeadlineHintOnCurrentThread(TimeStamp aDefault, + uint32_t aSearchBound) { + return gThreadWrapper.FindNextFireTimeForCurrentThread(aDefault, + aSearchBound); +} + +already_AddRefed NS_NewTimer() { return NS_NewTimer(nullptr); } + +already_AddRefed NS_NewTimer(nsIEventTarget* aTarget) { + return nsTimer::WithEventTarget(aTarget).forget(); +} + +mozilla::Result, nsresult> NS_NewTimerWithObserver( + nsIObserver* aObserver, uint32_t aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithObserver(getter_AddRefs(timer), aObserver, aDelay, + aType, aTarget)); + return std::move(timer); +} +nsresult NS_NewTimerWithObserver(nsITimer** aTimer, nsIObserver* aObserver, + uint32_t aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + auto timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->Init(aObserver, aDelay, aType)); + timer.forget(aTimer); + return NS_OK; +} + +mozilla::Result, nsresult> NS_NewTimerWithCallback( + nsITimerCallback* aCallback, uint32_t aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithCallback(getter_AddRefs(timer), aCallback, aDelay, + aType, aTarget)); + return std::move(timer); +} +nsresult NS_NewTimerWithCallback(nsITimer** aTimer, nsITimerCallback* aCallback, + uint32_t aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + auto timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->InitWithCallback(aCallback, aDelay, aType)); + timer.forget(aTimer); + return NS_OK; +} + +mozilla::Result, nsresult> NS_NewTimerWithCallback( + nsITimerCallback* aCallback, const TimeDuration& aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithCallback(getter_AddRefs(timer), aCallback, aDelay, + aType, aTarget)); + return std::move(timer); +} +nsresult NS_NewTimerWithCallback(nsITimer** aTimer, nsITimerCallback* aCallback, + const TimeDuration& aDelay, uint32_t aType, + nsIEventTarget* aTarget) { + auto timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->InitHighResolutionWithCallback(aCallback, aDelay, aType)); + timer.forget(aTimer); + return NS_OK; +} + +mozilla::Result, nsresult> NS_NewTimerWithCallback( + std::function&& aCallback, uint32_t aDelay, uint32_t aType, + const char* aNameString, nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithCallback(getter_AddRefs(timer), std::move(aCallback), + aDelay, aType, aNameString, aTarget)); + return timer; +} +nsresult NS_NewTimerWithCallback(nsITimer** aTimer, + std::function&& aCallback, + uint32_t aDelay, uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget) { + return NS_NewTimerWithCallback(aTimer, std::move(aCallback), + TimeDuration::FromMilliseconds(aDelay), aType, + aNameString, aTarget); +} + +mozilla::Result, nsresult> NS_NewTimerWithCallback( + std::function&& aCallback, const TimeDuration& aDelay, + uint32_t aType, const char* aNameString, nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithCallback(getter_AddRefs(timer), std::move(aCallback), + aDelay, aType, aNameString, aTarget)); + return timer; +} +nsresult NS_NewTimerWithCallback(nsITimer** aTimer, + std::function&& aCallback, + const TimeDuration& aDelay, uint32_t aType, + const char* aNameString, + nsIEventTarget* aTarget) { + RefPtr timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->InitWithClosureCallback(std::move(aCallback), aDelay, aType, + aNameString)); + timer.forget(aTimer); + return NS_OK; +} + +mozilla::Result, nsresult> NS_NewTimerWithFuncCallback( + nsTimerCallbackFunc aCallback, void* aClosure, uint32_t aDelay, + uint32_t aType, const char* aNameString, nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithFuncCallback(getter_AddRefs(timer), aCallback, + aClosure, aDelay, aType, aNameString, + aTarget)); + return std::move(timer); +} +nsresult NS_NewTimerWithFuncCallback(nsITimer** aTimer, + nsTimerCallbackFunc aCallback, + void* aClosure, uint32_t aDelay, + uint32_t aType, const char* aNameString, + nsIEventTarget* aTarget) { + auto timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->InitWithNamedFuncCallback(aCallback, aClosure, aDelay, aType, + aNameString)); + timer.forget(aTimer); + return NS_OK; +} + +mozilla::Result, nsresult> NS_NewTimerWithFuncCallback( + nsTimerCallbackFunc aCallback, void* aClosure, const TimeDuration& aDelay, + uint32_t aType, const char* aNameString, nsIEventTarget* aTarget) { + nsCOMPtr timer; + MOZ_TRY(NS_NewTimerWithFuncCallback(getter_AddRefs(timer), aCallback, + aClosure, aDelay, aType, aNameString, + aTarget)); + return std::move(timer); +} +nsresult NS_NewTimerWithFuncCallback(nsITimer** aTimer, + nsTimerCallbackFunc aCallback, + void* aClosure, const TimeDuration& aDelay, + uint32_t aType, const char* aNameString, + nsIEventTarget* aTarget) { + auto timer = nsTimer::WithEventTarget(aTarget); + + MOZ_TRY(timer->InitHighResolutionWithNamedFuncCallback( + aCallback, aClosure, aDelay, aType, aNameString)); + timer.forget(aTimer); + return NS_OK; +} + +// This module prints info about which timers are firing, which is useful for +// wakeups for the purposes of power profiling. Set the following environment +// variable before starting the browser. +// +// MOZ_LOG=TimerFirings:4 +// +// Then a line will be printed for every timer that fires. +// +// If you redirect this output to a file called "out", you can then +// post-process it with a command something like the following. +// +// cat out | grep timer | sort | uniq -c | sort -r -n +// +// This will show how often each unique line appears, with the most common ones +// first. +// +// More detailed docs are here: +// https://developer.mozilla.org/en-US/docs/Mozilla/Performance/TimerFirings_logging +// +static mozilla::LazyLogModule sTimerFiringsLog("TimerFirings"); + +static mozilla::LogModule* GetTimerFiringsLog() { return sTimerFiringsLog; } + +#include + +/* static */ +mozilla::StaticMutex nsTimerImpl::sDeltaMutex; +/* static */ +double nsTimerImpl::sDeltaSumSquared MOZ_GUARDED_BY(nsTimerImpl::sDeltaMutex) = + 0; +/* static */ +double nsTimerImpl::sDeltaSum MOZ_GUARDED_BY(nsTimerImpl::sDeltaMutex) = 0; +/* static */ +double nsTimerImpl::sDeltaNum MOZ_GUARDED_BY(nsTimerImpl::sDeltaMutex) = 0; + +static void myNS_MeanAndStdDev(double n, double sumOfValues, + double sumOfSquaredValues, double* meanResult, + double* stdDevResult) { + double mean = 0.0, var = 0.0, stdDev = 0.0; + if (n > 0.0 && sumOfValues >= 0) { + mean = sumOfValues / n; + double temp = (n * sumOfSquaredValues) - (sumOfValues * sumOfValues); + if (temp < 0.0 || n <= 1) { + var = 0.0; + } else { + var = temp / (n * (n - 1)); + } + // for some reason, Windows says sqrt(0.0) is "-1.#J" (?!) so do this: + stdDev = var != 0.0 ? sqrt(var) : 0.0; + } + *meanResult = mean; + *stdDevResult = stdDev; +} + +NS_IMPL_QUERY_INTERFACE(nsTimer, nsITimer) +NS_IMPL_ADDREF(nsTimer) + +NS_IMPL_ISUPPORTS(nsTimerManager, nsITimerManager) + +NS_IMETHODIMP nsTimerManager::GetTimers(nsTArray>& aRetVal) { + return gThreadWrapper.GetTimers(aRetVal); +} + +NS_IMETHODIMP_(MozExternalRefCountType) +nsTimer::Release(void) { + nsrefcnt count = --mRefCnt; + NS_LOG_RELEASE(this, count, "nsTimer"); + + if (count == 1) { + // Last ref, in nsTimerImpl::mITimer. Make sure the cycle is broken. + mImpl->CancelImpl(true); + } else if (count == 0) { + delete this; + } + + return count; +} + +nsTimerImpl::nsTimerImpl(nsITimer* aTimer, nsIEventTarget* aTarget) + : mEventTarget(aTarget), + mIsInTimerThread(false), + mType(0), + mGeneration(0), + mITimer(aTimer), + mMutex("nsTimerImpl::mMutex"), + mCallback(UnknownCallback{}), + mFiring(0) { + // XXX some code creates timers during xpcom shutdown, when threads are no + // longer available, so we cannot turn this on yet. + // MOZ_ASSERT(mEventTarget); +} + +// static +nsresult nsTimerImpl::Startup() { return gThreadWrapper.Init(); } + +void nsTimerImpl::Shutdown() { + if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug)) { + mozilla::StaticMutexAutoLock lock(sDeltaMutex); + double mean = 0, stddev = 0; + myNS_MeanAndStdDev(sDeltaNum, sDeltaSum, sDeltaSumSquared, &mean, &stddev); + + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("sDeltaNum = %f, sDeltaSum = %f, sDeltaSumSquared = %f\n", + sDeltaNum, sDeltaSum, sDeltaSumSquared)); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("mean: %fms, stddev: %fms\n", mean, stddev)); + } + + gThreadWrapper.Shutdown(); +} + +nsresult nsTimerImpl::InitCommon(const TimeDuration& aDelay, uint32_t aType, + Callback&& newCallback, + const MutexAutoLock& aProofOfLock) { + if (!mEventTarget) { + return NS_ERROR_NOT_INITIALIZED; + } + + gThreadWrapper.RemoveTimer(this, aProofOfLock); + + // If we have an existing callback, using `swap` ensures it's destroyed after + // the mutex is unlocked in our caller. + std::swap(mCallback, newCallback); + ++mGeneration; + + mType = (uint8_t)aType; + mDelay = aDelay; + mTimeout = TimeStamp::Now() + mDelay; + + return gThreadWrapper.AddTimer(this, aProofOfLock); +} + +nsresult nsTimerImpl::InitWithNamedFuncCallback(nsTimerCallbackFunc aFunc, + void* aClosure, uint32_t aDelay, + uint32_t aType, + const char* aName) { + return InitHighResolutionWithNamedFuncCallback( + aFunc, aClosure, TimeDuration::FromMilliseconds(aDelay), aType, aName); +} + +nsresult nsTimerImpl::InitHighResolutionWithNamedFuncCallback( + nsTimerCallbackFunc aFunc, void* aClosure, const TimeDuration& aDelay, + uint32_t aType, const char* aName) { + if (NS_WARN_IF(!aFunc)) { + return NS_ERROR_INVALID_ARG; + } + + Callback cb{FuncCallback{aFunc, aClosure, aName}}; + + MutexAutoLock lock(mMutex); + return InitCommon(aDelay, aType, std::move(cb), lock); +} + +nsresult nsTimerImpl::InitWithCallback(nsITimerCallback* aCallback, + uint32_t aDelayInMs, uint32_t aType) { + return InitHighResolutionWithCallback( + aCallback, TimeDuration::FromMilliseconds(aDelayInMs), aType); +} + +nsresult nsTimerImpl::InitHighResolutionWithCallback( + nsITimerCallback* aCallback, const TimeDuration& aDelay, uint32_t aType) { + if (NS_WARN_IF(!aCallback)) { + return NS_ERROR_INVALID_ARG; + } + + // Goes out of scope after the unlock, prevents deadlock + Callback cb{nsCOMPtr{aCallback}}; + + MutexAutoLock lock(mMutex); + return InitCommon(aDelay, aType, std::move(cb), lock); +} + +nsresult nsTimerImpl::Init(nsIObserver* aObserver, uint32_t aDelayInMs, + uint32_t aType) { + if (NS_WARN_IF(!aObserver)) { + return NS_ERROR_INVALID_ARG; + } + + Callback cb{nsCOMPtr{aObserver}}; + + MutexAutoLock lock(mMutex); + return InitCommon(TimeDuration::FromMilliseconds(aDelayInMs), aType, + std::move(cb), lock); +} + +nsresult nsTimerImpl::InitWithClosureCallback( + std::function&& aCallback, const TimeDuration& aDelay, + uint32_t aType, const char* aNameString) { + if (NS_WARN_IF(!aCallback)) { + return NS_ERROR_INVALID_ARG; + } + + Callback cb{ClosureCallback{std::move(aCallback), aNameString}}; + + MutexAutoLock lock(mMutex); + return InitCommon(aDelay, aType, std::move(cb), lock); +} + +nsresult nsTimerImpl::Cancel() { + CancelImpl(false); + return NS_OK; +} + +void nsTimerImpl::CancelImpl(bool aClearITimer) { + Callback cbTrash{UnknownCallback{}}; + RefPtr timerTrash; + + { + MutexAutoLock lock(mMutex); + gThreadWrapper.RemoveTimer(this, lock); + + // The swap ensures our callback isn't dropped until after the mutex is + // unlocked. + std::swap(cbTrash, mCallback); + ++mGeneration; + + // Don't clear this if we're firing; once Fire returns, we'll get this call + // again. + if (aClearITimer && !mFiring) { + MOZ_RELEASE_ASSERT( + mITimer, + "mITimer was nulled already! " + "This indicates that someone has messed up the refcount on nsTimer!"); + timerTrash.swap(mITimer); + } + } +} + +nsresult nsTimerImpl::SetDelay(uint32_t aDelay) { + MutexAutoLock lock(mMutex); + if (GetCallback().is() && !IsRepeating()) { + // This may happen if someone tries to re-use a one-shot timer + // by re-setting delay instead of reinitializing the timer. + NS_ERROR( + "nsITimer->SetDelay() called when the " + "one-shot timer is not set up."); + return NS_ERROR_NOT_INITIALIZED; + } + + bool reAdd = false; + reAdd = NS_SUCCEEDED(gThreadWrapper.RemoveTimer(this, lock)); + + mDelay = TimeDuration::FromMilliseconds(aDelay); + mTimeout = TimeStamp::Now() + mDelay; + + if (reAdd) { + gThreadWrapper.AddTimer(this, lock); + } + + return NS_OK; +} + +nsresult nsTimerImpl::GetDelay(uint32_t* aDelay) { + MutexAutoLock lock(mMutex); + *aDelay = mDelay.ToMilliseconds(); + return NS_OK; +} + +nsresult nsTimerImpl::SetType(uint32_t aType) { + MutexAutoLock lock(mMutex); + mType = (uint8_t)aType; + // XXX if this is called, we should change the actual type.. this could effect + // repeating timers. we need to ensure in Fire() that if mType has changed + // during the callback that we don't end up with the timer in the queue twice. + return NS_OK; +} + +nsresult nsTimerImpl::GetType(uint32_t* aType) { + MutexAutoLock lock(mMutex); + *aType = mType; + return NS_OK; +} + +nsresult nsTimerImpl::GetClosure(void** aClosure) { + MutexAutoLock lock(mMutex); + if (GetCallback().is()) { + *aClosure = GetCallback().as().mClosure; + } else { + *aClosure = nullptr; + } + return NS_OK; +} + +nsresult nsTimerImpl::GetCallback(nsITimerCallback** aCallback) { + MutexAutoLock lock(mMutex); + if (GetCallback().is()) { + NS_IF_ADDREF(*aCallback = GetCallback().as()); + } else { + *aCallback = nullptr; + } + return NS_OK; +} + +nsresult nsTimerImpl::GetTarget(nsIEventTarget** aTarget) { + MutexAutoLock lock(mMutex); + NS_IF_ADDREF(*aTarget = mEventTarget); + return NS_OK; +} + +nsresult nsTimerImpl::SetTarget(nsIEventTarget* aTarget) { + MutexAutoLock lock(mMutex); + if (NS_WARN_IF(!mCallback.is())) { + return NS_ERROR_ALREADY_INITIALIZED; + } + + if (aTarget) { + mEventTarget = aTarget; + } else { + mEventTarget = mozilla::GetCurrentSerialEventTarget(); + } + return NS_OK; +} + +nsresult nsTimerImpl::GetAllowedEarlyFiringMicroseconds(uint32_t* aValueOut) { + *aValueOut = gThreadWrapper.AllowedEarlyFiringMicroseconds(); + return NS_OK; +} + +void nsTimerImpl::Fire(int32_t aGeneration) { + uint8_t oldType; + uint32_t oldDelay; + TimeStamp oldTimeout; + Callback callbackDuringFire{UnknownCallback{}}; + nsCOMPtr timer; + + { + // Don't fire callbacks or fiddle with refcounts when the mutex is locked. + // If some other thread Cancels/Inits after this, they're just too late. + MutexAutoLock lock(mMutex); + if (aGeneration != mGeneration) { + // This timer got rescheduled or cancelled before we fired, so ignore this + // firing + return; + } + + // We modify mTimeout, so we must not be in the current TimerThread's + // mTimers list. + MOZ_ASSERT(!mIsInTimerThread); + + ++mFiring; + callbackDuringFire = mCallback; + oldType = mType; + oldDelay = mDelay.ToMilliseconds(); + oldTimeout = mTimeout; + // Ensure that the nsITimer does not unhook from the nsTimerImpl during + // Fire; this will cause null pointer crashes if the user of the timer drops + // its reference, and then uses the nsITimer* passed in the callback. + timer = mITimer; + } + + AUTO_PROFILER_LABEL("nsTimerImpl::Fire", OTHER); + + TimeStamp fireTime; + if (MOZ_LOG_TEST(GetTimerLog(), LogLevel::Debug)) { + fireTime = TimeStamp::Now(); + TimeDuration delta = fireTime - oldTimeout; + int32_t d = delta.ToMilliseconds(); // delta in ms + { + mozilla::StaticMutexAutoLock lock(sDeltaMutex); + sDeltaSum += abs(d); + sDeltaSumSquared += double(d) * double(d); + sDeltaNum++; + } + + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] expected delay time %4ums\n", this, oldDelay)); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] actual delay time %4dms\n", this, oldDelay + d)); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] (mType is %d) -------\n", this, oldType)); + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] delta %4dms\n", this, d)); + } + + if (MOZ_LOG_TEST(GetTimerFiringsLog(), LogLevel::Debug)) { + LogFiring(callbackDuringFire, oldType, oldDelay); + } + + callbackDuringFire.match( + [](const UnknownCallback&) {}, + [&](const InterfaceCallback& i) { i->Notify(timer); }, + [&](const ObserverCallback& o) { + o->Observe(timer, NS_TIMER_CALLBACK_TOPIC, nullptr); + }, + [&](const FuncCallback& f) { f.mFunc(timer, f.mClosure); }, + [&](const ClosureCallback& c) { c.mFunc(timer); }); + + TimeStamp now = TimeStamp::Now(); + + MutexAutoLock lock(mMutex); + if (aGeneration == mGeneration) { + if (IsRepeating()) { + // Repeating timer has not been re-init or canceled; reschedule + if (IsSlack()) { + mTimeout = now + mDelay; + } else { + if (mDelay) { + // If we are late enough finishing the callback that we have missed + // some firings, do not attempt to play catchup, just get back on the + // cadence we're supposed to maintain. + unsigned missedFirings = + static_cast((now - mTimeout) / mDelay); + mTimeout += mDelay * (missedFirings + 1); + } else { + // Can we stop allowing repeating timers with delay 0? + mTimeout = now; + } + } + gThreadWrapper.AddTimer(this, lock); + } else { + // Non-repeating timer that has not been re-scheduled. Clear. + // XXX(nika): Other callsites seem to go to some effort to avoid + // destroying mCallback when it's held. Why not this one? + mCallback = mozilla::AsVariant(UnknownCallback{}); + } + } + + --mFiring; + + MOZ_LOG(GetTimerLog(), LogLevel::Debug, + ("[this=%p] Took %fms to fire timer callback\n", this, + (now - fireTime).ToMilliseconds())); +} + +// See the big comment above GetTimerFiringsLog() to understand this code. +void nsTimerImpl::LogFiring(const Callback& aCallback, uint8_t aType, + uint32_t aDelay) { + const char* typeStr; + switch (aType) { + case nsITimer::TYPE_ONE_SHOT: + typeStr = "ONE_SHOT "; + break; + case nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY: + typeStr = "ONE_LOW "; + break; + case nsITimer::TYPE_REPEATING_SLACK: + typeStr = "SLACK "; + break; + case nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY: + typeStr = "SLACK_LOW "; + break; + case nsITimer::TYPE_REPEATING_PRECISE: /* fall through */ + case nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP: + typeStr = "PRECISE "; + break; + default: + MOZ_CRASH("bad type"); + } + + aCallback.match( + [&](const UnknownCallback&) { + MOZ_LOG( + GetTimerFiringsLog(), LogLevel::Debug, + ("[%d] ??? timer (%s, %5d ms)\n", getpid(), typeStr, aDelay)); + }, + [&](const InterfaceCallback& i) { + MOZ_LOG(GetTimerFiringsLog(), LogLevel::Debug, + ("[%d] iface timer (%s %5d ms): %p\n", getpid(), typeStr, + aDelay, i.get())); + }, + [&](const ObserverCallback& o) { + MOZ_LOG(GetTimerFiringsLog(), LogLevel::Debug, + ("[%d] obs timer (%s %5d ms): %p\n", getpid(), typeStr, + aDelay, o.get())); + }, + [&](const FuncCallback& f) { + MOZ_LOG(GetTimerFiringsLog(), LogLevel::Debug, + ("[%d] fn timer (%s %5d ms): %s\n", getpid(), typeStr, + aDelay, f.mName)); + }, + [&](const ClosureCallback& c) { + MOZ_LOG(GetTimerFiringsLog(), LogLevel::Debug, + ("[%d] closure timer (%s %5d ms): %s\n", getpid(), typeStr, + aDelay, c.mName)); + }); +} + +void nsTimerImpl::GetName(nsACString& aName, + const MutexAutoLock& aProofOfLock) { + GetCallback().match( + [&](const UnknownCallback&) { aName.AssignLiteral("Canceled_timer"); }, + [&](const InterfaceCallback& i) { + if (nsCOMPtr named = do_QueryInterface(i)) { + named->GetName(aName); + } else { + aName.AssignLiteral("Anonymous_interface_timer"); + } + }, + [&](const ObserverCallback& o) { + if (nsCOMPtr named = do_QueryInterface(o)) { + named->GetName(aName); + } else { + aName.AssignLiteral("Anonymous_observer_timer"); + } + }, + [&](const FuncCallback& f) { aName.Assign(f.mName); }, + [&](const ClosureCallback& c) { aName.Assign(c.mName); }); +} + +nsresult nsTimerImpl::GetName(nsACString& aName) { + MutexAutoLock lock(mMutex); + GetName(aName, lock); + return NS_OK; +} + +nsTimer::~nsTimer() = default; + +size_t nsTimer::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) { + return aMallocSizeOf(this); +} + +/* static */ +RefPtr nsTimer::WithEventTarget(nsIEventTarget* aTarget) { + if (!aTarget) { + aTarget = mozilla::GetCurrentSerialEventTarget(); + } + return do_AddRef(new nsTimer(aTarget)); +} + +/* static */ +nsresult nsTimer::XPCOMConstructor(REFNSIID aIID, void** aResult) { + *aResult = nullptr; + auto timer = WithEventTarget(nullptr); + + return timer->QueryInterface(aIID, aResult); +} diff --git a/xpcom/threads/nsTimerImpl.h b/xpcom/threads/nsTimerImpl.h new file mode 100644 index 0000000000..49f1fd00d5 --- /dev/null +++ b/xpcom/threads/nsTimerImpl.h @@ -0,0 +1,231 @@ +/* -*- 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 nsTimerImpl_h___ +#define nsTimerImpl_h___ + +#include "nsITimer.h" +#include "nsIEventTarget.h" +#include "nsIObserver.h" + +#include "nsCOMPtr.h" + +#include "mozilla/Attributes.h" +#include "mozilla/Mutex.h" +#include "mozilla/StaticMutex.h" +#include "mozilla/TimeStamp.h" +#include "mozilla/Variant.h" +#include "mozilla/Logging.h" + +extern mozilla::LogModule* GetTimerLog(); + +#define NS_TIMER_CID \ + { /* 5ff24248-1dd2-11b2-8427-fbab44f29bc8 */ \ + 0x5ff24248, 0x1dd2, 0x11b2, { \ + 0x84, 0x27, 0xfb, 0xab, 0x44, 0xf2, 0x9b, 0xc8 \ + } \ + } + +class nsIObserver; + +namespace mozilla { +class LogModule; +} + +// TimerThread, nsTimerEvent, and nsTimer have references to these. nsTimer has +// a separate lifecycle so we can Cancel() the underlying timer when the user of +// the nsTimer has let go of its last reference. +class nsTimerImpl { + ~nsTimerImpl() { + MOZ_ASSERT(!mIsInTimerThread); + + // The nsITimer interface requires that its users keep a reference to the + // timers they use while those timers are initialized but have not yet + // fired. If this assert ever fails, it is a bug in the code that created + // and used the timer. + // + // Further, note that this should never fail even with a misbehaving user, + // because nsTimer::Release checks for a refcount of 1 with an armed timer + // (a timer whose only reference is from the timer thread) and when it hits + // this will remove the timer from the timer thread and thus destroy the + // last reference, preventing this situation from occurring. + MOZ_ASSERT( + mCallback.is() || mEventTarget->IsOnCurrentThread(), + "Must not release mCallback off-target without canceling"); + } + + public: + typedef mozilla::TimeStamp TimeStamp; + + nsTimerImpl(nsITimer* aTimer, nsIEventTarget* aTarget); + NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsTimerImpl) + NS_DECL_NON_VIRTUAL_NSITIMER + + static nsresult Startup(); + static void Shutdown(); + + void SetDelayInternal(uint32_t aDelay, TimeStamp aBase = TimeStamp::Now()); + void CancelImpl(bool aClearITimer); + + void Fire(int32_t aGeneration); + + int32_t GetGeneration() { return mGeneration; } + + struct UnknownCallback {}; + + using InterfaceCallback = nsCOMPtr; + + using ObserverCallback = nsCOMPtr; + + /// A raw function pointer and its closed-over state, along with its name for + /// logging purposes. + struct FuncCallback { + nsTimerCallbackFunc mFunc; + void* mClosure; + const char* mName; + }; + + /// A callback defined by an owned closure and its name for logging purposes. + struct ClosureCallback { + std::function mFunc; + const char* mName; + }; + + using Callback = + mozilla::Variant; + + nsresult InitCommon(const mozilla::TimeDuration& aDelay, uint32_t aType, + Callback&& newCallback, + const mozilla::MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mMutex); + + Callback& GetCallback() MOZ_REQUIRES(mMutex) { + mMutex.AssertCurrentThreadOwns(); + return mCallback; + } + + bool IsRepeating() const { + static_assert(nsITimer::TYPE_ONE_SHOT < nsITimer::TYPE_REPEATING_SLACK, + "invalid ordering of timer types!"); + static_assert( + nsITimer::TYPE_REPEATING_SLACK < nsITimer::TYPE_REPEATING_PRECISE, + "invalid ordering of timer types!"); + static_assert(nsITimer::TYPE_REPEATING_PRECISE < + nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP, + "invalid ordering of timer types!"); + return mType >= nsITimer::TYPE_REPEATING_SLACK && + mType < nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY; + } + + bool IsLowPriority() const { + return mType == nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY || + mType == nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY; + } + + bool IsSlack() const { + return mType == nsITimer::TYPE_REPEATING_SLACK || + mType == nsITimer::TYPE_REPEATING_SLACK_LOW_PRIORITY; + } + + void GetName(nsACString& aName, const mozilla::MutexAutoLock& aProofOfLock) + MOZ_REQUIRES(mMutex); + + bool IsInTimerThread() const { return mIsInTimerThread; } + void SetIsInTimerThread(bool aIsInTimerThread) { + mIsInTimerThread = aIsInTimerThread; + } + + nsCOMPtr mEventTarget; + + void LogFiring(const Callback& aCallback, uint8_t aType, uint32_t aDelay); + + nsresult InitWithClosureCallback(std::function&& aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, const char* aNameString); + + // Is this timer currently referenced from a TimerThread::Entry? + // Note: It is cleared before the Entry is destroyed. Take() also sets it to + // false, to indicate it's no longer in the TimerThread's list. This Take() + // call is NOT made under the nsTimerImpl's mutex (all other + // SetIsInTimerThread calls are under the mutex). However, ALL accesses to + // mIsInTimerThread are under the TimerThread's Monitor lock, so consistency + // is guaranteed by that. + bool mIsInTimerThread; + + // These members are set by the initiating thread, when the timer's type is + // changed and during the period where it fires on that thread. + uint8_t mType; + + // The generation number of this timer, re-generated each time the timer is + // initialized so one-shot timers can be canceled and re-initialized by the + // arming thread without any bad race conditions. + // Updated only after this timer has been removed from the timer thread. + int32_t mGeneration; + + mozilla::TimeDuration mDelay MOZ_GUARDED_BY(mMutex); + // Never updated while in the TimerThread's timer list. Only updated + // before adding to that list or during nsTimerImpl::Fire(), when it has + // been removed from the TimerThread's list. TimerThread can access + // mTimeout of any timer in the list safely + mozilla::TimeStamp mTimeout; + + RefPtr mITimer MOZ_GUARDED_BY(mMutex); + mozilla::Mutex mMutex; + Callback mCallback MOZ_GUARDED_BY(mMutex); + // Counter because in rare cases we can Fire reentrantly + unsigned int mFiring MOZ_GUARDED_BY(mMutex); + + static mozilla::StaticMutex sDeltaMutex; + static double sDeltaSum MOZ_GUARDED_BY(sDeltaMutex); + static double sDeltaSumSquared MOZ_GUARDED_BY(sDeltaMutex); + static double sDeltaNum MOZ_GUARDED_BY(sDeltaMutex); +}; + +class nsTimer final : public nsITimer { + explicit nsTimer(nsIEventTarget* aTarget) + : mImpl(new nsTimerImpl(this, aTarget)) {} + + virtual ~nsTimer(); + + public: + friend class TimerThread; + friend class nsTimerEvent; + + NS_DECL_THREADSAFE_ISUPPORTS + NS_FORWARD_SAFE_NSITIMER(mImpl); + + // NOTE: This constructor is not exposed on `nsITimer` as NS_FORWARD_SAFE_ + // does not support forwarding rvalue references. + nsresult InitWithClosureCallback(std::function&& aCallback, + const mozilla::TimeDuration& aDelay, + uint32_t aType, const char* aNameString) { + return mImpl ? mImpl->InitWithClosureCallback(std::move(aCallback), aDelay, + aType, aNameString) + : NS_ERROR_NULL_POINTER; + } + + // Create a timer targeting the given target. nullptr indicates that the + // current thread should be used as the timer's target. + static RefPtr WithEventTarget(nsIEventTarget* aTarget); + + static nsresult XPCOMConstructor(REFNSIID aIID, void** aResult); + + private: + // nsTimerImpl holds a strong ref to us. When our refcount goes to 1, we will + // null this to break the cycle. + RefPtr mImpl; +}; + +class nsTimerManager final : public nsITimerManager { + public: + NS_DECL_ISUPPORTS + NS_DECL_NSITIMERMANAGER + private: + ~nsTimerManager() = default; +}; + +#endif /* nsTimerImpl_h___ */ -- cgit v1.2.3