Bug 1611046 - add ServiceWorker shutdown progress tracking r=dom-workers-and-storage-reviewers,asuth
authorPerry Jiang <perry@mozilla.com>
Wed, 29 Jan 2020 10:08:35 +0000
changeset 512061 21e6f8a6b086fe0b40ded08dbdc7fec6c49dc1d0
parent 512060 bdad1c3a97a018e9264017d6dbe8e7d02bfd55d0
child 512062 8f299cada00052284c11fdd5bc296b54716dca19
push id37068
push usernerli@mozilla.com
push dateWed, 29 Jan 2020 15:51:04 +0000
treeherdermozilla-central@019ae805259f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdom-workers-and-storage-reviewers, asuth
bugs1611046
milestone74.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1611046 - add ServiceWorker shutdown progress tracking r=dom-workers-and-storage-reviewers,asuth ServiceWorkerShutdownState encapsulates the shutdown progress for a particular ServiceWorker. The shutdown process involves a "shutdown message" passing multiple threads in both parent/content processes; we report a progress update when it's known that a shutdown message has reached a particular thread. The idea is that in the event of a shutdown timeout, ServiceWorkerShutdownBlocker will be able to provide diagnostics for where shutdown processes are stalled. Differential Revision: https://phabricator.services.mozilla.com/D60791
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
dom/serviceworkers/ServiceWorkerManager.cpp
dom/serviceworkers/ServiceWorkerManager.h
dom/serviceworkers/ServiceWorkerOp.cpp
dom/serviceworkers/ServiceWorkerOpArgs.ipdlh
dom/serviceworkers/ServiceWorkerPrivateImpl.cpp
dom/serviceworkers/ServiceWorkerPrivateImpl.h
dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp
dom/serviceworkers/ServiceWorkerShutdownBlocker.h
dom/serviceworkers/ServiceWorkerShutdownState.cpp
dom/serviceworkers/ServiceWorkerShutdownState.h
dom/serviceworkers/moz.build
dom/workers/remoteworkers/RemoteWorkerChild.cpp
dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -102,16 +102,17 @@
 #include "mozilla/dom/PPresentationParent.h"
 #include "mozilla/dom/ParentProcessMessageManager.h"
 #include "mozilla/dom/Permissions.h"
 #include "mozilla/dom/PresentationParent.h"
 #include "mozilla/dom/ProcessMessageManager.h"
 #include "mozilla/dom/PushNotifier.h"
 #include "mozilla/dom/SHEntryParent.h"
 #include "mozilla/dom/SHistoryParent.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
 #include "mozilla/dom/ServiceWorkerRegistrar.h"
 #include "mozilla/dom/ServiceWorkerUtils.h"
 #include "mozilla/dom/StorageIPC.h"
 #include "mozilla/dom/URLClassifierParent.h"
 #include "mozilla/dom/WakeLock.h"
 #include "mozilla/dom/WindowGlobalParent.h"
 #include "mozilla/dom/ipc/SharedMap.h"
 #include "mozilla/dom/ipc/StructuredCloneData.h"
@@ -6374,16 +6375,26 @@ PParentToChildStreamParent* ContentParen
 }
 
 PFileDescriptorSetParent* ContentParent::SendPFileDescriptorSetConstructor(
     const FileDescriptor& aFD) {
   MOZ_ASSERT(NS_IsMainThread());
   return PContentParent::SendPFileDescriptorSetConstructor(aFD);
 }
 
+mozilla::ipc::IPCResult ContentParent::RecvReportServiceWorkerShutdownProgress(
+    uint32_t aShutdownStateId, ServiceWorkerShutdownState::Progress aProgress) {
+  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+  MOZ_RELEASE_ASSERT(swm, "ServiceWorkers should shutdown before SWM.");
+
+  swm->ReportServiceWorkerShutdownProgress(aShutdownStateId, aProgress);
+
+  return IPC_OK();
+}
+
 mozilla::ipc::IPCResult ContentParent::RecvCommitWindowContextTransaction(
     WindowContext* aContext, WindowContext::BaseTransaction&& aTransaction,
     uint64_t aEpoch) {
   // Record the new BrowsingContextFieldEpoch associated with this transaction.
   // This should be done unconditionally, so that we're always in-sync.
   //
   // The order the parent process receives transactions is considered the
   // "canonical" ordering, so we don't need to worry about doing any
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -1231,16 +1231,20 @@ class ContentParent final
       ModulePaths&& aModPaths, bool aRunAtNormalPriority,
       GetModulesTrustResolver&& aResolver);
 
   mozilla::ipc::IPCResult RecvSessionStorageData(
       uint64_t aTopContextId, const nsACString& aOriginAttrs,
       const nsACString& aOriginKey, const nsTArray<KeyValuePair>& aDefaultData,
       const nsTArray<KeyValuePair>& aSessionData);
 
+  mozilla::ipc::IPCResult RecvReportServiceWorkerShutdownProgress(
+      uint32_t aShutdownStateId,
+      ServiceWorkerShutdownState::Progress aProgress);
+
   // Notify the ContentChild to enable the input event prioritization when
   // initializing.
   void MaybeEnableRemoteInputEventQueue();
 
 #if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
   void AppendSandboxParams(std::vector<std::string>& aArgs);
   void AppendDynamicSandboxParams(std::vector<std::string>& aArgs);
 #endif
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -116,16 +116,17 @@ using mozilla::dom::WindowContextTransac
 using mozilla::dom::WindowContextInitializer from "mozilla/dom/WindowContext.h";
 using base::SharedMemoryHandle from "base/shared_memory.h";
 using mozilla::ipc::SharedMemoryBasic::Handle from "mozilla/ipc/SharedMemoryBasic.h";
 using mozilla::fontlist::Pointer from "SharedFontList.h";
 using gfxSparseBitSet from "gfxFontUtils.h";
 using mozilla::dom::MediaControlKeysEvent from "ipc/MediaControlIPC.h";
 using mozilla::dom::ControlledMediaState from "ipc/MediaControlIPC.h";
 using refcounted class nsDocShellLoadState from "nsDocShellLoadState.h";
+using mozilla::dom::ServiceWorkerShutdownState::Progress from "mozilla/dom/ServiceWorkerShutdownState.h";
 
 union ChromeRegistryItem
 {
     ChromePackage;
     OverrideMapping;
     SubstitutionMapping;
 };
 
@@ -1519,16 +1520,25 @@ parent:
      * Due to sandboxing, a child process's UntrustedModulesProcessor cannot
      * obtain enough information about a DLL file to determine its
      * trustworthiness. This API asks the chrome process to perform that
      * evaluation.
      */
     async GetModulesTrust(ModulePaths aModPaths, bool aRunAtNormalPriority)
         returns (ModulesMapResult? modMapResult);
 
+    /**
+     * Used to route shutdown diagnostic info from the content process
+     * ServiceWorkers to the parent process' ServiceWorkerManager's
+     * ServiceWorkerShutdownBlocker. (The only other actor chain available
+     * for this would be very convoluted and create ordering problems).
+     */
+    async ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId,
+                                              Progress aProgress);
+
 both:
     async ScriptError(nsString message, nsString sourceName, nsString sourceLine,
                       uint32_t lineNumber, uint32_t colNumber, uint32_t flags,
                       nsCString category, bool privateWindow, uint64_t innerWindowId,
                       bool fromChromeContext);
 
     async CommitBrowsingContextTransaction(BrowsingContext aContext,
                                            BrowsingContextTransaction aTransaction,
--- a/dom/serviceworkers/ServiceWorkerManager.cpp
+++ b/dom/serviceworkers/ServiceWorkerManager.cpp
@@ -317,29 +317,29 @@ ServiceWorkerManager::~ServiceWorkerMana
   // NotAcceptingPromises state when it's destroyed, and this transition
   // normally happens in the "profile-change-teardown" notification callback
   // (which won't be called in ProfileManager mode).
   if (!mShuttingDown && mShutdownBlocker) {
     mShutdownBlocker->StopAcceptingPromises();
   }
 }
 
-void ServiceWorkerManager::BlockShutdownOn(
-    GenericNonExclusivePromise* aPromise) {
+void ServiceWorkerManager::BlockShutdownOn(GenericNonExclusivePromise* aPromise,
+                                           uint32_t aShutdownStateId) {
   AssertIsOnMainThread();
 
   // This may be called when in non-e10s mode with parent-intercept enabled.
   if (!ServiceWorkersAreCrossProcess()) {
     return;
   }
 
   MOZ_ASSERT(mShutdownBlocker);
   MOZ_ASSERT(aPromise);
 
-  mShutdownBlocker->WaitOnPromise(aPromise);
+  mShutdownBlocker->WaitOnPromise(aPromise, aShutdownStateId);
 }
 
 void ServiceWorkerManager::Init(ServiceWorkerRegistrar* aRegistrar) {
   nsCOMPtr<nsIAsyncShutdownClient> shutdownBarrier = GetAsyncShutdownBarrier();
 
   if (shutdownBarrier) {
     mShutdownBlocker =
         ServiceWorkerShutdownBlocker::CreateAndRegisterOn(shutdownBarrier);
@@ -3140,10 +3140,25 @@ void ServiceWorkerManager::RemoveOrphane
   MOZ_ASSERT(aRegistration->IsUnregistered());
   MOZ_ASSERT(!aRegistration->IsControllingClients());
   MOZ_ASSERT(aRegistration->IsIdle());
   MOZ_ASSERT(mOrphanedRegistrations.has(aRegistration));
 
   mOrphanedRegistrations.remove(aRegistration);
 }
 
+uint32_t ServiceWorkerManager::MaybeInitServiceWorkerShutdownProgress() const {
+  if (!mShutdownBlocker) {
+    return ServiceWorkerShutdownBlocker::kInvalidShutdownStateId;
+  }
+
+  return mShutdownBlocker->CreateShutdownState();
+}
+
+void ServiceWorkerManager::ReportServiceWorkerShutdownProgress(
+    uint32_t aShutdownStateId,
+    ServiceWorkerShutdownState::Progress aProgress) const {
+  MOZ_ASSERT(mShutdownBlocker);
+  mShutdownBlocker->ReportShutdownProgress(aShutdownStateId, aProgress);
+}
+
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/serviceworkers/ServiceWorkerManager.h
+++ b/dom/serviceworkers/ServiceWorkerManager.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_dom_workers_serviceworkermanager_h
 #define mozilla_dom_workers_serviceworkermanager_h
 
 #include "nsIServiceWorkerManager.h"
 #include "nsCOMPtr.h"
 
+#include "ServiceWorkerShutdownState.h"
 #include "ipc/IPCMessageUtils.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/AutoRestore.h"
 #include "mozilla/ConsoleReportCollector.h"
 #include "mozilla/HashTable.h"
 #include "mozilla/LinkedList.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/Preferences.h"
@@ -270,26 +271,35 @@ class ServiceWorkerManager final : publi
 
   void CheckPendingReadyPromises();
 
   void RemovePendingReadyPromise(const ClientInfo& aClientInfo);
 
   void NoteInheritedController(const ClientInfo& aClientInfo,
                                const ServiceWorkerDescriptor& aController);
 
-  void BlockShutdownOn(GenericNonExclusivePromise* aPromise);
+  void BlockShutdownOn(GenericNonExclusivePromise* aPromise,
+                       uint32_t aShutdownStateId);
 
   nsresult GetClientRegistration(
       const ClientInfo& aClientInfo,
       ServiceWorkerRegistrationInfo** aRegistrationInfo);
 
   void UpdateControlledClient(const ClientInfo& aOldClientInfo,
                               const ClientInfo& aNewClientInfo,
                               const ServiceWorkerDescriptor& aServiceWorker);
 
+  // Returns the shutdown state ID (may be an invalid ID if an
+  // nsIAsyncShutdownBlocker is not used).
+  uint32_t MaybeInitServiceWorkerShutdownProgress() const;
+
+  void ReportServiceWorkerShutdownProgress(
+      uint32_t aShutdownStateId,
+      ServiceWorkerShutdownState::Progress aProgress) const;
+
  private:
   struct RegistrationDataPerPrincipal;
 
   static bool FindScopeForPath(const nsACString& aScopeKey,
                                const nsACString& aPath,
                                RegistrationDataPerPrincipal** aData,
                                nsACString& aMatch);
 
--- a/dom/serviceworkers/ServiceWorkerOp.cpp
+++ b/dom/serviceworkers/ServiceWorkerOp.cpp
@@ -19,16 +19,17 @@
 #include "nsISupportsImpl.h"
 #include "nsITimer.h"
 #include "nsProxyRelease.h"
 #include "nsServiceManagerUtils.h"
 #include "nsTArray.h"
 #include "nsThreadUtils.h"
 
 #include "ServiceWorkerCloneData.h"
+#include "ServiceWorkerShutdownState.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/CycleCollectedJSContext.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/ErrorResult.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/ScopeExit.h"
 #include "mozilla/SystemGroup.h"
 #include "mozilla/Telemetry.h"
@@ -328,31 +329,35 @@ bool ServiceWorkerOp::MaybeStart(RemoteW
 
   RefPtr<ServiceWorkerOp> self = this;
 
   if (IsTerminationOp()) {
     aOwner->GetTerminationPromise()->Then(
         GetCurrentThreadSerialEventTarget(), __func__,
         [self](
             const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) {
+          MaybeReportServiceWorkerShutdownProgress(self->mArgs, true);
+
           MOZ_ASSERT(!self->mPromiseHolder.IsEmpty());
 
           if (NS_WARN_IF(aResult.IsReject())) {
             self->mPromiseHolder.Reject(aResult.RejectValue(), __func__);
             return;
           }
 
           self->mPromiseHolder.Resolve(NS_OK, __func__);
         });
   }
 
   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;
         }
 
--- a/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh
+++ b/dom/serviceworkers/ServiceWorkerOpArgs.ipdlh
@@ -16,17 +16,19 @@ namespace dom {
  * ServiceWorkerOpArgs
  */
 struct ServiceWorkerCheckScriptEvaluationOpArgs {};
 
 struct ServiceWorkerUpdateStateOpArgs {
   ServiceWorkerState state;
 };
 
-struct ServiceWorkerTerminateWorkerOpArgs {};
+struct ServiceWorkerTerminateWorkerOpArgs {
+  uint32_t shutdownStateId;
+};
 
 struct ServiceWorkerLifeCycleEventOpArgs {
   nsString eventName;
 };
 
 // Possibly need to differentiate an empty array from the absence of an array.
 union OptionalPushData {
   void_t;
--- a/dom/serviceworkers/ServiceWorkerPrivateImpl.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivateImpl.cpp
@@ -371,22 +371,25 @@ nsresult ServiceWorkerPrivateImpl::Check
                   const GenericPromise::ResolveOrRejectValue&) {
                 callback->SetResult(false);
                 callback->Run();
               });
 
           return;
         }
 
-        RefPtr<GenericNonExclusivePromise> promise = self->ShutdownInternal();
-
         RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
         MOZ_ASSERT(swm);
 
-        swm->BlockShutdownOn(promise);
+        auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress();
+
+        RefPtr<GenericNonExclusivePromise> promise =
+            self->ShutdownInternal(shutdownStateId);
+
+        swm->BlockShutdownOn(promise, shutdownStateId);
 
         promise->Then(
             GetCurrentThreadSerialEventTarget(), __func__,
             [callback = std::move(callback)](
                 const GenericNonExclusivePromise::ResolveOrRejectValue&) {
               callback->SetResult(false);
               callback->Run();
             });
@@ -669,25 +672,28 @@ void ServiceWorkerPrivateImpl::Shutdown(
 
   if (!WorkerIsDead()) {
     RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
 
     MOZ_ASSERT(swm,
                "All Service Workers should start shutting down before the "
                "ServiceWorkerManager does!");
 
-    RefPtr<GenericNonExclusivePromise> promise = ShutdownInternal();
-    swm->BlockShutdownOn(promise);
+    auto shutdownStateId = swm->MaybeInitServiceWorkerShutdownProgress();
+
+    RefPtr<GenericNonExclusivePromise> promise =
+        ShutdownInternal(shutdownStateId);
+    swm->BlockShutdownOn(promise, shutdownStateId);
   }
 
   MOZ_ASSERT(WorkerIsDead());
 }
 
-RefPtr<GenericNonExclusivePromise>
-ServiceWorkerPrivateImpl::ShutdownInternal() {
+RefPtr<GenericNonExclusivePromise> ServiceWorkerPrivateImpl::ShutdownInternal(
+    uint32_t aShutdownStateId) {
   AssertIsOnMainThread();
   MOZ_ASSERT(mControllerChild);
 
   mPendingFunctionalEvents.Clear();
 
   mControllerChild->get()->RevokeObserver(this);
 
   if (StaticPrefs::dom_serviceWorkers_testing_enabled()) {
@@ -696,17 +702,17 @@ ServiceWorkerPrivateImpl::ShutdownIntern
       os->NotifyObservers(nullptr, "service-worker-shutdown", nullptr);
     }
   }
 
   RefPtr<GenericNonExclusivePromise::Private> promise =
       new GenericNonExclusivePromise::Private(__func__);
 
   Unused << ExecServiceWorkerOp(
-      ServiceWorkerTerminateWorkerOpArgs(),
+      ServiceWorkerTerminateWorkerOpArgs(aShutdownStateId),
       [promise](ServiceWorkerOpResult&& aResult) {
         MOZ_ASSERT(aResult.type() == ServiceWorkerOpResult::Tnsresult);
         promise->Resolve(true, __func__);
       },
       [promise]() { promise->Reject(NS_ERROR_DOM_ABORT_ERR, __func__); });
 
   /**
    * After dispatching a termination operation, no new operations should
--- a/dom/serviceworkers/ServiceWorkerPrivateImpl.h
+++ b/dom/serviceworkers/ServiceWorkerPrivateImpl.h
@@ -118,17 +118,18 @@ class ServiceWorkerPrivateImpl final : p
 
   nsresult SendFetchEventInternal(
       RefPtr<ServiceWorkerRegistrationInfo>&& aRegistration,
       ServiceWorkerFetchEventOpArgs&& aArgs,
       nsCOMPtr<nsIInterceptedChannel>&& aChannel);
 
   void Shutdown();
 
-  RefPtr<GenericNonExclusivePromise> ShutdownInternal();
+  RefPtr<GenericNonExclusivePromise> ShutdownInternal(
+      uint32_t aShutdownStateId);
 
   nsresult ExecServiceWorkerOp(
       ServiceWorkerOpArgs&& aArgs,
       std::function<void(ServiceWorkerOpResult&&)>&& aSuccessCallback,
       std::function<void()>&& aFailureCallback = [] {});
 
   class PendingFunctionalEvent {
    public:
--- a/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp
+++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.cpp
@@ -59,16 +59,30 @@ NS_IMETHODIMP ServiceWorkerShutdownBlock
 
   rv = propertyBag->SetPropertyAsUint32(NS_LITERAL_STRING("pendingPromises"),
                                         GetPendingPromises());
 
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return rv;
   }
 
+  nsAutoCString shutdownStates;
+
+  for (auto iter = mShutdownStates.iter(); !iter.done(); iter.next()) {
+    shutdownStates.Append(iter.get().value().GetProgressString());
+    shutdownStates.Append(", ");
+  }
+
+  rv = propertyBag->SetPropertyAsACString(NS_LITERAL_STRING("shutdownStates"),
+                                          shutdownStates);
+
+  if (NS_WARN_IF(NS_FAILED(rv))) {
+    return rv;
+  }
+
   propertyBag.forget(aBagOut);
 
   return NS_OK;
 }
 
 /* static */ already_AddRefed<ServiceWorkerShutdownBlocker>
 ServiceWorkerShutdownBlocker::CreateAndRegisterOn(
     nsIAsyncShutdownClient* aShutdownBarrier) {
@@ -85,41 +99,77 @@ ServiceWorkerShutdownBlocker::CreateAndR
   if (NS_WARN_IF(NS_FAILED(rv))) {
     return nullptr;
   }
 
   return blocker.forget();
 }
 
 void ServiceWorkerShutdownBlocker::WaitOnPromise(
-    GenericNonExclusivePromise* aPromise) {
+    GenericNonExclusivePromise* aPromise, uint32_t aShutdownStateId) {
   AssertIsOnMainThread();
   MOZ_DIAGNOSTIC_ASSERT(IsAcceptingPromises());
   MOZ_ASSERT(aPromise);
+  MOZ_ASSERT(mShutdownStates.has(aShutdownStateId));
 
   ++mState.as<AcceptingPromises>().mPendingPromises;
 
   RefPtr<ServiceWorkerShutdownBlocker> self = this;
 
   aPromise->Then(GetCurrentThreadSerialEventTarget(), __func__,
-                 [self = std::move(self)](
+                 [self = std::move(self), shutdownStateId = aShutdownStateId](
                      const GenericNonExclusivePromise::ResolveOrRejectValue&) {
+                   // Progress reporting might race with aPromise settling.
+                   self->mShutdownStates.remove(shutdownStateId);
+
                    if (!self->PromiseSettled()) {
                      self->MaybeUnblockShutdown();
                    }
                  });
 }
 
 void ServiceWorkerShutdownBlocker::StopAcceptingPromises() {
   AssertIsOnMainThread();
   MOZ_ASSERT(IsAcceptingPromises());
 
   mState = AsVariant(NotAcceptingPromises(mState.as<AcceptingPromises>()));
 }
 
+uint32_t ServiceWorkerShutdownBlocker::CreateShutdownState() {
+  AssertIsOnMainThread();
+
+  static uint32_t nextShutdownStateId = 1;
+
+  MOZ_ALWAYS_TRUE(mShutdownStates.putNew(nextShutdownStateId,
+                                         ServiceWorkerShutdownState()));
+
+  return nextShutdownStateId++;
+}
+
+void ServiceWorkerShutdownBlocker::ReportShutdownProgress(
+    uint32_t aShutdownStateId, Progress aProgress) {
+  AssertIsOnMainThread();
+  MOZ_RELEASE_ASSERT(aShutdownStateId != kInvalidShutdownStateId);
+
+  auto lookup = mShutdownStates.lookup(aShutdownStateId);
+
+  // Progress reporting might race with the promise that WaitOnPromise is called
+  // with settling.
+  if (!lookup) {
+    return;
+  }
+
+  // This will check for a valid progress transition with assertions.
+  lookup->value().SetProgress(aProgress);
+
+  if (aProgress == Progress::ShutdownCompleted) {
+    mShutdownStates.remove(lookup);
+  }
+}
+
 ServiceWorkerShutdownBlocker::ServiceWorkerShutdownBlocker()
     : mState(VariantType<AcceptingPromises>()) {
   AssertIsOnMainThread();
 }
 
 ServiceWorkerShutdownBlocker::~ServiceWorkerShutdownBlocker() {
   MOZ_ASSERT(!IsAcceptingPromises());
   MOZ_ASSERT(!GetPendingPromises());
--- a/dom/serviceworkers/ServiceWorkerShutdownBlocker.h
+++ b/dom/serviceworkers/ServiceWorkerShutdownBlocker.h
@@ -6,52 +6,72 @@
 
 #ifndef mozilla_dom_serviceworkershutdownblocker_h__
 #define mozilla_dom_serviceworkershutdownblocker_h__
 
 #include "nsCOMPtr.h"
 #include "nsIAsyncShutdown.h"
 #include "nsISupportsImpl.h"
 
+#include "ServiceWorkerShutdownState.h"
 #include "mozilla/MozPromise.h"
+#include "mozilla/HashTable.h"
 
 namespace mozilla {
 namespace dom {
 
 /**
  * Main thread only.
  */
 class ServiceWorkerShutdownBlocker final : public nsIAsyncShutdownBlocker {
  public:
+  using Progress = ServiceWorkerShutdownState::Progress;
+  static const uint32_t kInvalidShutdownStateId = 0;
+
   NS_DECL_ISUPPORTS
   NS_DECL_NSIASYNCSHUTDOWNBLOCKER
 
   /**
    * Returns the registered shutdown blocker if registration succeeded and
    * nullptr otherwise.
    */
   static already_AddRefed<ServiceWorkerShutdownBlocker> CreateAndRegisterOn(
       nsIAsyncShutdownClient* aShutdownBarrier);
 
   /**
    * Blocks shutdown until `aPromise` settles.
    *
    * Can be called multiple times, and shutdown will be blocked until all the
    * calls' promises settle, but all of these calls must happen before
    * `StopAcceptingPromises()` is called (assertions will enforce this).
+   *
+   * See `CreateShutdownState` for aShutdownStateId, which is needed to clear
+   * the shutdown state if the shutdown process aborts for some reason.
    */
-  void WaitOnPromise(GenericNonExclusivePromise* aPromise);
+  void WaitOnPromise(GenericNonExclusivePromise* aPromise,
+                     uint32_t aShutdownStateId);
 
   /**
    * Once this is called, shutdown will be blocked until all promises
    * passed to `WaitOnPromise()` settle, and there must be no more calls to
    * `WaitOnPromise()` (assertions will enforce this).
    */
   void StopAcceptingPromises();
 
+  /**
+   * Start tracking the shutdown of an individual ServiceWorker for hang
+   * reporting purposes. Returns a "shutdown state ID" that should be used
+   * in subsequent calls to ReportShutdownProgress. The shutdown of an
+   * individual ServiceWorker is presumed to be completed when its `Progress`
+   * reaches `Progress::ShutdownCompleted`.
+   */
+  uint32_t CreateShutdownState();
+
+  void ReportShutdownProgress(uint32_t aShutdownStateId, Progress aProgress);
+
  private:
   ServiceWorkerShutdownBlocker();
 
   ~ServiceWorkerShutdownBlocker();
 
   /**
    * No-op if any of the following are true:
    * 1) `BlockShutdown()` hasn't been called yet, or
@@ -79,14 +99,16 @@ class ServiceWorkerShutdownBlocker final
     explicit NotAcceptingPromises(AcceptingPromises aPreviousState);
 
     uint32_t mPendingPromises = 0;
   };
 
   Variant<AcceptingPromises, NotAcceptingPromises> mState;
 
   nsCOMPtr<nsIAsyncShutdownClient> mShutdownClient;
+
+  HashMap<uint32_t, ServiceWorkerShutdownState> mShutdownStates;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // mozilla_dom_serviceworkershutdownblocker_h__
new file mode 100644
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownState.cpp
@@ -0,0 +1,166 @@
+/* -*- 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 "ServiceWorkerShutdownState.h"
+
+#include <array>
+#include <type_traits>
+
+#include "MainThreadUtils.h"
+#include "ServiceWorkerUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Unused.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/RemoteWorkerService.h"
+#include "mozilla/dom/ServiceWorkerManager.h"
+#include "mozilla/dom/WorkerCommon.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "nsDebug.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace dom {
+
+using Progress = ServiceWorkerShutdownState::Progress;
+
+namespace {
+
+constexpr inline auto UnderlyingProgressValue(Progress aProgress) {
+  return std::underlying_type_t<Progress>(aProgress);
+}
+
+constexpr std::array<const char*, UnderlyingProgressValue(Progress::EndGuard_)>
+    gProgressStrings = {{
+        // clang-format off
+      "parent process main thread",
+      "parent process IPDL background thread",
+      "content process worker launcher thread",
+      "content process main thread",
+      "shutdown completed"
+        // clang-format on
+    }};
+
+}  // anonymous namespace
+
+ServiceWorkerShutdownState::ServiceWorkerShutdownState()
+    : mProgress(Progress::ParentProcessMainThread) {
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(NS_IsMainThread());
+}
+
+ServiceWorkerShutdownState::~ServiceWorkerShutdownState() {
+  Unused << NS_WARN_IF(mProgress != Progress::ShutdownCompleted);
+}
+
+const char* ServiceWorkerShutdownState::GetProgressString() const {
+  return gProgressStrings[UnderlyingProgressValue(mProgress)];
+}
+
+void ServiceWorkerShutdownState::SetProgress(Progress aProgress) {
+  MOZ_ASSERT(aProgress != Progress::EndGuard_);
+  MOZ_RELEASE_ASSERT(UnderlyingProgressValue(mProgress) + 1 ==
+                     UnderlyingProgressValue(aProgress));
+
+  mProgress = aProgress;
+}
+
+namespace {
+
+void ReportProgressToServiceWorkerManager(uint32_t aShutdownStateId,
+                                          Progress aProgress) {
+  MOZ_ASSERT(XRE_IsParentProcess());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  RefPtr<ServiceWorkerManager> swm = ServiceWorkerManager::GetInstance();
+  MOZ_RELEASE_ASSERT(swm, "ServiceWorkers should shutdown before SWM.");
+
+  swm->ReportServiceWorkerShutdownProgress(aShutdownStateId, aProgress);
+}
+
+void ReportProgressToParentProcess(uint32_t aShutdownStateId,
+                                   Progress aProgress) {
+  MOZ_ASSERT(XRE_IsContentProcess());
+  MOZ_ASSERT(NS_IsMainThread());
+
+  ContentChild* contentChild = ContentChild::GetSingleton();
+  MOZ_ASSERT(contentChild);
+
+  contentChild->SendReportServiceWorkerShutdownProgress(aShutdownStateId,
+                                                        aProgress);
+}
+
+void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId,
+                                         Progress aProgress) {
+  MOZ_ASSERT(UnderlyingProgressValue(Progress::ParentProcessMainThread) <
+             UnderlyingProgressValue(aProgress));
+  MOZ_ASSERT(UnderlyingProgressValue(aProgress) <
+             UnderlyingProgressValue(Progress::EndGuard_));
+
+  nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+      __func__, [shutdownStateId = aShutdownStateId, progress = aProgress] {
+        if (XRE_IsParentProcess()) {
+          ReportProgressToServiceWorkerManager(shutdownStateId, progress);
+        } else {
+          ReportProgressToParentProcess(shutdownStateId, progress);
+        }
+      });
+
+  if (NS_IsMainThread()) {
+    MOZ_ALWAYS_SUCCEEDS(r->Run());
+  } else {
+    MOZ_ALWAYS_SUCCEEDS(SystemGroup::Dispatch(TaskCategory::Other, r.forget()));
+  }
+}
+
+void ReportServiceWorkerShutdownProgress(uint32_t aShutdownStateId) {
+  Progress progress = Progress::EndGuard_;
+
+  if (XRE_IsParentProcess()) {
+    mozilla::ipc::AssertIsOnBackgroundThread();
+
+    progress = Progress::ParentProcessIpdlBackgroundThread;
+  } else {
+    if (NS_IsMainThread()) {
+      progress = Progress::ContentProcessMainThread;
+    } else {
+      MOZ_ASSERT(RemoteWorkerService::Thread()->IsOnCurrentThread());
+      progress = Progress::ContentProcessWorkerLauncherThread;
+    }
+  }
+
+  ReportServiceWorkerShutdownProgress(aShutdownStateId, progress);
+}
+
+}  // anonymous namespace
+
+void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs,
+                                              bool aShutdownCompleted) {
+  if (!ServiceWorkerParentInterceptEnabled() ||
+      (XRE_IsParentProcess() && !XRE_IsE10sParentProcess())) {
+    return;
+  }
+
+  if (aShutdownCompleted) {
+    MOZ_ASSERT(aArgs.type() ==
+               ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs);
+
+    ReportServiceWorkerShutdownProgress(
+        aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId(),
+        Progress::ShutdownCompleted);
+
+    return;
+  }
+
+  if (aArgs.type() ==
+      ServiceWorkerOpArgs::TServiceWorkerTerminateWorkerOpArgs) {
+    ReportServiceWorkerShutdownProgress(
+        aArgs.get_ServiceWorkerTerminateWorkerOpArgs().shutdownStateId());
+  }
+}
+
+}  // namespace dom
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/dom/serviceworkers/ServiceWorkerShutdownState.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
+#define DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
+
+#include <cstdint>
+
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/dom/ServiceWorkerOpArgs.h"
+
+namespace mozilla {
+namespace dom {
+
+class ServiceWorkerShutdownState {
+ public:
+  // Represents the "location" of the shutdown message or completion of
+  // shutdown.
+  enum class Progress {
+    ParentProcessMainThread,
+    ParentProcessIpdlBackgroundThread,
+    ContentProcessWorkerLauncherThread,
+    ContentProcessMainThread,
+    ShutdownCompleted,
+    EndGuard_,
+  };
+
+  ServiceWorkerShutdownState();
+
+  ~ServiceWorkerShutdownState();
+
+  const char* GetProgressString() const;
+
+  void SetProgress(Progress aProgress);
+
+ private:
+  Progress mProgress;
+};
+
+// Asynchronously reports that shutdown has progressed to the calling thread
+// if aArgs is for shutdown. If aShutdownCompleted is true, aArgs must be for
+// shutdown.
+void MaybeReportServiceWorkerShutdownProgress(const ServiceWorkerOpArgs& aArgs,
+                                              bool aShutdownCompleted = false);
+
+}  // namespace dom
+}  // namespace mozilla
+
+namespace IPC {
+
+using Progress = mozilla::dom::ServiceWorkerShutdownState::Progress;
+
+template <>
+struct ParamTraits<Progress>
+    : public ContiguousEnumSerializer<
+          Progress, Progress::ParentProcessMainThread, Progress::EndGuard_> {};
+
+}  // namespace IPC
+
+#endif  // DOM_SERVICEWORKERS_SERVICEWORKERSHUTDOWNSTATE_H_
--- a/dom/serviceworkers/moz.build
+++ b/dom/serviceworkers/moz.build
@@ -26,16 +26,17 @@ EXPORTS.mozilla.dom += [
     'ServiceWorkerManagerChild.h',
     'ServiceWorkerManagerParent.h',
     'ServiceWorkerOp.h',
     'ServiceWorkerOpPromise.h',
     'ServiceWorkerRegistrar.h',
     'ServiceWorkerRegistration.h',
     'ServiceWorkerRegistrationDescriptor.h',
     'ServiceWorkerRegistrationInfo.h',
+    'ServiceWorkerShutdownState.h',
     'ServiceWorkerUtils.h',
 ]
 
 UNIFIED_SOURCES += [
     'FetchEventOpChild.cpp',
     'FetchEventOpParent.cpp',
     'FetchEventOpProxyChild.cpp',
     'FetchEventOpProxyParent.cpp',
@@ -73,16 +74,17 @@ UNIFIED_SOURCES += [
     'ServiceWorkerRegistrationChild.cpp',
     'ServiceWorkerRegistrationDescriptor.cpp',
     'ServiceWorkerRegistrationImpl.cpp',
     'ServiceWorkerRegistrationInfo.cpp',
     'ServiceWorkerRegistrationParent.cpp',
     'ServiceWorkerRegistrationProxy.cpp',
     'ServiceWorkerScriptCache.cpp',
     'ServiceWorkerShutdownBlocker.cpp',
+    'ServiceWorkerShutdownState.cpp',
     'ServiceWorkerUnregisterCallback.cpp',
     'ServiceWorkerUnregisterJob.cpp',
     'ServiceWorkerUpdateJob.cpp',
     'ServiceWorkerUpdaterChild.cpp',
     'ServiceWorkerUpdaterParent.cpp',
     'ServiceWorkerUtils.cpp',
 ]
 
--- a/dom/workers/remoteworkers/RemoteWorkerChild.cpp
+++ b/dom/workers/remoteworkers/RemoteWorkerChild.cpp
@@ -31,16 +31,17 @@
 #include "mozilla/dom/FetchEventOpProxyChild.h"
 #include "mozilla/dom/IndexedDatabaseManager.h"
 #include "mozilla/dom/MessagePort.h"
 #include "mozilla/dom/RemoteWorkerTypes.h"
 #include "mozilla/dom/ServiceWorkerDescriptor.h"
 #include "mozilla/dom/ServiceWorkerInterceptController.h"
 #include "mozilla/dom/ServiceWorkerOp.h"
 #include "mozilla/dom/ServiceWorkerRegistrationDescriptor.h"
+#include "mozilla/dom/ServiceWorkerShutdownState.h"
 #include "mozilla/dom/ServiceWorkerUtils.h"
 #include "mozilla/dom/workerinternals/ScriptLoader.h"
 #include "mozilla/dom/WorkerError.h"
 #include "mozilla/dom/WorkerPrivate.h"
 #include "mozilla/dom/WorkerRef.h"
 #include "mozilla/dom/WorkerRunnable.h"
 #include "mozilla/ipc/BackgroundUtils.h"
 #include "mozilla/ipc/URIUtils.h"
@@ -989,16 +990,18 @@ IPCResult RemoteWorkerChild::RecvExecOp(
 
 IPCResult RemoteWorkerChild::RecvExecServiceWorkerOp(
     ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) {
   MOZ_ASSERT(mIsServiceWorker);
   MOZ_ASSERT(
       aArgs.type() != ServiceWorkerOpArgs::TServiceWorkerFetchEventOpArgs,
       "FetchEvent operations should be sent via PFetchEventOp(Proxy) actors!");
 
+  MaybeReportServiceWorkerShutdownProgress(aArgs);
+
   MaybeStartOp(ServiceWorkerOp::Create(aArgs, std::move(aResolve)));
 
   return IPC_OK();
 }
 
 RefPtr<GenericPromise>
 RemoteWorkerChild::MaybeSendSetServiceWorkerSkipWaitingFlag() {
   RefPtr<GenericPromise::Private> promise =
--- a/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp
+++ b/dom/workers/remoteworkers/RemoteWorkerControllerParent.cpp
@@ -12,16 +12,17 @@
 #include "nsDebug.h"
 #include "nsError.h"
 #include "nsThreadUtils.h"
 
 #include "mozilla/Assertions.h"
 #include "mozilla/Unused.h"
 #include "mozilla/dom/FetchEventOpParent.h"
 #include "mozilla/dom/ServiceWorkerOpPromise.h"
+#include "mozilla/dom/ServiceWorkerShutdownState.h"
 #include "mozilla/ipc/BackgroundParent.h"
 
 namespace mozilla {
 
 using namespace ipc;
 
 namespace dom {
 
@@ -96,16 +97,18 @@ bool RemoteWorkerControllerParent::Deall
 }
 
 IPCResult RemoteWorkerControllerParent::RecvExecServiceWorkerOp(
     ServiceWorkerOpArgs&& aArgs, ExecServiceWorkerOpResolver&& aResolve) {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(mIPCActive);
   MOZ_ASSERT(mRemoteWorkerController);
 
+  MaybeReportServiceWorkerShutdownProgress(aArgs);
+
   mRemoteWorkerController->ExecServiceWorkerOp(std::move(aArgs))
       ->Then(GetCurrentThreadSerialEventTarget(), __func__,
              [resolve = std::move(aResolve)](
                  ServiceWorkerOpPromise::ResolveOrRejectValue&& aResult) {
                if (NS_WARN_IF(aResult.IsReject())) {
                  MOZ_ASSERT(NS_FAILED(aResult.RejectValue()));
                  resolve(aResult.RejectValue());
                  return;