Files
sousa-gecko/xpcom/threads/TaskQueue.h
T
Nika Layzell 325ec7d64a Bug 1983309 - Reduce lock contention in TaskQueue. r=xpcom-reviewers,jstutte
This patch aims to reduce lock contention when dispatching to TaskQueue,
by reducing the frequency with which the runner thread needs to acquire
the TaskQueue monitor.

Prior to this change, if there is a backlog of queued runnables,
TaskQueue::Runner would acquire mQueueMonitor 3 times per event. After
this change, mQueueMonitor will only be acquired when the set of tasks
read the last time the monitor was held have all been processed.

TaskQueue would need to change to use an unbounded lock-free MPSC
queue in order to further reduce contention when dispatching. Doing so
is a much larger project, and would require more substantial changes
to the TaskQueue type to make state management (especially around
shutdown) avoid locking.

Differential Revision: https://phabricator.services.mozilla.com/D261278
2026-05-21 12:38:58 +00:00

324 lines
12 KiB
C++

/* 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/RefPtr.h"
#include "mozilla/TargetShutdownTaskSet.h"
#include "mozilla/TaskDispatcher.h"
#include "nsIDirectTaskDispatcher.h"
#include "nsITargetShutdownTask.h"
#include "nsThreadUtils.h"
#define MOZILLA_TASKQUEUE_IID \
{0xb5181e3a, 0x39cf, 0x4d32, {0x81, 0x4a, 0xea, 0x86, 0x94, 0x16, 0x95, 0xd1}}
namespace mozilla {
typedef MozPromise<bool, bool, false> ShutdownPromise;
class TaskQueueTargetShutdownTask;
// 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 that since a TaskQueue is an AbstractThread and thus an nsIEventTarget,
// it's 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 unless it has shutdown tasks
// registered to it. TaskQueue provides a BeginShutdown() method that places
// TaskQueue in a shut down state and returns a promise that gets resolved once
// all shutdown and pending tasks have completed.
//
// On implicit release without regular shutdown the runner will have ensured
// that all dispatched tasks completed before (provided the target
// SUPPORTS_SHUTDOWN_TASK_DISPATCH, as most do) and TaskQueue asserts there to
// be no shutdown tasks registered. If there are shutdown tasks registered,
// TaskQueue registers a shutdown task with its target to guarantee an explicit
// shutdown when the target goes away.
class TaskQueue final : public AbstractThread,
public nsIDirectTaskDispatcher,
public nsITargetShutdownTask {
class EventTargetWrapper;
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIDIRECTTASKDISPATCHER
MOZ_DECLARE_REFCOUNTED_TYPENAME(TaskQueue)
NS_INLINE_DECL_STATIC_IID(MOZILLA_TASKQUEUE_IID)
void TargetShutdown() override;
static RefPtr<TaskQueue> Create(already_AddRefed<nsIEventTarget> aTarget,
StaticString aName,
bool aSupportsTailDispatch = false);
TaskDispatcher& TailDispatcher() override;
NS_IMETHOD DispatchFromScript(nsIRunnable* aEvent,
DispatchFlags aFlags) override {
return Dispatch(do_AddRef(aEvent), aFlags);
}
NS_IMETHOD Dispatch(already_AddRefed<nsIRunnable> aEvent,
DispatchFlags aFlags) override {
// NOTE: This dispatch implementation never leaks the runnable on failure,
// even if `NS_DISPATCH_FALLIBLE` is not specified.
nsCOMPtr<nsIRunnable> 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<nsIRunnable> aRunnable,
DispatchReason aReason = NormalDispatch) override {
nsCOMPtr<nsIRunnable> 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;
NS_IMETHOD_(FeatureFlags) GetFeatures() override;
using CancelPromise = MozPromise<bool, bool, false>;
// 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<ShutdownPromise> 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;
class Observer {
public:
NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
// Called before an event is processed on the TaskQueue on its event target.
virtual void WillProcessEvent(TaskQueue* aQueue) = 0;
// Called after an event has been processed on the TaskQueue on its event
// target.
// Note that it is not safe to add direct tasks from DidProcessEvent().
virtual void DidProcessEvent(TaskQueue* aQueue) = 0;
protected:
virtual ~Observer() = default;
};
// Set an observer to be notified as this TaskQueue processes events.
// Callable from any thread. Transactional, i.e. WillProcess always comes
// first and is always matched by DidProcess.
void SetObserver(Observer* aObserver);
private:
TaskQueue(already_AddRefed<nsIEventTarget> 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<nsIRunnable>& aRunnable,
DispatchFlags aFlags,
DispatchReason aReason = NormalDispatch);
void MaybeResolveShutdown();
void MaybeUnregisterTargetShutdownTask() MOZ_REQUIRES(mQueueMonitor);
nsCOMPtr<nsIEventTarget> mTarget MOZ_GUARDED_BY(mQueueMonitor);
// Monitor that protects the queue, mIsRunning, mIsShutdown and
// mShutdownTasks;
Monitor mQueueMonitor;
struct TaskStruct {
nsCOMPtr<nsIRunnable> event;
DispatchFlags flags;
};
// NOTE: A `mozilla::Queue` is not used here, as Runner steals tasks from the
// TaskQueue in bulk. The efficiency of `mozilla::Queue` for mixing adding and
// removing entries would be unused. A small inline buffer is used to reduce
// allocations in the common case where few tasks are queued.
using TaskArray = AutoTArray<TaskStruct, 4>;
// Queue of tasks to run.
TaskArray mTasks MOZ_GUARDED_BY(mQueueMonitor);
// List of tasks to run during shutdown.
TargetShutdownTaskSet 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<PRThread*> mRunningThread;
// RAII class that gets instantiated for each dispatched task.
class AutoTaskGuard {
public:
AutoTaskGuard(TaskQueue* aQueue, TaskQueue::Observer* aObserver)
: mQueue(aQueue), mObserver(aObserver), 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();
mEventTargetGuard.emplace(mQueue);
if (mObserver) {
mObserver->WillProcessEvent(mQueue);
}
}
~AutoTaskGuard() {
mTaskDispatcher->DrainDirectTasks();
if (mObserver) {
mObserver->DidProcessEvent(mQueue);
MOZ_ASSERT(!mTaskDispatcher->HaveDirectTasks(),
"TaskQueue::Observer instance in "
"DidProcessEvent(TaskQueue*) added direct tasks in error");
}
mTaskDispatcher.reset();
mQueue->mTailDispatcher = nullptr;
mEventTargetGuard = Nothing();
MOZ_ASSERT(mQueue->mRunningThread == PR_GetCurrentThread());
mQueue->mRunningThread = nullptr;
sCurrentThreadTLS.set(mLastCurrentThread);
}
private:
Maybe<AutoTaskDispatcher> mTaskDispatcher;
Maybe<SerialEventTargetGuard> mEventTargetGuard;
TaskQueue* mQueue;
TaskQueue::Observer* mObserver;
AbstractThread* mLastCurrentThread;
};
TaskDispatcher* mTailDispatcher;
// True if we have registered a target shutdown task with mTarget.
bool mIsTargetShutdownTaskRegistered MOZ_GUARDED_BY(mQueueMonitor);
// 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<ShutdownPromise> mShutdownPromise
MOZ_GUARDED_BY(mQueueMonitor);
// The name of this TaskQueue. Useful when debugging dispatch failures.
const char* const mName;
SimpleTaskQueue mDirectTasks;
RefPtr<Observer> mObserver MOZ_GUARDED_BY(mQueueMonitor);
class Runner : public Runnable {
public:
Runner(TaskQueue* aQueue, nsIEventTarget* aTarget, Observer* aObserver,
TaskArray&& aTasks)
: Runnable("TaskQueue::Runner"),
mQueue(aQueue),
mTarget(aTarget),
mObserver(aObserver),
mTasks(std::move(aTasks)) {}
NS_IMETHOD Run() override;
private:
RefPtr<TaskQueue> mQueue;
// Local cache of mTarget and mObserver in the task runner, so that they can
// be read without acquiring mQueue->mMonitor. Re-loaded from the TaskQueue
// every time mTasks is exhausted.
nsCOMPtr<nsIEventTarget> mTarget;
RefPtr<Observer> mObserver;
// List of tasks taken from the TaskQueue and being actively processed.
TaskArray mTasks;
// Index of the next task to process in mTasks. Once this reaches
// mTasks.Length(), Runner will re-load mTasks from the task queue and
// continue running if new tasks have been posted.
size_t mNextTask = 0;
};
};
} // namespace mozilla
#endif // TaskQueue_h_