fune/dom/workers/WorkerThread.cpp
Eden Chuang fd5b902f9f Bug 1879272 - Clear Worker thread event queue in WorkerPrivate::RunLoopNevenRan and WorkerThread::SetWorker(nullptr). r=asuth
There is two situatoins cause pending events in the worker thread after Worker status changes to "Dead."

1. The WorkerPrivate::DoRunLoop is never executed.
This is the case where the worker initialization on the worker thread fails. And then WorkerPrivate::RunLoopNeverRan() would be called for handling these fail cases. However, WorkerPrivate::mPreStartRunnables had already dispatched when WorkerPrivate::SetWorkerPrivateInWorkerThread() is called, so when the moment executing WorkerPrivate::RunLoopNeverRan(), mPreStartRunnables must in the worker thread already. CompileScriptRunnable is one of these runnable.

2. Worker enters into DoRunLoop(). The WorkerThread can be held as nsIThread or nsIEventTarget by

nsCOMPtr<nsIThread> thread = NS_GetCurrentThread()
thread->Dispatch(myRunnable);

We don't block these runnable dispatchings after the worker's status changes to "Dead" because some objects, such as cycle_collector, need to complete their shutdown after the worker's shutdown. So ExternalWrappedRunnables(wrapped here) shows up in the dispatching stack when we hit the assertion. The original runnable is from everywhere and is related to Worker.

To fix case 1, the patch wants to handle the pending events in WorkerPrivate::RunLoopNevenRan() correctly. So, call ProcessPendingEvents if needed.
To fix case 2, we process the pending events before setting WorkerThread::mWorkerPrivate as nullptr. I think this is a correct time point because I think any other shutdown jobs should be finished before we detach the WorkerPrivate from the Worker thread.

Differential Revision: https://phabricator.services.mozilla.com/D203551
2024-03-26 07:33:36 +00:00

383 lines
11 KiB
C++

/* -*- 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 "WorkerThread.h"
#include <utility>
#include "WorkerPrivate.h"
#include "WorkerRunnable.h"
#include "mozilla/Assertions.h"
#include "mozilla/Atomics.h"
#include "mozilla/CycleCollectedJSContext.h"
#include "mozilla/EventQueue.h"
#include "mozilla/Logging.h"
#include "mozilla/MacroForEach.h"
#include "mozilla/NotNull.h"
#include "mozilla/ThreadEventQueue.h"
#include "mozilla/UniquePtr.h"
#include "mozilla/ipc/BackgroundChild.h"
#include "nsCOMPtr.h"
#include "nsDebug.h"
#include "nsICancelableRunnable.h"
#include "nsIEventTarget.h"
#include "nsIRunnable.h"
#include "nsIThreadInternal.h"
#include "nsString.h"
#include "nsThreadUtils.h"
#include "prthread.h"
static mozilla::LazyLogModule gWorkerThread("WorkerThread");
#ifdef LOGV
# undef LOGV
#endif
#define LOGV(msg) MOZ_LOG(gWorkerThread, LogLevel::Verbose, msg);
namespace mozilla {
using namespace ipc;
namespace dom {
namespace {
// The C stack size. We use the same stack size on all platforms for
// consistency.
//
// Note: Our typical equation of 256 machine words works out to 2MB on 64-bit
// platforms. Since that works out to the size of a VM huge page, that can
// sometimes lead to an OS allocating an entire huge page for the stack at once.
// To avoid this, we subtract the size of 2 pages, to be safe.
const uint32_t kWorkerStackSize = 256 * sizeof(size_t) * 1024 - 8192;
} // namespace
WorkerThreadFriendKey::WorkerThreadFriendKey() {
MOZ_COUNT_CTOR(WorkerThreadFriendKey);
}
WorkerThreadFriendKey::~WorkerThreadFriendKey() {
MOZ_COUNT_DTOR(WorkerThreadFriendKey);
}
class WorkerThread::Observer final : public nsIThreadObserver {
WorkerPrivate* mWorkerPrivate;
public:
explicit Observer(WorkerPrivate* aWorkerPrivate)
: mWorkerPrivate(aWorkerPrivate) {
MOZ_ASSERT(aWorkerPrivate);
aWorkerPrivate->AssertIsOnWorkerThread();
}
NS_DECL_THREADSAFE_ISUPPORTS
private:
~Observer() { mWorkerPrivate->AssertIsOnWorkerThread(); }
NS_DECL_NSITHREADOBSERVER
};
WorkerThread::WorkerThread(ConstructorKey)
: nsThread(
MakeNotNull<ThreadEventQueue*>(MakeUnique<mozilla::EventQueue>()),
nsThread::NOT_MAIN_THREAD, {.stackSize = kWorkerStackSize}),
mLock("WorkerThread::mLock"),
mWorkerPrivateCondVar(mLock, "WorkerThread::mWorkerPrivateCondVar"),
mWorkerPrivate(nullptr),
mOtherThreadsDispatchingViaEventTarget(0)
#ifdef DEBUG
,
mAcceptingNonWorkerRunnables(true)
#endif
{
}
WorkerThread::~WorkerThread() {
MOZ_ASSERT(!mWorkerPrivate);
MOZ_ASSERT(!mOtherThreadsDispatchingViaEventTarget);
MOZ_ASSERT(mAcceptingNonWorkerRunnables);
}
// static
SafeRefPtr<WorkerThread> WorkerThread::Create(
const WorkerThreadFriendKey& /* aKey */) {
SafeRefPtr<WorkerThread> thread =
MakeSafeRefPtr<WorkerThread>(ConstructorKey());
if (NS_FAILED(thread->Init("DOM Worker"_ns))) {
NS_WARNING("Failed to create new thread!");
return nullptr;
}
return thread;
}
void WorkerThread::SetWorker(const WorkerThreadFriendKey& /* aKey */,
WorkerPrivate* aWorkerPrivate) {
MOZ_ASSERT(PR_GetCurrentThread() == mThread);
if (aWorkerPrivate) {
{
MutexAutoLock lock(mLock);
MOZ_ASSERT(!mWorkerPrivate);
MOZ_ASSERT(mAcceptingNonWorkerRunnables);
mWorkerPrivate = aWorkerPrivate;
#ifdef DEBUG
mAcceptingNonWorkerRunnables = false;
#endif
}
mObserver = new Observer(aWorkerPrivate);
MOZ_ALWAYS_SUCCEEDS(AddObserver(mObserver));
} else {
MOZ_ALWAYS_SUCCEEDS(RemoveObserver(mObserver));
mObserver = nullptr;
{
MutexAutoLock lock(mLock);
MOZ_ASSERT(mWorkerPrivate);
MOZ_ASSERT(!mAcceptingNonWorkerRunnables);
// mOtherThreadsDispatchingViaEventTarget can still be non-zero here
// because WorkerThread::Dispatch isn't atomic so a thread initiating
// dispatch can have dispatched a runnable at this thread allowing us to
// begin shutdown before that thread gets a chance to decrement
// mOtherThreadsDispatchingViaEventTarget back to 0. So we need to wait
// for that.
while (mOtherThreadsDispatchingViaEventTarget) {
mWorkerPrivateCondVar.Wait();
}
// Need to clean up the dispatched runnables if
// mOtherThreadsDispatchingViaEventTarget was non-zero.
if (NS_HasPendingEvents(nullptr)) {
NS_ProcessPendingEvents(nullptr);
}
#ifdef DEBUG
mAcceptingNonWorkerRunnables = true;
#endif
mWorkerPrivate = nullptr;
}
}
}
nsresult WorkerThread::DispatchPrimaryRunnable(
const WorkerThreadFriendKey& /* aKey */,
already_AddRefed<nsIRunnable> aRunnable) {
nsCOMPtr<nsIRunnable> runnable(aRunnable);
#ifdef DEBUG
MOZ_ASSERT(PR_GetCurrentThread() != mThread);
MOZ_ASSERT(runnable);
{
MutexAutoLock lock(mLock);
MOZ_ASSERT(!mWorkerPrivate);
MOZ_ASSERT(mAcceptingNonWorkerRunnables);
}
#endif
nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult WorkerThread::DispatchAnyThread(
const WorkerThreadFriendKey& /* aKey */,
already_AddRefed<WorkerRunnable> aWorkerRunnable) {
// May be called on any thread!
#ifdef DEBUG
{
const bool onWorkerThread = PR_GetCurrentThread() == mThread;
{
MutexAutoLock lock(mLock);
MOZ_ASSERT(mWorkerPrivate);
MOZ_ASSERT(!mAcceptingNonWorkerRunnables);
if (onWorkerThread) {
mWorkerPrivate->AssertIsOnWorkerThread();
}
}
}
#endif
nsCOMPtr<nsIRunnable> runnable(aWorkerRunnable);
nsresult rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// We don't need to notify the worker's condition variable here because we're
// being called from worker-controlled code and it will make sure to wake up
// the worker thread if needed.
return NS_OK;
}
NS_IMETHODIMP
WorkerThread::DispatchFromScript(nsIRunnable* aRunnable, uint32_t aFlags) {
nsCOMPtr<nsIRunnable> runnable(aRunnable);
return Dispatch(runnable.forget(), aFlags);
}
NS_IMETHODIMP
WorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
uint32_t aFlags) {
// May be called on any thread!
nsCOMPtr<nsIRunnable> runnable(aRunnable); // in case we exit early
LOGV(("WorkerThread::Dispatch [%p] runnable: %p", this, runnable.get()));
// Workers only support asynchronous dispatch.
if (NS_WARN_IF(aFlags != NS_DISPATCH_NORMAL)) {
return NS_ERROR_UNEXPECTED;
}
const bool onWorkerThread = PR_GetCurrentThread() == mThread;
WorkerPrivate* workerPrivate = nullptr;
if (onWorkerThread) {
// No need to lock here because it is only modified on this thread.
MOZ_ASSERT(mWorkerPrivate);
mWorkerPrivate->AssertIsOnWorkerThread();
workerPrivate = mWorkerPrivate;
} else {
MutexAutoLock lock(mLock);
MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget < UINT32_MAX);
if (mWorkerPrivate) {
workerPrivate = mWorkerPrivate;
// Incrementing this counter will make the worker thread sleep if it
// somehow tries to unset mWorkerPrivate while we're using it.
mOtherThreadsDispatchingViaEventTarget++;
}
}
nsresult rv;
if (runnable && onWorkerThread) {
RefPtr<WorkerRunnable> workerRunnable =
workerPrivate->MaybeWrapAsWorkerRunnable(runnable.forget());
rv = nsThread::Dispatch(workerRunnable.forget(), NS_DISPATCH_NORMAL);
} else {
rv = nsThread::Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
}
if (!onWorkerThread && workerPrivate) {
// We need to wake the worker thread if we're not already on the right
// thread and the dispatch succeeded.
if (NS_SUCCEEDED(rv)) {
MutexAutoLock workerLock(workerPrivate->mMutex);
workerPrivate->mCondVar.Notify();
}
// Now unset our waiting flag.
{
MutexAutoLock lock(mLock);
MOZ_ASSERT(mOtherThreadsDispatchingViaEventTarget);
if (!--mOtherThreadsDispatchingViaEventTarget) {
mWorkerPrivateCondVar.Notify();
}
}
}
if (NS_WARN_IF(NS_FAILED(rv))) {
LOGV(("WorkerThread::Dispatch [%p] failed, runnable: %p", this,
runnable.get()));
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
WorkerThread::DelayedDispatch(already_AddRefed<nsIRunnable>, uint32_t) {
return NS_ERROR_NOT_IMPLEMENTED;
}
uint32_t WorkerThread::RecursionDepth(
const WorkerThreadFriendKey& /* aKey */) const {
MOZ_ASSERT(PR_GetCurrentThread() == mThread);
return mNestedEventLoopDepth;
}
NS_IMETHODIMP
WorkerThread::HasPendingEvents(bool* aResult) {
MOZ_ASSERT(aResult);
const bool onWorkerThread = PR_GetCurrentThread() == mThread;
// If is on the worker thread, call nsThread::HasPendingEvents directly.
if (onWorkerThread) {
return nsThread::HasPendingEvents(aResult);
}
// Checking if is on the parent thread, otherwise, returns unexpected error.
{
MutexAutoLock lock(mLock);
// return directly if the mWorkerPrivate has not yet set or had already
// unset
if (!mWorkerPrivate) {
*aResult = false;
return NS_OK;
}
if (!mWorkerPrivate->IsOnParentThread()) {
*aResult = false;
return NS_ERROR_UNEXPECTED;
}
}
*aResult = mEvents->HasPendingEvent();
return NS_OK;
}
NS_IMPL_ISUPPORTS(WorkerThread::Observer, nsIThreadObserver)
NS_IMETHODIMP
WorkerThread::Observer::OnDispatchedEvent() {
MOZ_CRASH("OnDispatchedEvent() should never be called!");
}
NS_IMETHODIMP
WorkerThread::Observer::OnProcessNextEvent(nsIThreadInternal* /* aThread */,
bool aMayWait) {
mWorkerPrivate->AssertIsOnWorkerThread();
// If the PBackground child is not created yet, then we must permit
// blocking event processing to support
// BackgroundChild::GetOrCreateCreateForCurrentThread(). If this occurs
// then we are spinning on the event queue at the start of
// PrimaryWorkerRunnable::Run() and don't want to process the event in
// mWorkerPrivate yet.
if (aMayWait) {
MOZ_ASSERT(CycleCollectedJSContext::Get()->RecursionDepth() == 2);
MOZ_ASSERT(!BackgroundChild::GetForCurrentThread());
return NS_OK;
}
mWorkerPrivate->OnProcessNextEvent();
return NS_OK;
}
NS_IMETHODIMP
WorkerThread::Observer::AfterProcessNextEvent(nsIThreadInternal* /* aThread */,
bool /* aEventWasProcessed */) {
mWorkerPrivate->AssertIsOnWorkerThread();
mWorkerPrivate->AfterProcessNextEvent();
return NS_OK;
}
} // namespace dom
} // namespace mozilla