Bug 1775784 - Improve worker shutdown ordering. r=dom-worker-reviewers,smaug

This patch simplifies and cleans up the RemoteWorker state machine by:
- (In a previous patch in the stack) making PRemoteWorker refcounted instead of
  ManualAlloc allows us to eliminate use of ThreadSafeWeakPtr and some legwork
  that was attempting to ensure we only drop references to RemoteWorkerChild on
  the "Worker Launcher" thread.
  - Although we are not allowed to call IPC methods on a different thread, we
    can absolutely add and remove refcounts on other threads as needed.
  - This allowed removal of the `SelfHolder` class as well as a number of calls
    to NS_ProxyRelease.
  - This allowed removal of the `{get,m}RemoteWorkerControllerWeakRef` hack.
- Adding 2 optional lambda callbacks to the WorkerPrivate constructor that will
  be notified on the parent thread in order to simplify tracking the state of
  the worker:
  - CancellationCallback: Invoked when the parent thread tells the worker to
    cancel itself.  This can be the automatic result of a script load error,
    a call to `self.close()`, or an explicit call to Cancel() by the parent.
    - Note that cancellation was always being initiated by the parent thread,
      even in the case of `self.close()`!
  - TerminationCallback: Invoked when the worker thread has finished and the
    WorkerPrivate's self-ref is about to be dropped.  This indicates that the
    worker transitioned to Killing and fully completed that process.
- Changing the RemoteWorkerChild to always perform state transitions on the
  owning parent thread (although the state may be checked from other threads,
  more in the code comments).
  - The CancellationCallback mechanism replaces a complicated use of
    WorkerRefs.  This allowed removal of ReleaseWorkerRunnable.
  - In general this reduces the number of permutations of thread interactions
    we have to worry about.
- Changing the RemoteWorkerChild to only report that the worker has finished
  shutting down as a result of the TerminationCallback rather than when the
  worker has transitioned to Cancelling.
- Changing the states to better represent the worker's state in terms of
  WorkerStatus terminology.

Some related cleanups:
- Use of mechanisms provided by IPC:
  - RemoteWorkerChild had an mOwningEventTarget with a GetOwningEventTarget
    getter that we were able to replace with the IPC-maintained
    GetActorEventTarget().
  - We replaced the `mIPCActive` with use of IPC's CanSend().
- MOZ_LOG logging was added under "RemoteWorkerChild".

Differential Revision: https://phabricator.services.mozilla.com/D164644
This commit is contained in:
Andrew Sutherland 2023-03-14 23:57:57 +00:00
Родитель 17cdd0ba6c
Коммит b21cc21e8a
9 изменённых файлов: 794 добавлений и 546 удалений

Просмотреть файл

@ -20,7 +20,6 @@
#include "nsIPushErrorReporter.h"
#include "nsISupportsImpl.h"
#include "nsITimer.h"
#include "nsProxyRelease.h"
#include "nsServiceManagerUtils.h"
#include "nsTArray.h"
#include "nsThreadUtils.h"
@ -324,11 +323,11 @@ bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner,
RemoteWorkerChild::State& aState) {
MOZ_ASSERT(!mStarted);
MOZ_ASSERT(aOwner);
MOZ_ASSERT(aOwner->GetOwningEventTarget()->IsOnCurrentThread());
MOZ_ASSERT(aOwner->GetActorEventTarget()->IsOnCurrentThread());
auto launcherData = aOwner->mLauncherData.Access();
if (NS_WARN_IF(!launcherData->mIPCActive)) {
if (NS_WARN_IF(!aOwner->CanSend())) {
RejectAll(NS_ERROR_DOM_ABORT_ERR);
mStarted = true;
return true;
@ -339,8 +338,8 @@ bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner,
return false;
}
if (NS_WARN_IF(aState.is<RemoteWorkerChild::PendingTerminated>()) ||
NS_WARN_IF(aState.is<RemoteWorkerChild::Terminated>())) {
if (NS_WARN_IF(aState.is<RemoteWorkerChild::Canceled>()) ||
NS_WARN_IF(aState.is<RemoteWorkerChild::Killed>())) {
RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR);
mStarted = true;
return true;
@ -368,35 +367,13 @@ bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner,
});
}
// NewRunnableMethod doesn't work here because the template does not appear to
// be able to deal with the owner argument having storage as a RefPtr but
// with the method taking a RefPtr&.
RefPtr<RemoteWorkerChild> owner = aOwner;
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [self = std::move(self), owner = std::move(owner)]() mutable {
MaybeReportServiceWorkerShutdownProgress(self->mArgs);
auto lock = owner->mState.Lock();
auto& state = lock.ref();
if (NS_WARN_IF(!state.is<Running>() && !self->IsTerminationOp())) {
self->RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
if (self->IsTerminationOp()) {
owner->CloseWorkerOnMainThread(state);
} else {
MOZ_ASSERT(state.is<Running>());
RefPtr<WorkerRunnable> workerRunnable =
self->GetRunnable(state.as<Running>().mWorkerPrivate);
if (NS_WARN_IF(!workerRunnable->Dispatch())) {
self->RejectAll(NS_ERROR_FAILURE);
}
}
nsCOMPtr<nsIEventTarget> target = owner->GetOwningEventTarget();
NS_ProxyRelease(__func__, target, owner.forget());
self->StartOnMainThread(owner);
});
mStarted = true;
@ -407,6 +384,33 @@ bool ServiceWorkerOp::MaybeStart(RemoteWorkerChild* aOwner,
return true;
}
void ServiceWorkerOp::StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) {
MaybeReportServiceWorkerShutdownProgress(mArgs);
{
auto lock = aOwner->mState.Lock();
if (NS_WARN_IF(!lock->is<Running>() && !IsTerminationOp())) {
RejectAll(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
}
if (IsTerminationOp()) {
aOwner->CloseWorkerOnMainThread();
} else {
auto lock = aOwner->mState.Lock();
MOZ_ASSERT(lock->is<Running>());
RefPtr<WorkerRunnable> workerRunnable =
GetRunnable(lock->as<Running>().mWorkerPrivate);
if (NS_WARN_IF(!workerRunnable->Dispatch())) {
RejectAll(NS_ERROR_FAILURE);
}
}
}
void ServiceWorkerOp::Cancel() { RejectAll(NS_ERROR_DOM_ABORT_ERR); }
ServiceWorkerOp::ServiceWorkerOp(
@ -1215,11 +1219,6 @@ FetchEventOp::~FetchEventOp() {
NS_ERROR_DOM_ABORT_ERR,
FetchEventTimeStamps(mFetchHandlerStart, mFetchHandlerFinish)),
__func__);
if (mActor) {
NS_ProxyRelease("FetchEventOp::mActor", RemoteWorkerService::Thread(),
mActor.forget());
}
}
void FetchEventOp::RejectAll(nsresult aStatus) {

Просмотреть файл

@ -50,6 +50,8 @@ class ServiceWorkerOp : public RemoteWorkerChild::Op {
bool MaybeStart(RemoteWorkerChild* aOwner,
RemoteWorkerChild::State& aState) final;
void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) final;
void Cancel() final;
protected:

Просмотреть файл

@ -116,7 +116,7 @@ class ReportErrorRunnable final : public WorkerDebuggeeRunnable {
// the ServiceWorkerManager to report on any controlled documents.
if (aWorkerPrivate->IsServiceWorker()) {
RefPtr<RemoteWorkerChild> actor(
aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
aWorkerPrivate->GetRemoteWorkerController());
Unused << NS_WARN_IF(!actor);
@ -195,7 +195,7 @@ class ReportGenericErrorRunnable final : public WorkerDebuggeeRunnable {
if (aWorkerPrivate->IsServiceWorker()) {
RefPtr<RemoteWorkerChild> actor(
aWorkerPrivate->GetRemoteWorkerControllerWeakRef());
aWorkerPrivate->GetRemoteWorkerController());
Unused << NS_WARN_IF(!actor);

Просмотреть файл

@ -1741,6 +1741,8 @@ bool WorkerPrivate::Start() {
// aCx is null when called from the finalizer
bool WorkerPrivate::Notify(WorkerStatus aStatus) {
AssertIsOnParentThread();
// This method is only called for Canceling or later.
MOZ_DIAGNOSTIC_ASSERT(aStatus >= Canceling);
bool pending;
{
@ -1754,6 +1756,11 @@ bool WorkerPrivate::Notify(WorkerStatus aStatus) {
mParentStatus = aStatus;
}
if (mCancellationCallback) {
mCancellationCallback(!pending);
mCancellationCallback = nullptr;
}
if (pending) {
#ifdef DEBUG
{
@ -2308,7 +2315,9 @@ WorkerPrivate::WorkerPrivate(
enum WorkerType aWorkerType, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo& aLoadInfo,
nsString&& aId, const nsID& aAgentClusterId,
const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy)
const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy,
CancellationCallback&& aCancellationCallback,
TerminationCallback&& aTerminationCallback)
: mMutex("WorkerPrivate Mutex"),
mCondVar(mMutex, "WorkerPrivate CondVar"),
mParent(aParent),
@ -2317,6 +2326,8 @@ WorkerPrivate::WorkerPrivate(
mCredentialsMode(aRequestCredentials),
mWorkerType(aWorkerType), // If the worker runs as a script or a module
mWorkerKind(aWorkerKind),
mCancellationCallback(std::move(aCancellationCallback)),
mTerminationCallback(std::move(aTerminationCallback)),
mLoadInfo(std::move(aLoadInfo)),
mDebugger(nullptr),
mJSContext(nullptr),
@ -2551,24 +2562,15 @@ WorkerPrivate::ComputeAgentClusterIdAndCoop(WorkerPrivate* aParent,
return {nsID::GenerateUUID(), agentClusterCoop};
}
already_AddRefed<WorkerPrivate> WorkerPrivate::Constructor(
JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker,
WorkerKind aWorkerKind, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo,
ErrorResult& aRv, nsString aId) {
return WorkerPrivate::Constructor(
aCx, aScriptURL, aIsChromeWorker, aWorkerKind, RequestCredentials::Omit,
WorkerType::Classic, aWorkerName, aServiceWorkerScope, aLoadInfo, aRv,
std::move(aId));
}
// static
already_AddRefed<WorkerPrivate> WorkerPrivate::Constructor(
JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker,
WorkerKind aWorkerKind, RequestCredentials aRequestCredentials,
enum WorkerType aWorkerType, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo,
ErrorResult& aRv, nsString aId) {
ErrorResult& aRv, nsString aId,
CancellationCallback&& aCancellationCallback,
TerminationCallback&& aTerminationCallback) {
WorkerPrivate* parent =
NS_IsMainThread() ? nullptr : GetCurrentThreadWorkerPrivate();
@ -2632,7 +2634,8 @@ already_AddRefed<WorkerPrivate> WorkerPrivate::Constructor(
RefPtr<WorkerPrivate> worker = new WorkerPrivate(
parent, aScriptURL, aIsChromeWorker, aWorkerKind, aRequestCredentials,
aWorkerType, aWorkerName, aServiceWorkerScope, *aLoadInfo, std::move(aId),
idAndCoop.mId, idAndCoop.mCoop);
idAndCoop.mId, idAndCoop.mCoop, std::move(aCancellationCallback),
std::move(aTerminationCallback));
// Gecko contexts always have an explicitly-set default locale (set by
// XPJSRuntime::Initialize for the main thread, set by
@ -5703,26 +5706,11 @@ RemoteWorkerChild* WorkerPrivate::GetRemoteWorkerController() {
return mRemoteWorkerController;
}
void WorkerPrivate::SetRemoteWorkerControllerWeakRef(
ThreadSafeWeakPtr<RemoteWorkerChild> aWeakRef) {
MOZ_ASSERT(!aWeakRef.IsNull());
MOZ_ASSERT(mRemoteWorkerControllerWeakRef.IsNull());
MOZ_ASSERT(IsServiceWorker());
mRemoteWorkerControllerWeakRef = std::move(aWeakRef);
}
ThreadSafeWeakPtr<RemoteWorkerChild>
WorkerPrivate::GetRemoteWorkerControllerWeakRef() {
MOZ_ASSERT(IsServiceWorker());
return mRemoteWorkerControllerWeakRef;
}
RefPtr<GenericPromise> WorkerPrivate::SetServiceWorkerSkipWaitingFlag() {
AssertIsOnWorkerThread();
MOZ_ASSERT(IsServiceWorker());
RefPtr<RemoteWorkerChild> rwc(mRemoteWorkerControllerWeakRef);
RefPtr<RemoteWorkerChild> rwc = mRemoteWorkerController;
if (!rwc) {
return GenericPromise::CreateAndReject(NS_ERROR_DOM_ABORT_ERR, __func__);
@ -5731,9 +5719,6 @@ RefPtr<GenericPromise> WorkerPrivate::SetServiceWorkerSkipWaitingFlag() {
RefPtr<GenericPromise> promise =
rwc->MaybeSendSetServiceWorkerSkipWaitingFlag();
NS_ProxyRelease("WorkerPrivate::mRemoteWorkerControllerWeakRef",
RemoteWorkerService::Thread(), rwc.forget());
return promise;
}

Просмотреть файл

@ -130,6 +130,78 @@ class WorkerPrivate final
: public RelativeTimeline,
public SupportsCheckedUnsafePtr<CheckIf<DiagnosticAssertEnabled>> {
public:
// Callback invoked on the parent thread when the worker's cancellation is
// about to be requested. This covers both calls to
// WorkerPrivate::Cancel() by the owner as well as self-initiated cancellation
// due to top-level script evaluation failing or close() being invoked on the
// global scope for Dedicated and Shared workers, but not Service Workers as
// they do not expose a close() method.
//
// ### Parent-Initiated Cancellation
//
// When WorkerPrivate::Cancel is invoked on the parent thread (by the binding
// exposed Worker::Terminate), this callback is invoked synchronously inside
// that call.
//
// ### Worker Self-Cancellation
//
// When a worker initiates self-cancellation, the worker's notification to the
// parent thread is a non-blocking, async mechanism triggered by
// `WorkerPrivate::DispatchCancelingRunnable`.
//
// Self-cancellation races a normally scheduled runnable against a timer that
// is scheduled against the parent. The 2 paths initiated by
// DispatchCancelingRunnable are:
//
// 1. A CancelingRunnable is dispatched at the worker's normal event target to
// wait for the event loop to be clear of runnables. When the
// CancelingRunnable runs it will dispatch a CancelingOnParentRunnable to
// its parent which is a normal, non-control WorkerDebuggeeRunnable to
// ensure that any postMessages to the parent or similar events get a
// chance to be processed prior to cancellation. The timer scheduled in
// the next bullet will not be canceled unless
//
// 2. A CancelingWithTimeoutOnParentRunnable control runnable is dispatched
// to the parent to schedule a timer which will (also) fire on the parent
// thread. This handles the case where the worker does not yield
// control-flow, and so the normal runnable scheduled above does not get to
// run in a timely fashion. Because this is a control runnable, if the
// parent is a worker then the runnable will be processed with urgency.
// However, if the worker is top-level, then the control-like throttled
// WorkerPrivate::mMainThreadEventTarget will end up getting used which is
// nsIRunnablePriority::PRIORITY_MEDIUMHIGH and distinct from the
// mMainThreadDebuggeeEventTarget which most runnables (like postMessage)
// use.
//
// The timer will explicitly use the control event target if the parent is
// a worker and the implicit event target (via `NS_NewTimer()`) otherwise.
// The callback is CancelingTimerCallback which just calls
// WorkerPrivate::Cancel.
using CancellationCallback = std::function<void(bool aEverRan)>;
// Callback invoked on the parent just prior to dropping the worker thread's
// strong reference that keeps the WorkerPrivate alive while the worker thread
// is running. This does not provide a guarantee that the underlying thread
// has fully shutdown, just that the worker logic has fully shutdown.
//
// ### Details
//
// The last thing the worker thread's WorkerThreadPrimaryRunnable does before
// initiating the shutdown of the underlying thread is call ScheduleDeletion.
// ScheduleDeletion dispatches a runnable to the parent to notify it that the
// worker has completed its work and will never touch the WorkerPrivate again
// and that the strong self-reference can be dropped.
//
// For parents that are themselves workers, this will be done by
// WorkerFinishedRunnable which is a WorkerControlRunnable, ensuring that this
// is processed in a timely fashion. For main-thread parents,
// TopLevelWorkerFinishedRunnable will be used and sent via
// mMainThreadEventTargetForMessaging which is a weird ThrottledEventQueue
// which does not provide any ordering guarantees relative to
// mMainThreadDebuggeeEventTarget, so if you want those, you need to enhance
// things.
using TerminationCallback = std::function<void(void)>;
struct LocationInfo {
nsCString mHref;
nsCString mProtocol;
@ -149,13 +221,9 @@ class WorkerPrivate final
WorkerKind aWorkerKind, RequestCredentials aRequestCredentials,
const WorkerType aWorkerType, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo,
ErrorResult& aRv, nsString aId = u""_ns);
static already_AddRefed<WorkerPrivate> Constructor(
JSContext* aCx, const nsAString& aScriptURL, bool aIsChromeWorker,
WorkerKind aWorkerKind, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo* aLoadInfo,
ErrorResult& aRv, nsString aId = u""_ns);
ErrorResult& aRv, nsString aId = u""_ns,
CancellationCallback&& aCancellationCallback = {},
TerminationCallback&& aTerminationCallback = {});
enum LoadGroupBehavior { InheritLoadGroup, OverrideLoadGroup };
@ -171,6 +239,12 @@ class WorkerPrivate final
void ClearSelfAndParentEventTargetRef() {
AssertIsOnParentThread();
MOZ_ASSERT(mSelfRef);
if (mTerminationCallback) {
mTerminationCallback();
mTerminationCallback = nullptr;
}
mParentEventTargetRef = nullptr;
mSelfRef = nullptr;
}
@ -910,11 +984,6 @@ class WorkerPrivate final
void SetRemoteWorkerController(RemoteWorkerChild* aController);
void SetRemoteWorkerControllerWeakRef(
ThreadSafeWeakPtr<RemoteWorkerChild> aWeakRef);
ThreadSafeWeakPtr<RemoteWorkerChild> GetRemoteWorkerControllerWeakRef();
RefPtr<GenericPromise> SetServiceWorkerSkipWaitingFlag();
// We can assume that an nsPIDOMWindow will be available for Freeze, Thaw
@ -1068,7 +1137,9 @@ class WorkerPrivate final
enum WorkerType aWorkerType, const nsAString& aWorkerName,
const nsACString& aServiceWorkerScope, WorkerLoadInfo& aLoadInfo,
nsString&& aId, const nsID& aAgentClusterId,
const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy);
const nsILoadInfo::CrossOriginOpenerPolicy aAgentClusterOpenerPolicy,
CancellationCallback&& aCancellationCallback,
TerminationCallback&& aTerminationCallback);
~WorkerPrivate();
@ -1239,6 +1310,13 @@ class WorkerPrivate final
RefPtr<Worker> mParentEventTargetRef;
RefPtr<WorkerPrivate> mSelfRef;
CancellationCallback mCancellationCallback;
// The termination callback is passed into the constructor on the parent
// thread and invoked by `ClearSelfAndParentEventTargetRef` just before it
// drops its self-ref.
TerminationCallback mTerminationCallback;
// The lifetime of these objects within LoadInfo is managed explicitly;
// they do not need to be cycle collected.
WorkerLoadInfo mLoadInfo;
@ -1302,12 +1380,10 @@ class WorkerPrivate final
// Protected by mMutex.
nsTArray<RefPtr<WorkerRunnable>> mPreStartRunnables MOZ_GUARDED_BY(mMutex);
// Only touched on the parent thread. This is set only if IsSharedWorker().
// Only touched on the parent thread. Used for both SharedWorker and
// ServiceWorker RemoteWorkers.
RefPtr<RemoteWorkerChild> mRemoteWorkerController;
// This is set only if IsServiceWorker().
ThreadSafeWeakPtr<RemoteWorkerChild> mRemoteWorkerControllerWeakRef;
JS::UniqueChars mDefaultLocale; // nulled during worker JSContext init
TimeStamp mKillTime;
WorkerStatus mParentStatus MOZ_GUARDED_BY(mMutex);

Просмотреть файл

@ -16,7 +16,6 @@
#include "nsIInterfaceRequestor.h"
#include "nsIPrincipal.h"
#include "nsNetUtil.h"
#include "nsProxyRelease.h"
#include "nsThreadUtils.h"
#include "nsXULAppAPI.h"
@ -51,6 +50,13 @@
#include "mozilla/net/CookieJarSettings.h"
#include "mozilla/PermissionManager.h"
mozilla::LazyLogModule gRemoteWorkerChildLog("RemoteWorkerChild");
#ifdef LOG
# undef LOG
#endif
#define LOG(fmt) MOZ_LOG(gRemoteWorkerChildLog, mozilla::LogLevel::Verbose, fmt)
namespace mozilla {
using namespace ipc;
@ -59,46 +65,6 @@ namespace dom {
using workerinternals::ChannelFromScriptURLMainThread;
// TODO: Remove this ad hoc class when bug 1805830 is fixed.
class SelfHolder {
public:
MOZ_IMPLICIT SelfHolder(RemoteWorkerChild* aSelf) : mSelf(aSelf) {
MOZ_ASSERT(mSelf);
}
SelfHolder(const SelfHolder&) = default;
SelfHolder& operator=(const SelfHolder&) = default;
SelfHolder(SelfHolder&&) = default;
SelfHolder& operator=(SelfHolder&&) = default;
~SelfHolder() {
if (!mSelf) {
return;
}
nsCOMPtr<nsISerialEventTarget> target = mSelf->GetOwningEventTarget();
NS_ProxyRelease("SelfHolder::mSelf", target, mSelf.forget());
}
RemoteWorkerChild* get() const {
MOZ_ASSERT(mSelf);
return mSelf.get();
}
RemoteWorkerChild* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN {
return get();
}
bool operator!() { return !mSelf.get(); }
private:
RefPtr<RemoteWorkerChild> mSelf;
};
namespace {
class SharedWorkerInterfaceRequestor final : public nsIInterfaceRequestor {
@ -142,7 +108,7 @@ NS_IMPL_QUERY_INTERFACE(SharedWorkerInterfaceRequestor, nsIInterfaceRequestor)
class MessagePortIdentifierRunnable final : public WorkerRunnable {
public:
MessagePortIdentifierRunnable(WorkerPrivate* aWorkerPrivate,
SelfHolder aActor,
RefPtr<RemoteWorkerChild>& aActor,
const MessagePortIdentifier& aPortIdentifier)
: WorkerRunnable(aWorkerPrivate),
mActor(std::move(aActor)),
@ -154,133 +120,29 @@ class MessagePortIdentifierRunnable final : public WorkerRunnable {
return true;
}
SelfHolder mActor;
RefPtr<RemoteWorkerChild> mActor;
UniqueMessagePortId mPortIdentifier;
};
// This is used to release WeakWorkerRefs which can only have their refcount
// modified on the owning thread (worker thread in this case). It also keeps
// alive the associated WorkerPrivate until the WeakWorkerRef is released.
class ReleaseWorkerRunnable final : public WorkerControlRunnable {
public:
ReleaseWorkerRunnable(RefPtr<WorkerPrivate>&& aWorkerPrivate,
RefPtr<WeakWorkerRef>&& aWeakRef)
: WorkerControlRunnable(aWorkerPrivate, WorkerThreadUnchangedBusyCount),
mWorkerPrivate(std::move(aWorkerPrivate)),
mWeakRef(std::move(aWeakRef)) {
MOZ_ASSERT(mWorkerPrivate);
MOZ_ASSERT(!mWorkerPrivate->IsOnWorkerThread());
MOZ_ASSERT(mWeakRef);
}
private:
~ReleaseWorkerRunnable() { ReleaseMembers(); }
bool WorkerRun(JSContext*, WorkerPrivate*) override {
ReleaseMembers();
return true;
}
nsresult Cancel() override {
// We need to check first if cancel is called twice
nsresult rv = WorkerRunnable::Cancel();
NS_ENSURE_SUCCESS(rv, rv);
ReleaseMembers();
return NS_OK;
}
void ReleaseMembers() {
if (!mWorkerPrivate) {
MOZ_ASSERT(!mWeakRef);
return;
}
mWeakRef = nullptr;
NS_ReleaseOnMainThread("ReleaseWorkerRunnable::mWorkerPrivate",
mWorkerPrivate.forget());
}
RefPtr<WorkerPrivate> mWorkerPrivate;
RefPtr<WeakWorkerRef> mWeakRef;
};
} // anonymous namespace
class RemoteWorkerChild::InitializeWorkerRunnable final
: public WorkerRunnable {
public:
InitializeWorkerRunnable(WorkerPrivate* aWorkerPrivate, SelfHolder aActor)
: WorkerRunnable(aWorkerPrivate), mActor(std::move(aActor)) {
MOZ_ASSERT(mActor);
}
private:
~InitializeWorkerRunnable() { MaybeAbort(); }
bool WorkerRun(JSContext*, WorkerPrivate*) override {
MOZ_ASSERT(mActor);
mActor->InitializeOnWorker();
SelfHolder holder = std::move(mActor);
MOZ_ASSERT(!mActor);
return true;
}
nsresult Cancel() override {
// We need to check first if cancel is called twice
nsresult rv = WorkerRunnable::Cancel();
NS_ENSURE_SUCCESS(rv, rv);
MaybeAbort();
return NS_OK;
}
// Slowly running out of synonyms for cancel, abort, terminate, etc...
void MaybeAbort() {
if (!mActor) {
return;
}
mActor->TransitionStateToTerminated();
mActor->CreationFailedOnAnyThread();
mActor->ShutdownOnWorker();
SelfHolder holder = std::move(mActor);
MOZ_ASSERT(!mActor);
}
// Falsy indicates that WorkerRun or MaybeAbort has already been called.
SelfHolder mActor;
};
RemoteWorkerChild::RemoteWorkerChild(const RemoteWorkerData& aData)
: mState(VariantType<Pending>(), "RemoteWorkerChild::mState"),
mServiceKeepAlive(RemoteWorkerService::MaybeGetKeepAlive()),
mIsServiceWorker(aData.serviceWorkerData().type() ==
OptionalServiceWorkerData::TServiceWorkerData),
mOwningEventTarget(GetCurrentSerialEventTarget()) {
OptionalServiceWorkerData::TServiceWorkerData) {
MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
MOZ_ASSERT(mOwningEventTarget);
}
RemoteWorkerChild::~RemoteWorkerChild() {
#ifdef DEBUG
auto lock = mState.Lock();
MOZ_ASSERT(lock->is<Terminated>());
MOZ_ASSERT(lock->is<Killed>());
#endif
}
nsISerialEventTarget* RemoteWorkerChild::GetOwningEventTarget() const {
return mOwningEventTarget;
}
void RemoteWorkerChild::ActorDestroy(ActorDestroyReason) {
auto launcherData = mLauncherData.Access();
launcherData->mIPCActive = false;
Unused << NS_WARN_IF(!launcherData->mTerminationPromise.IsEmpty());
launcherData->mTerminationPromise.RejectIfExists(NS_ERROR_DOM_ABORT_ERR,
@ -288,27 +150,51 @@ void RemoteWorkerChild::ActorDestroy(ActorDestroyReason) {
auto lock = mState.Lock();
Unused << NS_WARN_IF(!lock->is<Terminated>());
*lock = VariantType<Terminated>();
// If the worker hasn't shutdown or begun shutdown, we need to ensure it gets
// canceled.
if (NS_WARN_IF(!lock->is<Killed>() && !lock->is<Canceled>())) {
// In terms of strong references to this RemoteWorkerChild, at this moment:
// - IPC is holding a strong reference that will be dropped in the near
// future after this method returns.
// - If the worker has been started by ExecWorkerOnMainThread, then
// WorkerPrivate::mRemoteWorkerController is a strong reference to us.
// If the worker has not been started, ExecWorker's runnable lambda will
// have a strong reference that will cover the call to
// ExecWorkerOnMainThread.
// - The WorkerPrivate cancellation and termination callbacks will also
// hold strong references, but those callbacks will not outlive the
// WorkerPrivate and are not exposed to callers like
// mRemoteWorkerController is.
//
// Note that this call to RequestWorkerCancellation can still race worker
// cancellation, in which case the strong reference obtained by
// NewRunnableMethod can end up being the last strong reference.
// (RequestWorkerCancellation handles the case that the Worker is already
// canceled if this happens.)
RefPtr<nsIRunnable> runnable =
NewRunnableMethod("RequestWorkerCancellation", this,
&RemoteWorkerChild::RequestWorkerCancellation);
MOZ_ALWAYS_SUCCEEDS(
SchedulerGroup::Dispatch(TaskCategory::Other, runnable.forget()));
}
}
void RemoteWorkerChild::ExecWorker(const RemoteWorkerData& aData) {
#ifdef DEBUG
MOZ_ASSERT(GetOwningEventTarget()->IsOnCurrentThread());
MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread());
auto launcherData = mLauncherData.Access();
MOZ_ASSERT(launcherData->mIPCActive);
MOZ_ASSERT(CanSend());
#endif
SelfHolder self = this;
RefPtr<RemoteWorkerChild> self = this;
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [self = std::move(self), data = aData]() mutable {
nsresult rv = self->ExecWorkerOnMainThread(std::move(data));
if (NS_WARN_IF(NS_FAILED(rv))) {
self->CreationFailedOnAnyThread();
}
// Creation failure will already have been reported via the method
// above internally using ScopeExit.
Unused << NS_WARN_IF(NS_FAILED(rv));
});
MOZ_ALWAYS_SUCCEEDS(
@ -318,10 +204,13 @@ void RemoteWorkerChild::ExecWorker(const RemoteWorkerData& aData) {
nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
MOZ_ASSERT(NS_IsMainThread());
// Ensure that the IndexedDatabaseManager is initialized
// Ensure that the IndexedDatabaseManager is initialized so that if any
// workers do any IndexedDB calls that all of IDB's prefs/etc. are
// initialized.
Unused << NS_WARN_IF(!IndexedDatabaseManager::GetOrCreate());
auto scopeExit = MakeScopeExit([&] { TransitionStateToTerminated(); });
auto scopeExit =
MakeScopeExit([&] { ExceptionalErrorTransitionDuringExecWorker(); });
// Verify the the RemoteWorker should be really allowed to run in this
// process, and fail if it shouldn't (This shouldn't normally happen,
@ -449,11 +338,21 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
jsapi.Init();
ErrorResult error;
RefPtr<RemoteWorkerChild> self = this;
RefPtr<WorkerPrivate> workerPrivate = WorkerPrivate::Constructor(
jsapi.cx(), aData.originalScriptURL(), false,
mIsServiceWorker ? WorkerKindService : WorkerKindShared,
aData.credentials(), aData.type(), aData.name(), VoidCString(), &info,
error, std::move(workerPrivateId));
error, std::move(workerPrivateId),
[self](bool aEverRan) {
self->OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled();
},
// This will be invoked here on the main thread when the worker is already
// fully shutdown. This replaces a prior approach where we would
// begin to transition when the worker thread would reach the Canceling
// state. This lambda ensures that we not only wait for the Killing state
// to be reached but that the global shutdown has already occurred.
[self]() { self->TransitionStateFromCanceledToKilled(); });
if (NS_WARN_IF(error.Failed())) {
MOZ_ASSERT(!workerPrivate);
@ -462,16 +361,16 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
return rv;
}
if (mIsServiceWorker) {
RefPtr<RemoteWorkerChild> self = this;
workerPrivate->SetRemoteWorkerControllerWeakRef(
ThreadSafeWeakPtr<RemoteWorkerChild>(self));
} else {
workerPrivate->SetRemoteWorkerController(this);
}
workerPrivate->SetRemoteWorkerController(this);
RefPtr<InitializeWorkerRunnable> runnable =
new InitializeWorkerRunnable(workerPrivate, SelfHolder(this));
// This wants to run as a normal task sequentially after the top level script
// evaluation, so the hybrid target is the correct choice between hybrid and
// `ControlEventTarget`.
nsCOMPtr<nsISerialEventTarget> workerTarget =
workerPrivate->HybridEventTarget();
nsCOMPtr<nsIRunnable> runnable = NewCancelableRunnableMethod(
"InitialzeOnWorker", this, &RemoteWorkerChild::InitializeOnWorker);
{
MOZ_ASSERT(workerPrivate);
@ -481,21 +380,11 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
}
if (mIsServiceWorker) {
SelfHolder self = this;
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [initializeWorkerRunnable = std::move(runnable),
self = std::move(self)] {
// Checking RemoteWorkerChild.mState
bool isPending;
{
auto lock = self->mState.Lock();
isPending = lock->is<Pending>();
}
if (NS_WARN_IF(!isPending || !initializeWorkerRunnable->Dispatch())) {
self->TransitionStateToTerminated();
self->CreationFailedOnAnyThread();
}
__func__, [workerTarget,
initializeWorkerRunnable = std::move(runnable)]() mutable {
Unused << NS_WARN_IF(NS_FAILED(
workerTarget->Dispatch(initializeWorkerRunnable.forget())));
});
RefPtr<PermissionManager> permissionManager =
@ -505,7 +394,7 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
}
permissionManager->WhenPermissionsAvailable(principal, r);
} else {
if (NS_WARN_IF(!runnable->Dispatch())) {
if (NS_WARN_IF(NS_FAILED(workerTarget->Dispatch(runnable.forget())))) {
rv = NS_ERROR_FAILURE;
return rv;
}
@ -516,91 +405,44 @@ nsresult RemoteWorkerChild::ExecWorkerOnMainThread(RemoteWorkerData&& aData) {
return NS_OK;
}
void RemoteWorkerChild::InitializeOnWorker() {
RefPtr<WorkerPrivate> workerPrivate;
void RemoteWorkerChild::RequestWorkerCancellation() {
MOZ_ASSERT(NS_IsMainThread());
LOG(("RequestWorkerCancellation[this=%p]", this));
// We want to ensure that we've requested the worker be canceled. So if the
// worker is running, cancel it. We can't do this with the lock held,
// however, because our lambdas will want to manipulate the state.
RefPtr<WorkerPrivate> cancelWith;
{
auto lock = mState.Lock();
if (lock->is<PendingTerminated>()) {
TransitionStateToTerminated(lock.ref());
ShutdownOnWorker();
return;
if (lock->is<Pending>()) {
cancelWith = lock->as<Pending>().mWorkerPrivate;
} else if (lock->is<Running>()) {
cancelWith = lock->as<Running>().mWorkerPrivate;
}
// XXX: Are we sure we cannot be in any other state here?
// We are executed as part of the InitializeWorkerRunnable
// which is scheduled from ExecWorkerOnMainThread, so we
// assume that our state remains <Pending> as it was before
// the dispatch. There seem to be no crashes here, so for
// now this assumption holds apparently.
workerPrivate = std::move(lock->as<Pending>().mWorkerPrivate);
}
MOZ_ASSERT(workerPrivate);
workerPrivate->AssertIsOnWorkerThread();
RefPtr<RemoteWorkerChild> self = this;
ThreadSafeWeakPtr<RemoteWorkerChild> selfWeakRef(self);
auto scopeExit = MakeScopeExit([&] {
MOZ_ASSERT(self);
NS_ProxyRelease(__func__, mOwningEventTarget, self.forget());
});
// Let RemoteWorkerChild own the WorkerPrivate; RemoteWorkerChild's state
// transitions should guarantee the WorkerPrivate is cleaned up correctly.
// This also reduces some complexity around thread lifetimes guarantees that
// RemoteWorkerChild's state transitions rely on (e.g. the worker thread
// terminating unexpectedly).
RefPtr<StrongWorkerRef> strongRef =
StrongWorkerRef::Create(workerPrivate, __func__);
RefPtr<WeakWorkerRef> workerRef = WeakWorkerRef::Create(
workerPrivate, [selfWeakRef = std::move(selfWeakRef),
strongRef = std::move(strongRef)]() mutable {
RefPtr<RemoteWorkerChild> self(selfWeakRef);
if (NS_WARN_IF(!self)) {
return;
}
self->TransitionStateToTerminated();
self->ShutdownOnWorker();
nsCOMPtr<nsISerialEventTarget> target = self->GetOwningEventTarget();
NS_ProxyRelease(__func__, target, self.forget());
});
if (NS_WARN_IF(!workerRef)) {
TransitionStateToTerminated();
CreationFailedOnAnyThread();
ShutdownOnWorker();
return;
if (cancelWith) {
cancelWith->Cancel();
}
TransitionStateToRunning(workerPrivate.forget(), workerRef.forget());
CreationSucceededOnAnyThread();
}
void RemoteWorkerChild::ShutdownOnWorker() {
RefPtr<RemoteWorkerChild> self = this;
// This method will be invoked on the worker after the top-level
// CompileScriptRunnable task has succeeded and as long as the worker has not
// been closed/canceled. There are edge-cases related to cancellation, but we
// have our caller ensure that we are only called as long as the worker's state
// is Running.
//
// (https://bugzilla.mozilla.org/show_bug.cgi?id=1800659 will eliminate
// cancellation, and the documentation around that bug / in design documents
// helps provide more context about this.)
void RemoteWorkerChild::InitializeOnWorker() {
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction(__func__, [self = std::move(self)] {
auto launcherData = self->mLauncherData.Access();
if (!launcherData->mIPCActive) {
return;
}
launcherData->mIPCActive = false;
Unused << self->SendClose();
});
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
NewRunnableMethod("TransitionStateToRunning", this,
&RemoteWorkerChild::TransitionStateToRunning);
MOZ_ALWAYS_SUCCEEDS(
SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
}
RefPtr<GenericNonExclusivePromise> RemoteWorkerChild::GetTerminationPromise() {
@ -622,7 +464,7 @@ void RemoteWorkerChild::CreationSucceededOrFailedOnAnyThread(
{
auto lock = mState.Lock();
MOZ_ASSERT_IF(aDidCreationSucceed, lock->is<Running>());
MOZ_ASSERT_IF(!aDidCreationSucceed, lock->is<Terminated>());
MOZ_ASSERT_IF(!aDidCreationSucceed, lock->is<Killed>());
}
#endif
@ -633,37 +475,47 @@ void RemoteWorkerChild::CreationSucceededOrFailedOnAnyThread(
[self = std::move(self), didCreationSucceed = aDidCreationSucceed] {
auto launcherData = self->mLauncherData.Access();
if (!launcherData->mIPCActive) {
if (!self->CanSend() || launcherData->mDidSendCreated) {
return;
}
Unused << self->SendCreated(didCreationSucceed);
launcherData->mDidSendCreated = true;
});
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void RemoteWorkerChild::CloseWorkerOnMainThread(State& aState) {
void RemoteWorkerChild::CloseWorkerOnMainThread() {
AssertIsOnMainThread();
MOZ_ASSERT(!aState.is<PendingTerminated>());
// WeakWorkerRef callback will be asynchronously invoked after
// WorkerPrivate::Cancel.
LOG(("CloseWorkerOnMainThread[this=%p]", this));
if (aState.is<Pending>()) {
// SharedWorkerOp::MaybeStart would not block terminate operation while
// RemoteWorkerChild::mState is still Pending, and the
// Pending.mWorkerPrivate is still nullptr. For the case, just switching the
// State to PendingTerminated.
if (aState.as<Pending>().mWorkerPrivate) {
aState.as<Pending>().mWorkerPrivate->Cancel();
// We can't hold the state lock while calling WorkerPrivate::Cancel because
// the lambda callback will want to touch the state, so save off the
// WorkerPrivate so we can cancel it (if we need to cancel it).
RefPtr<WorkerPrivate> cancelWith;
{
auto lock = mState.Lock();
if (lock->is<Pending>()) {
cancelWith = lock->as<Pending>().mWorkerPrivate;
// There should be no way for this code to run before we
// ExecWorkerOnMainThread runs, which means that either it should have
// set a WorkerPrivate on Pending, or its error handling should already
// have transitioned us to Canceled and Killing in that order. (It's
// also possible that it assigned a WorkerPrivate and subsequently we
// transitioned to Running, which would put us in the next branch.)
MOZ_DIAGNOSTIC_ASSERT(cancelWith);
} else if (lock->is<Running>()) {
cancelWith = lock->as<Running>().mWorkerPrivate;
}
TransitionStateToPendingTerminated(aState);
return;
}
if (aState.is<Running>()) {
aState.as<Running>().mWorkerPrivate->Cancel();
// It's very okay for us to not have a WorkerPrivate here if we've already
// canceled the worker or if errors happened.
if (cancelWith) {
cancelWith->Cancel();
}
}
@ -671,11 +523,9 @@ void RemoteWorkerChild::CloseWorkerOnMainThread(State& aState) {
* Error reporting method
*/
void RemoteWorkerChild::ErrorPropagation(const ErrorValue& aValue) {
MOZ_ASSERT(GetOwningEventTarget()->IsOnCurrentThread());
MOZ_ASSERT(GetActorEventTarget()->IsOnCurrentThread());
auto launcherData = mLauncherData.Access();
if (!launcherData->mIPCActive) {
if (!CanSend()) {
return;
}
@ -690,7 +540,7 @@ void RemoteWorkerChild::ErrorPropagationDispatch(nsresult aError) {
"RemoteWorkerChild::ErrorPropagationDispatch",
[self = std::move(self), aError]() { self->ErrorPropagation(aError); });
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void RemoteWorkerChild::ErrorPropagationOnMainThread(
@ -716,22 +566,20 @@ void RemoteWorkerChild::ErrorPropagationOnMainThread(
"RemoteWorkerChild::ErrorPropagationOnMainThread",
[self = std::move(self), value]() { self->ErrorPropagation(value); });
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void RemoteWorkerChild::NotifyLock(bool aCreated) {
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction(__func__, [self = RefPtr(this), aCreated] {
auto launcherData = self->mLauncherData.Access();
if (!launcherData->mIPCActive) {
if (!self->CanSend()) {
return;
}
Unused << self->SendNotifyLock(aCreated);
});
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void RemoteWorkerChild::FlushReportsOnMainThread(
@ -761,115 +609,154 @@ void RemoteWorkerChild::FlushReportsOnMainThread(
*/
RemoteWorkerChild::WorkerPrivateAccessibleState::
~WorkerPrivateAccessibleState() {
// We should now only be performing state transitions on the main thread, so
// we should assert we're only releasing on the main thread.
MOZ_ASSERT(!mWorkerPrivate || NS_IsMainThread());
// mWorkerPrivate can be safely released on the main thread.
if (!mWorkerPrivate || NS_IsMainThread()) {
return;
}
// But as a backstop, do proxy the release to the main thread.
NS_ReleaseOnMainThread(
"RemoteWorkerChild::WorkerPrivateAccessibleState::mWorkerPrivate",
mWorkerPrivate.forget());
}
RemoteWorkerChild::Running::~Running() {
// This can occur if the current object is a temporary.
if (!mWorkerPrivate) {
return;
}
void RemoteWorkerChild::
OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled() {
auto lock = mState.Lock();
if (mWorkerPrivate->IsOnWorkerThread()) {
return;
}
LOG(("TransitionStateFromPendingOrRunningToCanceled[this=%p]", this));
RefPtr<ReleaseWorkerRunnable> runnable = new ReleaseWorkerRunnable(
std::move(mWorkerPrivate), std::move(mWorkerRef));
nsCOMPtr<nsIRunnable> dispatchWorkerRunnableRunnable =
NS_NewRunnableFunction(__func__, [runnable = std::move(runnable)] {
Unused << NS_WARN_IF(!runnable->Dispatch());
});
if (NS_IsMainThread()) {
dispatchWorkerRunnableRunnable->Run();
if (lock->is<Pending>()) {
TransitionStateFromPendingToCanceled(lock.ref());
} else if (lock->is<Running>()) {
*lock = VariantType<Canceled>();
} else {
SchedulerGroup::Dispatch(TaskCategory::Other,
dispatchWorkerRunnableRunnable.forget());
MOZ_ASSERT(false, "State should have been Pending or Running");
}
}
void RemoteWorkerChild::TransitionStateToPendingTerminated(State& aState) {
void RemoteWorkerChild::TransitionStateFromPendingToCanceled(State& aState) {
AssertIsOnMainThread();
MOZ_ASSERT(aState.is<Pending>());
LOG(("TransitionStateFromPendingToCanceled[this=%p]", this));
CancelAllPendingOps(aState);
aState = VariantType<PendingTerminated>();
aState = VariantType<Canceled>();
}
void RemoteWorkerChild::TransitionStateToRunning(
already_AddRefed<WorkerPrivate> aWorkerPrivate,
already_AddRefed<WeakWorkerRef> aWorkerRef) {
RefPtr<WorkerPrivate> workerPrivate = aWorkerPrivate;
MOZ_ASSERT(workerPrivate);
void RemoteWorkerChild::TransitionStateFromCanceledToKilled() {
AssertIsOnMainThread();
RefPtr<WeakWorkerRef> workerRef = aWorkerRef;
MOZ_ASSERT(workerRef);
LOG(("TransitionStateFromCanceledToKilled[this=%p]", this));
auto lock = mState.Lock();
MOZ_ASSERT(lock->is<Canceled>());
// TransitionStateToRunning is supposed to be used only
// in InitializeOnWorker where we know we are <Pending>.
auto pendingOps = std::move(lock->as<Pending>().mPendingOps);
*lock = VariantType<Killed>();
/**
* I'd initialize the WorkerPrivate and WeakWorkerRef in the constructor,
* but mozilla::Variant attempts to call the thread-unsafe `AddRef()` on
* WorkerPrivate.
*/
*lock = VariantType<Running>();
lock->as<Running>().mWorkerPrivate = std::move(workerPrivate);
lock->as<Running>().mWorkerRef = std::move(workerRef);
RefPtr<RemoteWorkerChild> self = this;
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self]() {
auto launcherData = self->mLauncherData.Access();
SelfHolder self = this;
// (We maintain the historical ordering of resolving this promise prior to
// calling SendClose, however the previous code used 2 separate dispatches
// to this thread for the resolve and SendClose, and there inherently
// would be a race between the runnables resulting from the resolved
// promise and the promise containing the call to SendClose. Now it's
// entirely clear that our call to SendClose will effectively run before
// any of the resolved promises are able to do anything.)
launcherData->mTerminationPromise.ResolveIfExists(true, __func__);
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [pendingOps = std::move(pendingOps), self = std::move(self)]() {
for (auto& op : pendingOps) {
// XXX: Is it on purpose that we renew the lock for each pending op ?
auto lock = self->mState.Lock();
if (self->CanSend()) {
Unused << self->SendClose();
}
});
DebugOnly<bool> started = op->MaybeStart(self.get(), lock.ref());
MOZ_ASSERT(started);
}
});
MOZ_ALWAYS_SUCCEEDS(
mOwningEventTarget->Dispatch(r.forget(), NS_DISPATCH_NORMAL));
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
}
void RemoteWorkerChild::TransitionStateToTerminated() {
auto lock = mState.Lock();
void RemoteWorkerChild::TransitionStateToRunning() {
AssertIsOnMainThread();
TransitionStateToTerminated(lock.ref());
}
LOG(("TransitionStateToRunning[this=%p]", this));
void RemoteWorkerChild::TransitionStateToTerminated(State& aState) {
if (aState.is<Pending>()) {
CancelAllPendingOps(aState);
nsTArray<RefPtr<Op>> pendingOps;
{
auto lock = mState.Lock();
// Because this is an async notification sent from the worker to the main
// thread, it's very possible that we've already decided on the main thread
// to transition to the Canceled state, in which case there is nothing for
// us to do here.
if (!lock->is<Pending>()) {
LOG(("State is already not pending in TransitionStateToRunning[this=%p]!",
this));
return;
}
RefPtr<WorkerPrivate> workerPrivate =
std::move(lock->as<Pending>().mWorkerPrivate);
pendingOps = std::move(lock->as<Pending>().mPendingOps);
// Move the worker private into place to avoid gratuitous ref churn; prior
// comments here suggest the Variant can't accept a move.
*lock = VariantType<Running>();
lock->as<Running>().mWorkerPrivate = std::move(workerPrivate);
}
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction(__func__, [self = SelfHolder(this)]() {
auto launcherData = self->mLauncherData.Access();
launcherData->mTerminationPromise.ResolveIfExists(true, __func__);
});
CreationSucceededOnAnyThread();
if (GetOwningEventTarget()->IsOnCurrentThread()) {
r->Run();
RefPtr<RemoteWorkerChild> self = this;
for (auto& op : pendingOps) {
op->StartOnMainThread(self);
}
}
void RemoteWorkerChild::ExceptionalErrorTransitionDuringExecWorker() {
AssertIsOnMainThread();
LOG(("ExceptionalErrorTransitionDuringExecWorker[this=%p]", this));
// This method is called synchronously by ExecWorkerOnMainThread in the event
// of any error. Because we only transition to Running on the main thread
// as the result of a notification from the worker, we know our state will be
// Pending, but mWorkerPrivate may or may not be null, as we may not have
// gotten to spawning the worker.
//
// In the event the worker exists, we need to Cancel() it. We must do this
// without the lock held because our call to Cancel() will invoke the
// cancellation callback we created which will call TransitionStateToCanceled,
// and we can't be holding the lock when that happens.
RefPtr<WorkerPrivate> cancelWith;
{
auto lock = mState.Lock();
MOZ_ASSERT(lock->is<Pending>());
if (lock->is<Pending>()) {
cancelWith = lock->as<Pending>().mWorkerPrivate;
if (!cancelWith) {
// The worker wasn't actually created, so we should synthetically
// transition to canceled and onward. Since we have the lock,
// perform the transition now for clarity, but we'll handle the rest of
// this case after dropping the lock.
TransitionStateFromPendingToCanceled(lock.ref());
}
}
}
if (cancelWith) {
cancelWith->Cancel();
} else {
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
TransitionStateFromCanceledToKilled();
CreationFailedOnAnyThread();
}
aState = VariantType<Terminated>();
}
/**
@ -885,24 +772,18 @@ class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op {
RemoteWorkerChild::State& aState) override {
MOZ_ASSERT(!mStarted);
MOZ_ASSERT(aOwner);
// Thread: We are on the Worker Launcher thread.
auto launcherData = aOwner->mLauncherData.Access();
if (NS_WARN_IF(!launcherData->mIPCActive)) {
Unused << NS_WARN_IF(!aState.is<Terminated>());
#ifdef DEBUG
mStarted = true;
#endif
return true;
}
// Return false, indicating we should queue this op if our current state is
// pending and this isn't a termination op (which should skip the line).
if (aState.is<Pending>() && !IsTerminationOp()) {
return false;
}
if (aState.is<PendingTerminated>() || aState.is<Terminated>()) {
// If the worker is already shutting down (which should be unexpected
// because we should be told new operations after a termination op), just
// return true to indicate the op should be discarded.
if (aState.is<Canceled>() || aState.is<Killed>()) {
#ifdef DEBUG
mStarted = true;
#endif
@ -913,21 +794,20 @@ class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op {
MOZ_ASSERT(aState.is<Running>() || IsTerminationOp());
RefPtr<SharedWorkerOp> self = this;
SelfHolder owner = aOwner;
RefPtr<RemoteWorkerChild> owner = aOwner;
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
__func__, [self = std::move(self), owner = std::move(owner)]() mutable {
{
auto lock = owner->mState.Lock();
if (NS_WARN_IF(lock->is<Terminated>())) {
if (NS_WARN_IF(lock->is<Canceled>() || lock->is<Killed>())) {
self->Cancel();
return;
}
// XXX: All other possible states are ok here?
}
self->Exec(owner);
self->StartOnMainThread(owner);
});
MOZ_ALWAYS_SUCCEEDS(
@ -940,31 +820,17 @@ class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op {
return true;
}
void Cancel() override {
#ifdef DEBUG
mStarted = true;
#endif
}
private:
~SharedWorkerOp() { MOZ_ASSERT(mStarted); }
bool IsTerminationOp() const {
return mOp.type() == RemoteWorkerOp::TRemoteWorkerTerminateOp;
}
void Exec(SelfHolder& aOwner) {
void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) final {
using Running = RemoteWorkerChild::Running;
AssertIsOnMainThread();
auto lock = aOwner->mState.Lock();
if (IsTerminationOp()) {
aOwner->CloseWorkerOnMainThread(lock.ref());
aOwner->CloseWorkerOnMainThread();
return;
}
auto lock = aOwner->mState.Lock();
MOZ_ASSERT(lock->is<Running>());
if (!lock->is<Running>()) {
aOwner->ErrorPropagationDispatch(NS_ERROR_DOM_INVALID_STATE_ERR);
@ -1003,6 +869,19 @@ class RemoteWorkerChild::SharedWorkerOp : public RemoteWorkerChild::Op {
}
}
void Cancel() override {
#ifdef DEBUG
mStarted = true;
#endif
}
private:
~SharedWorkerOp() { MOZ_ASSERT(mStarted); }
bool IsTerminationOp() const {
return mOp.type() == RemoteWorkerOp::TRemoteWorkerTerminateOp;
}
RemoteWorkerOp mOp;
#ifdef DEBUG
@ -1072,9 +951,7 @@ RemoteWorkerChild::MaybeSendSetServiceWorkerSkipWaitingFlag() {
nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(__func__, [self = std::move(
self),
promise] {
auto launcherData = self->mLauncherData.Access();
if (!launcherData->mIPCActive) {
if (!self->CanSend()) {
promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__);
return;
}
@ -1093,7 +970,7 @@ RemoteWorkerChild::MaybeSendSetServiceWorkerSkipWaitingFlag() {
});
});
GetOwningEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
GetActorEventTarget()->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
return promise;
}

Просмотреть файл

@ -15,7 +15,6 @@
#include "mozilla/MozPromise.h"
#include "mozilla/RefPtr.h"
#include "mozilla/ThreadBound.h"
#include "mozilla/ThreadSafeWeakPtr.h"
#include "mozilla/dom/PRemoteWorkerChild.h"
#include "mozilla/dom/ServiceWorkerOpArgs.h"
@ -27,6 +26,7 @@ namespace mozilla::dom {
class ErrorValue;
class FetchEventOpProxyChild;
class RemoteWorkerData;
class RemoteWorkerServiceKeepAlive;
class ServiceWorkerOp;
class UniqueMessagePortId;
class WeakWorkerRef;
@ -40,9 +40,7 @@ class WorkerPrivate;
* created on background threads and other ownership invariants, most of which
* can be relaxed in the future.
*/
class RemoteWorkerChild final
: public SupportsThreadSafeWeakPtr<RemoteWorkerChild>,
public PRemoteWorkerChild {
class RemoteWorkerChild final : public PRemoteWorkerChild {
friend class FetchEventOpProxyChild;
friend class PRemoteWorkerChild;
friend class ServiceWorkerOp;
@ -50,12 +48,13 @@ class RemoteWorkerChild final
~RemoteWorkerChild();
public:
NS_INLINE_DECL_REFCOUNTING(RemoteWorkerChild, final)
// Note that all IPC-using methods must only be invoked on the
// RemoteWorkerService thread which the inherited
// IProtocol::GetActorEventTarget() will return for us.
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerChild, final)
explicit RemoteWorkerChild(const RemoteWorkerData& aData);
nsISerialEventTarget* GetOwningEventTarget() const;
void ExecWorker(const RemoteWorkerData& aData);
void ErrorPropagationOnMainThread(const WorkerErrorReport* aReport,
@ -85,23 +84,62 @@ class RemoteWorkerChild final
RefPtr<WorkerPrivate> mWorkerPrivate;
};
// Initial state, mWorkerPrivate is initially null but will be initialized on
// the main thread by ExecWorkerOnMainThread when the WorkerPrivate is
// created. The state will transition to Running or Canceled, also from the
// main thread.
struct Pending : WorkerPrivateAccessibleState {
nsTArray<RefPtr<Op>> mPendingOps;
};
struct PendingTerminated {};
// Running, with the state transition happening on the main thread as a result
// of the worker successfully processing our initialization runnable,
// indicating that top-level script execution successfully completed. Because
// all of our state transitions happen on the main thread and are posed in
// terms of the main thread's perspective of the worker's state, it's very
// possible for us to skip directly from Pending to Canceled because we decide
// to cancel/terminate the worker prior to it finishing script loading or
// reporting back to us.
struct Running : WorkerPrivateAccessibleState {};
struct Running : WorkerPrivateAccessibleState {
~Running();
RefPtr<WeakWorkerRef> mWorkerRef;
};
// Cancel() has been called on the WorkerPrivate on the main thread by a
// TerminationOp, top-level script evaluation has failed and canceled the
// worker, or in the case of a SharedWorker, close() has been called on
// the global scope by content code and the worker has advanced to the
// Canceling state. (Dedicated Workers can also self close, but they will
// never be RemoteWorkers. Although a SharedWorker can own DedicatedWorkers.)
// Browser shutdown will result in a TerminationOp thanks to use of a shutdown
// blocker in the parent, so the RuntimeService shouldn't get involved, but we
// would also handle that case acceptably too.
//
// Because worker self-closing is still handled by dispatching a runnable to
// the main thread to effectively call WorkerPrivate::Cancel(), there isn't
// a race between a worker deciding to self-close and our termination ops.
//
// In this state, we have dropped the reference to the WorkerPrivate and will
// no longer be dispatching runnables to the worker. We wait in this state
// until the termination lambda is invoked letting us know that the worker has
// entirely shutdown and we can advanced to the Killed state.
struct Canceled {};
struct Terminated {};
// The worker termination lambda has been invoked and we know the Worker is
// entirely shutdown. (Inherently it is possible for us to advance to this
// state while the nsThread for the worker is still in the process of
// shutting down, but no more worker code will run on it.)
//
// This name is chosen to match the Worker's own state model.
struct Killed {};
using State = Variant<Pending, Running, PendingTerminated, Terminated>;
using State = Variant<Pending, Running, Canceled, Killed>;
// The state of the WorkerPrivate as perceived by the owner on the main
// thread. All state transitions now happen on the main thread, but the
// Worker Launcher thread will consult the state and will directly append ops
// to the Pending queue
DataMutex<State> mState;
const RefPtr<RemoteWorkerServiceKeepAlive> mServiceKeepAlive;
class Op {
public:
NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
@ -110,6 +148,8 @@ class RemoteWorkerChild final
virtual bool MaybeStart(RemoteWorkerChild* aOwner, State& aState) = 0;
virtual void StartOnMainThread(RefPtr<RemoteWorkerChild>& aOwner) = 0;
virtual void Cancel() = 0;
};
@ -129,9 +169,11 @@ class RemoteWorkerChild final
nsresult ExecWorkerOnMainThread(RemoteWorkerData&& aData);
void InitializeOnWorker();
void ExceptionalErrorTransitionDuringExecWorker();
void ShutdownOnWorker();
void RequestWorkerCancellation();
void InitializeOnWorker();
void CreationSucceededOnAnyThread();
@ -139,16 +181,31 @@ class RemoteWorkerChild final
void CreationSucceededOrFailedOnAnyThread(bool aDidCreationSucceed);
void CloseWorkerOnMainThread(State& aState);
// Cancels the worker if it has been started and ensures that we transition
// to the Terminated state once the worker has been terminated or we have
// ensured that it will never start.
void CloseWorkerOnMainThread();
void ErrorPropagation(const ErrorValue& aValue);
void ErrorPropagationDispatch(nsresult aError);
void TransitionStateToPendingTerminated(State& aState);
// When the WorkerPrivate Cancellation lambda is invoked, it's possible that
// we have not yet advanced to running from pending, so we could be in either
// state. This method is expected to be called by the Workers' cancellation
// lambda and will obtain the lock and call the
// TransitionStateFromPendingToCanceled if appropriate. Otherwise it will
// directly move from the running state to the canceled state which does not
// require additional cleanup.
void OnWorkerCancellationTransitionStateFromPendingOrRunningToCanceled();
// A helper used by the above method by the worker cancellation lambda if the
// the worker hasn't started running, or in exceptional cases where we bail
// out of the ExecWorker method early. The caller must be holding the lock
// (in order to pass in the state).
void TransitionStateFromPendingToCanceled(State& aState);
void TransitionStateFromCanceledToKilled();
void TransitionStateToRunning(already_AddRefed<WorkerPrivate> aWorkerPrivate,
already_AddRefed<WeakWorkerRef> aWorkerRef);
void TransitionStateToRunning();
void TransitionStateToTerminated();
@ -159,14 +216,15 @@ class RemoteWorkerChild final
void MaybeStartOp(RefPtr<Op>&& aOp);
const bool mIsServiceWorker;
const nsCOMPtr<nsISerialEventTarget> mOwningEventTarget;
// Touched on main-thread only.
nsTArray<uint64_t> mWindowIDs;
struct LauncherBoundData {
bool mIPCActive = true;
MozPromiseHolder<GenericNonExclusivePromise> mTerminationPromise;
// Flag to ensure we report creation at most once. This could be cleaned up
// further.
bool mDidSendCreated = false;
};
ThreadBound<LauncherBoundData> mLauncherData;

Просмотреть файл

@ -36,6 +36,95 @@ StaticRefPtr<RemoteWorkerService> sRemoteWorkerService;
} // namespace
/**
* Block shutdown until the RemoteWorkers have shutdown so that we do not try
* and shutdown the RemoteWorkerService "Worker Launcher" thread until they have
* cleanly shutdown.
*
* Note that this shutdown blocker is not used to initiate shutdown of any of
* the workers directly; their shutdown is initiated from PBackground in the
* parent process. The shutdown blocker just exists to avoid races around
* shutting down the worker launcher thread after all of the workers have
* shutdown and torn down their actors.
*
* Currently, it should be the case that the ContentParent should want to keep
* the content processes alive until the RemoteWorkers have all reported their
* shutdown over IPC (on the "Worker Launcher" thread). So for an orderly
* content process shutdown that is waiting for there to no longer be a reason
* to keep the content process alive, this blocker should only hang around for
* a brief period of time, helping smooth out lifecycle edge cases.
*
* In the event the content process is trying to shutdown while the
* RemoteWorkers think they should still be alive, it's possible that this
* blocker could expose the relevant logic error in the parent process if no
* attempt is made to shutdown the RemoteWorker.
*/
class RemoteWorkerServiceShutdownBlocker final
: public nsIAsyncShutdownBlocker {
~RemoteWorkerServiceShutdownBlocker() = default;
public:
RemoteWorkerServiceShutdownBlocker(RemoteWorkerService* aService,
nsIAsyncShutdownClient* aShutdownClient)
: mService(aService), mShutdownClient(aShutdownClient) {
nsAutoString blockerName;
MOZ_ALWAYS_SUCCEEDS(GetName(blockerName));
MOZ_ALWAYS_SUCCEEDS(mShutdownClient->AddBlocker(
this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__, blockerName));
}
void RemoteWorkersAllGoneAllowShutdown() {
mShutdownClient->RemoveBlocker(this);
mShutdownClient = nullptr;
mService->FinishShutdown();
mService = nullptr;
}
NS_IMETHOD
GetName(nsAString& aNameOut) override {
aNameOut = nsLiteralString(
u"RemoteWorkerService: waiting for RemoteWorkers to shutdown");
return NS_OK;
}
NS_IMETHOD
BlockShutdown(nsIAsyncShutdownClient* aClient) override {
mService->BeginShutdown();
return NS_OK;
}
NS_IMETHOD
GetState(nsIPropertyBag**) override { return NS_OK; }
NS_DECL_ISUPPORTS
RefPtr<RemoteWorkerService> mService;
nsCOMPtr<nsIAsyncShutdownClient> mShutdownClient;
};
NS_IMPL_ISUPPORTS(RemoteWorkerServiceShutdownBlocker, nsIAsyncShutdownBlocker)
RemoteWorkerServiceKeepAlive::RemoteWorkerServiceKeepAlive(
RemoteWorkerServiceShutdownBlocker* aBlocker)
: mBlocker(aBlocker) {
MOZ_ASSERT(NS_IsMainThread());
}
RemoteWorkerServiceKeepAlive::~RemoteWorkerServiceKeepAlive() {
// Dispatch a runnable to the main thread to tell the Shutdown Blocker to
// remove itself and notify the RemoteWorkerService it can finish its
// shutdown. We dispatch this to the main thread even if we are already on
// the main thread.
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] {
blocker->RemoteWorkersAllGoneAllowShutdown();
});
MOZ_ALWAYS_SUCCEEDS(
SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
}
/* static */
void RemoteWorkerService::Initialize() {
MOZ_ASSERT(NS_IsMainThread());
@ -45,15 +134,64 @@ void RemoteWorkerService::Initialize() {
RefPtr<RemoteWorkerService> service = new RemoteWorkerService();
// ## Content Process Initialization Case
//
// We are being told to initialize now that we know what our remote type is.
// But if we want to register with the JS-based AsyncShutdown mechanism, it's
// too early to do this, so we must delay using a runnable.
//
// ### Additional Details
//
// In content processes, we will be invoked by ContentChild::RecvRemoteType
// after specializing the content process, which was approximately the
// appropriate time to register when this was written. A new complication is
// that the async shutdown service is implemented in JS and currently
// ContentParent::InitInternal calls SendRemoteType immediately before calling
// ScriptPreloader::InitContentChild which will call
// SendPScriptCacheConstructor. If we try and load JS before
// RecvPScriptCacheConstructor happens, we will fail to load the AsyncShutdown
// service.
//
// There are notifications generated when RecvPScriptCacheConstructor is
// called; it will call NS_CreateServicesFromCategory with
// "content-process-ready-for-script" as both the category to enumerate to
// call do_GetService on and then to generate an observer notification with
// that same name on the service as long as it directly implements
// nsIObserver. (It does not generate a general observer notification because
// the code reasonably assumes that the code would be using the category
// manager to start itself up.)
//
// TODO: Convert this into an XPCOM service that could leverage the category
// service. For now, we just schedule a runnable to initialize the
// service once we get to the event loop. This should largely be okay because
// we need the main thread event loop to be spinning for this to work. This
// could move latencies attribution around slightly so that metrics see a
// longer time to have a content process spawn a worker in and less time for
// the first spawn in that process. In the event PBackground is janky, this
// could introduce new latencies that aren't just changes in attribution,
// however.
if (!XRE_IsParentProcess()) {
nsresult rv = service->InitializeOnMainThread();
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
sRemoteWorkerService = service;
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction(__func__, [initService = std::move(service)] {
nsresult rv = initService->InitializeOnMainThread();
Unused << NS_WARN_IF(NS_FAILED(rv));
});
MOZ_ALWAYS_SUCCEEDS(
SchedulerGroup::Dispatch(TaskCategory::Other, r.forget()));
return;
}
// ## Parent Process Initialization Case
//
// Otherwise we are in the parent process and were invoked by
// nsLayoutStatics::Initialize. We wait until profile-after-change to kick
// off the Worker Launcher thread and have it connect to PBackground. This is
// an appropriate time for remote worker APIs to come online, especially
// because the PRemoteWorkerService mechanism needs processes to eagerly
// register themselves with PBackground since the design explicitly intends to
// avoid blocking on the main threads. (Disclaimer: Currently, things block
// on the main thread.)
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
@ -76,6 +214,24 @@ nsIThread* RemoteWorkerService::Thread() {
return sRemoteWorkerService->mThread;
}
/* static */
already_AddRefed<RemoteWorkerServiceKeepAlive>
RemoteWorkerService::MaybeGetKeepAlive() {
StaticMutexAutoLock lock(sRemoteWorkerServiceMutex);
// In normal operation no one should be calling this without a service
// existing, so assert, but we'll also handle this being null as it is a
// plausible shutdown race.
MOZ_ASSERT(sRemoteWorkerService);
if (!sRemoteWorkerService) {
return nullptr;
}
// Note that this value can be null, but this all handles that.
auto lockedKeepAlive = sRemoteWorkerService->mKeepAlive.Lock();
RefPtr<RemoteWorkerServiceKeepAlive> extraRef = *lockedKeepAlive;
return extraRef.forget();
}
nsresult RemoteWorkerService::InitializeOnMainThread() {
// I would like to call this thread "DOM Remote Worker Launcher", but the max
// length is 16 chars.
@ -84,14 +240,37 @@ nsresult RemoteWorkerService::InitializeOnMainThread() {
return rv;
}
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (NS_WARN_IF(!obs)) {
nsCOMPtr<nsIAsyncShutdownService> shutdownSvc =
services::GetAsyncShutdownService();
if (NS_WARN_IF(!shutdownSvc)) {
return NS_ERROR_FAILURE;
}
rv = obs->AddObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
nsCOMPtr<nsIAsyncShutdownClient> phase;
// Register a blocker that will ensure that the Worker Launcher thread does
// not go away until all of its RemoteWorkerChild instances have let their
// workers shutdown. This is necessary for sanity and should add a minimal
// shutdown delay.
//
// (Previously we shutdown at "xpcom-shutdown", but the service currently only
// provides "xpcom-will-shutdown". This is arguably beneficial since many
// things on "xpcom-shutdown" may spin event loops whereas we and the other
// blockers can operate in parallel and avoid being delayed by those serial
// blockages.)
MOZ_ALWAYS_SUCCEEDS(shutdownSvc->GetXpcomWillShutdown(getter_AddRefs(phase)));
if (NS_WARN_IF(!phase)) {
return NS_ERROR_FAILURE;
}
RefPtr<RemoteWorkerServiceShutdownBlocker> blocker =
new RemoteWorkerServiceShutdownBlocker(this, phase);
{
RefPtr<RemoteWorkerServiceKeepAlive> keepAlive =
new RemoteWorkerServiceKeepAlive(blocker);
auto lockedKeepAlive = mKeepAlive.Lock();
*lockedKeepAlive = std::move(keepAlive);
}
RefPtr<RemoteWorkerService> self = this;
@ -106,7 +285,10 @@ nsresult RemoteWorkerService::InitializeOnMainThread() {
return NS_OK;
}
RemoteWorkerService::RemoteWorkerService() { MOZ_ASSERT(NS_IsMainThread()); }
RemoteWorkerService::RemoteWorkerService()
: mKeepAlive(nullptr, "RemoteWorkerService::mKeepAlive") {
MOZ_ASSERT(NS_IsMainThread());
}
RemoteWorkerService::~RemoteWorkerService() = default;
@ -146,34 +328,6 @@ void RemoteWorkerService::CloseActorOnTargetThread() {
NS_IMETHODIMP
RemoteWorkerService::Observe(nsISupports* aSubject, const char* aTopic,
const char16_t* aData) {
if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
MOZ_ASSERT(mThread);
nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
if (obs) {
obs->RemoveObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID);
}
RefPtr<RemoteWorkerService> self = this;
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction("RemoteWorkerService::CloseActorOnTargetThread",
[self]() { self->CloseActorOnTargetThread(); });
mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
// We've posted a shutdown message; now shutdown the thread. This will spin
// a nested event loop waiting for the thread to process all pending events
// (including the just dispatched CloseActorOnTargetThread which will close
// the actor), ensuring to block main thread shutdown long enough to avoid
// races.
mThread->Shutdown();
mThread = nullptr;
StaticMutexAutoLock lock(sRemoteWorkerServiceMutex);
sRemoteWorkerService = nullptr;
return NS_OK;
}
MOZ_ASSERT(!strcmp(aTopic, "profile-after-change"));
MOZ_ASSERT(XRE_IsParentProcess());
@ -185,6 +339,42 @@ RemoteWorkerService::Observe(nsISupports* aSubject, const char* aTopic,
return InitializeOnMainThread();
}
void RemoteWorkerService::BeginShutdown() {
// Drop our keepalive reference which may allow near-immediate removal of the
// blocker.
auto lockedKeepAlive = mKeepAlive.Lock();
*lockedKeepAlive = nullptr;
}
void RemoteWorkerService::FinishShutdown() {
// Clear the singleton before spinning the event loop when shutting down the
// thread so that MaybeGetKeepAlive() can assert if there are any late calls
// and to better reflect the actual state.
//
// Our caller, the RemoteWorkerServiceShutdownBlocker, will continue to hold a
// strong reference to us until we return from this call, so there are no
// lifecycle implications to dropping this reference.
{
StaticMutexAutoLock lock(sRemoteWorkerServiceMutex);
sRemoteWorkerService = nullptr;
}
RefPtr<RemoteWorkerService> self = this;
nsCOMPtr<nsIRunnable> r =
NS_NewRunnableFunction("RemoteWorkerService::CloseActorOnTargetThread",
[self]() { self->CloseActorOnTargetThread(); });
mThread->Dispatch(r.forget(), NS_DISPATCH_NORMAL);
// We've posted a shutdown message; now shutdown the thread. This will spin
// a nested event loop waiting for the thread to process all pending events
// (including the just dispatched CloseActorOnTargetThread which will close
// the actor), ensuring to block main thread shutdown long enough to avoid
// races.
mThread->Shutdown();
mThread = nullptr;
}
NS_IMPL_ISUPPORTS(RemoteWorkerService, nsIObserver)
} // namespace dom

Просмотреть файл

@ -7,14 +7,42 @@
#ifndef mozilla_dom_RemoteWorkerService_h
#define mozilla_dom_RemoteWorkerService_h
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/DataMutex.h"
#include "nsCOMPtr.h"
#include "nsIObserver.h"
#include "nsISupportsImpl.h"
class nsIThread;
namespace mozilla::dom {
class RemoteWorkerService;
class RemoteWorkerServiceChild;
class RemoteWorkerServiceShutdownBlocker;
/**
* Refcounted lifecycle helper; when its refcount goes to zero its destructor
* will call RemoteWorkerService::Shutdown() which will remove the shutdown
* blocker and shutdown the "Worker Launcher" thread.
*
* The RemoteWorkerService itself will hold a reference to this singleton which
* it will use to hand out additional refcounts to RemoteWorkerChild instances.
* When the shutdown blocker is notified that it's time to shutdown, the
* RemoteWorkerService's reference will be dropped.
*/
class RemoteWorkerServiceKeepAlive {
public:
explicit RemoteWorkerServiceKeepAlive(
RemoteWorkerServiceShutdownBlocker* aBlocker);
private:
~RemoteWorkerServiceKeepAlive();
NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteWorkerServiceKeepAlive);
RefPtr<RemoteWorkerServiceShutdownBlocker> mBlocker;
};
/**
* Every process has a RemoteWorkerService which does the actual spawning of
@ -31,6 +59,9 @@ class RemoteWorkerServiceChild;
* manipulation of the worker must happen from the owning thread.)
*/
class RemoteWorkerService final : public nsIObserver {
friend class RemoteWorkerServiceShutdownBlocker;
friend class RemoteWorkerServiceKeepAlive;
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_NSIOBSERVER
@ -40,6 +71,22 @@ class RemoteWorkerService final : public nsIObserver {
static nsIThread* Thread();
// Called by RemoteWorkerChild instances on the "Worker Launcher" thread at
// their creation to assist in tracking when it's safe to shutdown the
// RemoteWorkerService and "Worker Launcher" thread. This method will return
// a null pointer if the RemoteWorkerService has already begun shutdown.
//
// This is somewhat awkwardly a static method because the RemoteWorkerChild
// instances are not managed by RemoteWorkerServiceChild, but instead are
// managed by PBackground(Child). So we either need to find the
// RemoteWorkerService via the hidden singleton or by having the
// RemoteWorkerChild use PBackgroundChild::ManagedPRemoteWorkerServiceChild()
// to locate the instance. We are choosing to use the singleton because we
// already need to acquire a mutex in the call regardless and the upcoming
// refactorings may want to start using new toplevel protocols and this will
// avoid requiring a change when that happens.
static already_AddRefed<RemoteWorkerServiceKeepAlive> MaybeGetKeepAlive();
private:
RemoteWorkerService();
~RemoteWorkerService();
@ -50,8 +97,22 @@ class RemoteWorkerService final : public nsIObserver {
void CloseActorOnTargetThread();
// Called by RemoteWorkerServiceShutdownBlocker when it's time to drop the
// RemoteWorkerServiceKeepAlive reference.
void BeginShutdown();
// Called by RemoteWorkerServiceShutdownBlocker when the blocker has been
// removed and it's safe to shutdown the "Worker Launcher" thread.
void FinishShutdown();
nsCOMPtr<nsIThread> mThread;
RefPtr<RemoteWorkerServiceChild> mActor;
// The keep-alive is set and cleared on the main thread but we will hand out
// additional references to it from the "Worker Launcher" thread, so it's
// appropriate to use a mutex. (Alternately we could have used a ThreadBound
// and set and cleared on the "Worker Launcher" thread, but that would
// involve more moving parts and could have complicated edge cases.)
DataMutex<RefPtr<RemoteWorkerServiceKeepAlive>> mKeepAlive;
};
} // namespace mozilla::dom