Bug 1359490 - add an event loop spinning abstraction function; r=gerald
authorNathan Froyd <froydnj@mozilla.com>
Mon, 15 May 2017 09:34:19 -0400
changeset 358431 c8840a3a2c3b99270e26709c9549496fa206b8eb
parent 358430 b38af5ceaae52b562ccce5f5b8df7276c63f467e
child 358432 1eab3a251445b308f406592913a8903e142a641e
push id31825
push userkwierso@gmail.com
push dateMon, 15 May 2017 23:22:30 +0000
treeherdermozilla-central@3e166b683893 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgerald
bugs1359490
milestone55.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 1359490 - add an event loop spinning abstraction function; r=gerald This function is arguably nicer than calling NS_ProcessNextEvent manually, is slightly more efficient, and will enable better auditing for NS_ProcessNextEvent when we do Quantum DOM scheduling changes.
devtools/server/nsJSInspector.cpp
dom/base/nsGlobalWindow.cpp
dom/cache/Manager.cpp
dom/file/ipc/Blob.cpp
dom/filehandle/ActorsParent.cpp
dom/indexedDB/ActorsParent.cpp
dom/ipc/ContentParent.cpp
dom/media/gmp/GMPServiceParent.cpp
dom/media/gtest/GMPTestMonitor.h
dom/media/gtest/TestGMPCrossOrigin.cpp
dom/media/gtest/TestMediaDataDecoder.cpp
dom/network/UDPSocketChild.cpp
dom/plugins/base/nsPluginHost.cpp
dom/quota/ActorsParent.cpp
dom/workers/ServiceWorkerRegistrar.cpp
dom/xhr/XMLHttpRequestMainThread.cpp
dom/xml/XMLDocument.cpp
extensions/pref/autoconfig/src/nsAutoConfig.cpp
gfx/layers/ipc/CompositorBridgeChild.cpp
gfx/layers/ipc/CompositorThread.cpp
ipc/glue/BackgroundImpl.cpp
layout/printing/ipc/RemotePrintJobChild.cpp
media/webrtc/signaling/test/signaling_unittests.cpp
netwerk/base/ProxyAutoConfig.cpp
netwerk/base/nsAsyncRedirectVerifyHelper.cpp
netwerk/base/nsSyncStreamListener.cpp
netwerk/protocol/http/nsHttpConnectionMgr.cpp
netwerk/test/TestCommon.h
storage/mozStorageService.cpp
toolkit/components/places/Database.cpp
toolkit/components/places/tests/gtest/places_test_harness.h
toolkit/components/places/tests/gtest/places_test_harness_tail.h
toolkit/components/places/tests/gtest/test_IHistory.cpp
toolkit/components/printingui/ipc/nsPrintingProxy.cpp
toolkit/components/url-classifier/tests/gtest/Common.cpp
toolkit/xre/ProfileReset.cpp
toolkit/xre/nsAppRunner.cpp
widget/windows/nsDataObj.cpp
xpcom/threads/SharedThreadPool.cpp
xpcom/threads/ThrottledEventQueue.cpp
xpcom/threads/nsThread.cpp
xpcom/threads/nsThreadPool.cpp
xpcom/threads/nsThreadUtils.h
xpfe/appshell/nsXULWindow.cpp
--- a/devtools/server/nsJSInspector.cpp
+++ b/devtools/server/nsJSInspector.cpp
@@ -69,19 +69,18 @@ nsJSInspector::EnterNestedEventLoop(JS::
 
   mLastRequestor = requestor;
   mRequestors.AppendElement(requestor);
   mozilla::HoldJSObjects(this);
 
   mozilla::dom::AutoNoJSAPI nojsapi;
 
   uint32_t nestLevel = ++mNestedLoopLevel;
-  while (NS_SUCCEEDED(rv) && mNestedLoopLevel >= nestLevel) {
-    if (!NS_ProcessNextEvent())
-      rv = NS_ERROR_UNEXPECTED;
+  if (!SpinEventLoopUntil([&]() { return mNestedLoopLevel < nestLevel; })) {
+    rv = NS_ERROR_UNEXPECTED;
   }
 
   NS_ASSERTION(mNestedLoopLevel <= nestLevel,
                "nested event didn't unwind properly");
 
   if (mNestedLoopLevel == nestLevel) {
     mLastRequestor = mRequestors.ElementAt(--mNestedLoopLevel);
   }
--- a/dom/base/nsGlobalWindow.cpp
+++ b/dom/base/nsGlobalWindow.cpp
@@ -11664,19 +11664,17 @@ nsGlobalWindow::ShowSlowScriptDialog()
     }
 
     if (action == ProcessHangMonitor::StartDebugger) {
       // Spin a nested event loop so that the debugger in the parent can fetch
       // any information it needs. Once the debugger has started, return to the
       // script.
       RefPtr<nsGlobalWindow> outer = GetOuterWindowInternal();
       outer->EnterModalState();
-      while (!monitor->IsDebuggerStartupComplete()) {
-        NS_ProcessNextEvent(nullptr, true);
-      }
+      SpinEventLoopUntil([&]() { return monitor->IsDebuggerStartupComplete(); });
       outer->LeaveModalState();
       return ContinueSlowScript;
     }
 
     return ContinueSlowScriptAndKeepNotifying;
   }
 
   // Reached only on non-e10s - once per slow script dialog.
--- a/dom/cache/Manager.cpp
+++ b/dom/cache/Manager.cpp
@@ -1418,21 +1418,20 @@ Manager::Get(ManagerId* aManagerId)
 // static
 void
 Manager::ShutdownAll()
 {
   mozilla::ipc::AssertIsOnBackgroundThread();
 
   Factory::ShutdownAll();
 
-  while (!Factory::IsShutdownAllComplete()) {
-    if (!NS_ProcessNextEvent()) {
-      NS_WARNING("Something bad happened!");
-      break;
-    }
+  if (!mozilla::SpinEventLoopUntil([]() {
+        return Factory::IsShutdownAllComplete();
+      })) {
+    NS_WARNING("Something bad happened!");
   }
 }
 
 // static
 void
 Manager::Abort(const nsACString& aOrigin)
 {
   mozilla::ipc::AssertIsOnBackgroundThread();
--- a/dom/file/ipc/Blob.cpp
+++ b/dom/file/ipc/Blob.cpp
@@ -4785,22 +4785,17 @@ BlobParent::RecvBlobStreamSync(const uin
 
   if (finished) {
     // The actor is already dead and we have already set our out params.
     return IPC_OK();
   }
 
   // The actor is alive and will be doing asynchronous work to load the stream.
   // Spin a nested loop here while we wait for it.
-  nsIThread* currentThread = NS_GetCurrentThread();
-  MOZ_ASSERT(currentThread);
-
-  while (!finished) {
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread));
-  }
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return finished; }));
 
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 BlobParent::RecvWaitForSliceCreation()
 {
   AssertIsOnOwningThread();
--- a/dom/filehandle/ActorsParent.cpp
+++ b/dom/filehandle/ActorsParent.cpp
@@ -942,22 +942,17 @@ FileHandleThreadPool::Shutdown()
 
   if (!mDirectoryInfos.Count()) {
     Cleanup();
 
     MOZ_ASSERT(mShutdownComplete);
     return;
   }
 
-  nsIThread* currentThread = NS_GetCurrentThread();
-  MOZ_ASSERT(currentThread);
-
-  while (!mShutdownComplete) {
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread));
-  }
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return mShutdownComplete; }));
 }
 
 nsresult
 FileHandleThreadPool::Init()
 {
   AssertIsOnOwningThread();
 
   mThreadPool = new nsThreadPool();
--- a/dom/indexedDB/ActorsParent.cpp
+++ b/dom/indexedDB/ActorsParent.cpp
@@ -12527,22 +12527,17 @@ ConnectionPool::Shutdown()
     MOZ_ASSERT(!mTransactions.Count());
 
     Cleanup();
 
     MOZ_ASSERT(mShutdownComplete);
     return;
   }
 
-  nsIThread* currentThread = NS_GetCurrentThread();
-  MOZ_ASSERT(currentThread);
-
-  while (!mShutdownComplete) {
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread));
-  }
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return mShutdownComplete; }));
 }
 
 void
 ConnectionPool::Cleanup()
 {
   AssertIsOnOwningThread();
   MOZ_ASSERT(mShutdownRequested);
   MOZ_ASSERT(!mShutdownComplete);
@@ -13489,17 +13484,17 @@ ThreadRunnable::Run()
   mFirstRun = false;
 
   {
     // Scope for the profiler label.
     PROFILER_LABEL("IndexedDB",
                    "ConnectionPool::ThreadRunnable::Run",
                    js::ProfileEntry::Category::STORAGE);
 
-    nsIThread* currentThread = NS_GetCurrentThread();
+    DebugOnly<nsIThread*> currentThread = NS_GetCurrentThread();
     MOZ_ASSERT(currentThread);
 
 #ifdef DEBUG
     if (kDEBUGTransactionThreadPriority !=
           nsISupportsPriority::PRIORITY_NORMAL) {
       NS_WARNING("ConnectionPool thread debugging enabled, priority has been "
                  "modified!");
 
@@ -13511,27 +13506,36 @@ ThreadRunnable::Run()
     }
 
     if (kDEBUGTransactionThreadSleepMS) {
       NS_WARNING("TransactionThreadPool thread debugging enabled, sleeping "
                  "after every event!");
     }
 #endif // DEBUG
 
-    while (mContinueRunning) {
-      MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread));
+    DebugOnly<bool> b = SpinEventLoopUntil([&]() -> bool {
+        if (!mContinueRunning) {
+          return true;
+        }
 
 #ifdef DEBUG
       if (kDEBUGTransactionThreadSleepMS) {
         MOZ_ALWAYS_TRUE(
           PR_Sleep(PR_MillisecondsToInterval(kDEBUGTransactionThreadSleepMS)) ==
             PR_SUCCESS);
       }
 #endif // DEBUG
-    }
+
+      return false;
+    });
+    // MSVC can't stringify lambdas, so we have to separate the expression
+    // generating the value from the assert itself.
+#if DEBUG
+    MOZ_ALWAYS_TRUE(b);
+#endif
   }
 
   return NS_OK;
 }
 
 ConnectionPool::
 ThreadInfo::ThreadInfo()
 {
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -2699,19 +2699,17 @@ ContentParent::Observe(nsISupports* aSub
   if (mSubprocess && (!strcmp(aTopic, "profile-before-change") ||
                       !strcmp(aTopic, "xpcom-shutdown"))) {
     // Okay to call ShutDownProcess multiple times.
     ShutDownProcess(SEND_SHUTDOWN_MESSAGE);
 
     // Wait for shutdown to complete, so that we receive any shutdown
     // data (e.g. telemetry) from the child before we quit.
     // This loop terminate prematurely based on mForceKillTimer.
-    while (mIPCOpen && !mCalledKillHard) {
-      NS_ProcessNextEvent(nullptr, true);
-    }
+    SpinEventLoopUntil([&]() { return !mIPCOpen || mCalledKillHard; });
     NS_ASSERTION(!mSubprocess, "Close should have nulled mSubprocess");
   }
 
   if (!mIsAlive || !mSubprocess)
     return NS_OK;
 
   // listening for memory pressure event
   if (!strcmp(aTopic, "memory-pressure") &&
--- a/dom/media/gmp/GMPServiceParent.cpp
+++ b/dom/media/gmp/GMPServiceParent.cpp
@@ -294,20 +294,17 @@ GeckoMediaPluginServiceParent::Observe(n
       LOGD(("%s::%s Starting to unload plugins, waiting for sync shutdown..."
             , __CLASS__, __FUNCTION__));
       gmpThread->Dispatch(
         NewRunnableMethod(this,
                           &GeckoMediaPluginServiceParent::UnloadPlugins),
         NS_DISPATCH_NORMAL);
 
       // Wait for UnloadPlugins() to do sync shutdown...
-      while (mWaitingForPluginsSyncShutdown) {
-        NS_ProcessNextEvent(NS_GetCurrentThread(), true);
-      }
-
+      SpinEventLoopUntil([&]() { return !mWaitingForPluginsSyncShutdown; });
     } else {
       // GMP thread has already shutdown.
       MOZ_ASSERT(mPlugins.IsEmpty());
       mWaitingForPluginsSyncShutdown = false;
     }
 
   } else if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) {
     MOZ_ASSERT(mShuttingDown);
--- a/dom/media/gtest/GMPTestMonitor.h
+++ b/dom/media/gtest/GMPTestMonitor.h
@@ -15,19 +15,17 @@ public:
   GMPTestMonitor()
     : mFinished(false)
   {
   }
 
   void AwaitFinished()
   {
     MOZ_ASSERT(NS_IsMainThread());
-    while (!mFinished) {
-      NS_ProcessNextEvent(nullptr, true);
-    }
+    mozilla::SpinEventLoopUntil([&]() { return mFinished; });
     mFinished = false;
   }
 
 private:
   void MarkFinished()
   {
     MOZ_ASSERT(NS_IsMainThread());
     mFinished = true;
--- a/dom/media/gtest/TestGMPCrossOrigin.cpp
+++ b/dom/media/gtest/TestGMPCrossOrigin.cpp
@@ -1153,19 +1153,17 @@ class GMPStorageTest : public GMPDecrypt
                     update);
   }
 
   void Expect(const nsCString& aMessage, already_AddRefed<nsIRunnable> aContinuation) {
     mExpected.AppendElement(ExpectedMessage(aMessage, Move(aContinuation)));
   }
 
   void AwaitFinished() {
-    while (!mFinished) {
-      NS_ProcessNextEvent(nullptr, true);
-    }
+    mozilla::SpinEventLoopUntil([&]() -> bool { return mFinished; });
     mFinished = false;
   }
 
   void ShutdownThen(already_AddRefed<nsIRunnable> aContinuation) {
     EXPECT_TRUE(!!mDecryptor);
     if (!mDecryptor) {
       return;
     }
--- a/dom/media/gtest/TestMediaDataDecoder.cpp
+++ b/dom/media/gtest/TestMediaDataDecoder.cpp
@@ -30,19 +30,17 @@ public:
     mBenchmark->Init();
     mBenchmark->Run()->Then(
       // Non DocGroup-version of AbstractThread::MainThread() is fine for testing.
       AbstractThread::MainThread(), __func__,
       [&](uint32_t aDecodeFps) { result = aDecodeFps; done = true; },
       [&]() { done = true; });
 
     // Wait until benchmark completes.
-    while (!done) {
-      NS_ProcessNextEvent();
-    }
+    SpinEventLoopUntil([&]() { return done; });
     return result;
   }
 
 private:
   RefPtr<Benchmark> mBenchmark;
 };
 
 TEST(MediaDataDecoder, H264)
--- a/dom/network/UDPSocketChild.cpp
+++ b/dom/network/UDPSocketChild.cpp
@@ -117,21 +117,18 @@ UDPSocketChild::CreatePBackgroundSpinUnt
   bool done = false;
   nsCOMPtr<nsIIPCBackgroundChildCreateCallback> callback =
     new UDPSocketBackgroundChildCallback(&done);
 
   if (NS_WARN_IF(!BackgroundChild::GetOrCreateForCurrentThread(callback))) {
     return NS_ERROR_FAILURE;
   }
 
-  nsIThread* thread = NS_GetCurrentThread();
-  while (!done) {
-    if (NS_WARN_IF(!NS_ProcessNextEvent(thread, true /* aMayWait */))) {
-      return NS_ERROR_FAILURE;
-    }
+  if (!SpinEventLoopUntil([&done]() { return done; })) {
+    return NS_ERROR_FAILURE;
   }
 
   if (NS_WARN_IF(!BackgroundChild::GetForCurrentThread())) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
--- a/dom/plugins/base/nsPluginHost.cpp
+++ b/dom/plugins/base/nsPluginHost.cpp
@@ -1722,19 +1722,17 @@ nsPluginHost::SiteHasData(nsIPluginTag* 
 
   PluginLibrary* library = tag->mPlugin->GetLibrary();
 
   // Get the list of sites from the plugin
   nsCOMPtr<GetSitesClosure> closure(new GetSitesClosure(domain, this));
   rv = library->NPP_GetSitesWithData(nsCOMPtr<nsIGetSitesWithDataCallback>(do_QueryInterface(closure)));
   NS_ENSURE_SUCCESS(rv, rv);
   // Spin the event loop while we wait for the async call to GetSitesWithData
-  while (closure->keepWaiting) {
-    NS_ProcessNextEvent(nullptr, true);
-  }
+  SpinEventLoopUntil([&]() { return !closure->keepWaiting; });
   *result = closure->result;
   return closure->retVal;
 }
 
 nsPluginHost::SpecialType
 nsPluginHost::GetSpecialType(const nsACString & aMIMEType)
 {
   if (aMIMEType.LowerCaseEqualsASCII("application/x-test")) {
--- a/dom/quota/ActorsParent.cpp
+++ b/dom/quota/ActorsParent.cpp
@@ -2853,22 +2853,17 @@ ShutdownObserver::Observe(nsISupports* a
   }
 
   bool done = false;
 
   RefPtr<ShutdownRunnable> shutdownRunnable = new ShutdownRunnable(done);
   MOZ_ALWAYS_SUCCEEDS(
     mBackgroundThread->Dispatch(shutdownRunnable, NS_DISPATCH_NORMAL));
 
-  nsIThread* currentThread = NS_GetCurrentThread();
-  MOZ_ASSERT(currentThread);
-
-  while (!done) {
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(currentThread));
-  }
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return done; }));
 
   return NS_OK;
 }
 
 /*******************************************************************************
  * Quota object
  ******************************************************************************/
 
--- a/dom/workers/ServiceWorkerRegistrar.cpp
+++ b/dom/workers/ServiceWorkerRegistrar.cpp
@@ -984,23 +984,17 @@ ServiceWorkerRegistrar::ProfileStopped()
     return;
   }
 
   bool completed = false;
   mShutdownCompleteFlag = &completed;
 
   child->SendShutdownServiceWorkerRegistrar();
 
-  nsCOMPtr<nsIThread> thread(do_GetCurrentThread());
-  while (true) {
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(thread));
-    if (completed) {
-      break;
-    }
-  }
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return completed; }));
 }
 
 void
 ServiceWorkerRegistrar::Shutdown()
 {
   AssertIsOnBackgroundThread();
   MOZ_ASSERT(!mShuttingDown);
 
--- a/dom/xhr/XMLHttpRequestMainThread.cpp
+++ b/dom/xhr/XMLHttpRequestMainThread.cpp
@@ -2995,22 +2995,18 @@ XMLHttpRequestMainThread::SendInternal(c
     SyncTimeoutType syncTimeoutType = MaybeStartSyncTimeoutTimer();
     if (syncTimeoutType == eErrorOrExpired) {
       Abort();
       rv = NS_ERROR_DOM_NETWORK_ERR;
     }
 
     if (NS_SUCCEEDED(rv)) {
       nsAutoSyncOperation sync(mSuspendedDoc);
-      nsIThread *thread = NS_GetCurrentThread();
-      while (mFlagSyncLooping) {
-        if (!NS_ProcessNextEvent(thread)) {
-          rv = NS_ERROR_UNEXPECTED;
-          break;
-        }
+      if (!SpinEventLoopUntil([&]() { return !mFlagSyncLooping; })) {
+        rv = NS_ERROR_UNEXPECTED;
       }
 
       // Time expired... We should throw.
       if (syncTimeoutType == eTimerStarted && !mSyncTimeoutTimer) {
         rv = NS_ERROR_DOM_NETWORK_ERR;
       }
 
       CancelSyncTimeoutTimer();
--- a/dom/xml/XMLDocument.cpp
+++ b/dom/xml/XMLDocument.cpp
@@ -457,24 +457,19 @@ XMLDocument::Load(const nsAString& aUrl,
   rv = channel->AsyncOpen2(listener);
   if (NS_FAILED(rv)) {
     mChannelIsPending = false;
     aRv.Throw(rv);
     return false;
   }
 
   if (!mAsync) {
-    nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
-
     nsAutoSyncOperation sync(this);
     mLoopingForSyncLoad = true;
-    while (mLoopingForSyncLoad) {
-      if (!NS_ProcessNextEvent(thread))
-        break;
-    }
+    SpinEventLoopUntil([&]() { return !mLoopingForSyncLoad; });
 
     // We set return to true unless there was a parsing error
     Element* rootElement = GetRootElement();
     if (!rootElement) {
       return false;
     }
 
     if (rootElement->LocalName().EqualsLiteral("parsererror")) {
--- a/extensions/pref/autoconfig/src/nsAutoConfig.cpp
+++ b/extensions/pref/autoconfig/src/nsAutoConfig.cpp
@@ -305,32 +305,27 @@ nsresult nsAutoConfig::downloadAutoConfi
     
     // Set a repeating timer if the pref is set.
     // This is to be done only once.
     // Also We are having the event queue processing only for the startup
     // It is not needed with the repeating timer.
     if (firstTime) {
         firstTime = false;
     
-        // Getting the current thread. If we start an AsyncOpen, the thread
-        // needs to wait before the reading of autoconfig is done
-
-        nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
-        NS_ENSURE_STATE(thread);
-    
         /* process events until we're finished. AutoConfig.jsc reading needs
            to be finished before the browser starts loading up
            We are waiting for the mLoaded which will be set through 
            onStopRequest or readOfflineFile methods
            There is a possibility of deadlock so we need to make sure
            that mLoaded will be set to true in any case (success/failure)
         */
-        
-        while (!mLoaded)
-            NS_ENSURE_STATE(NS_ProcessNextEvent(thread));
+
+        if (!mozilla::SpinEventLoopUntil([&]() { return mLoaded; })) {
+            return NS_ERROR_FAILURE;
+        }
         
         int32_t minutes;
         rv = mPrefBranch->GetIntPref("autoadmin.refresh_interval", 
                                      &minutes);
         if (NS_SUCCEEDED(rv) && minutes > 0) {
             // Create a new timer and pass this nsAutoConfig 
             // object as a timer callback. 
             mTimer = do_CreateInstance("@mozilla.org/timer;1",&rv);
--- a/gfx/layers/ipc/CompositorBridgeChild.cpp
+++ b/gfx/layers/ipc/CompositorBridgeChild.cpp
@@ -187,19 +187,17 @@ CompositorBridgeChild::Destroy()
 }
 
 // static
 void
 CompositorBridgeChild::ShutDown()
 {
   if (sCompositorBridge) {
     sCompositorBridge->Destroy();
-    do {
-      NS_ProcessNextEvent(nullptr, true);
-    } while (sCompositorBridge);
+    SpinEventLoopUntil([&]() { return !sCompositorBridge; });
   }
 }
 
 bool
 CompositorBridgeChild::LookupCompositorFrameMetrics(const FrameMetrics::ViewID aId,
                                                     FrameMetrics& aFrame)
 {
   SharedFrameMetricsData* data = mFrameMetricsTable.Get(aId);
--- a/gfx/layers/ipc/CompositorThread.cpp
+++ b/gfx/layers/ipc/CompositorThread.cpp
@@ -127,19 +127,17 @@ CompositorThreadHolder::Shutdown()
   ReleaseImageBridgeParentSingleton();
   gfx::ReleaseVRManagerParentSingleton();
   MediaSystemResourceService::Shutdown();
 
   sCompositorThreadHolder = nullptr;
 
   // No locking is needed around sFinishedCompositorShutDown because it is only
   // ever accessed on the main thread.
-  while (!sFinishedCompositorShutDown) {
-    NS_ProcessNextEvent(nullptr, true);
-  }
+  SpinEventLoopUntil([&]() { return sFinishedCompositorShutDown; });
 
   CompositorBridgeParent::FinishShutdown();
 }
 
 /* static */ bool
 CompositorThreadHolder::IsInCompositorThread()
 {
   return CompositorThread() &&
--- a/ipc/glue/BackgroundImpl.cpp
+++ b/ipc/glue/BackgroundImpl.cpp
@@ -1205,22 +1205,17 @@ ParentImpl::ShutdownBackgroundThread()
       TimerCallbackClosure closure(thread, liveActors);
 
       MOZ_ALWAYS_SUCCEEDS(
         shutdownTimer->InitWithFuncCallback(&ShutdownTimerCallback,
                                             &closure,
                                             kShutdownTimerDelayMS,
                                             nsITimer::TYPE_ONE_SHOT));
 
-      nsIThread* currentThread = NS_GetCurrentThread();
-      MOZ_ASSERT(currentThread);
-
-      while (sLiveActorCount) {
-        NS_ProcessNextEvent(currentThread);
-      }
+      SpinEventLoopUntil([&]() { return !sLiveActorCount; });
 
       MOZ_ASSERT(liveActors->IsEmpty());
 
       MOZ_ALWAYS_SUCCEEDS(shutdownTimer->Cancel());
     }
 
     // Dispatch this runnable to unregister the thread from the profiler.
     nsCOMPtr<nsIRunnable> shutdownRunnable =
@@ -1684,23 +1679,18 @@ ChildImpl::SynchronouslyCreateForCurrent
 
   bool done = false;
   nsCOMPtr<nsIIPCBackgroundChildCreateCallback> callback = new Callback(&done);
 
   if (NS_WARN_IF(!GetOrCreateForCurrentThread(callback))) {
     return nullptr;
   }
 
-  nsIThread* currentThread = NS_GetCurrentThread();
-  MOZ_ASSERT(currentThread);
-
-  while (!done) {
-    if (NS_WARN_IF(!NS_ProcessNextEvent(currentThread, true /* aMayWait */))) {
-      return nullptr;
-    }
+  if (NS_WARN_IF(!SpinEventLoopUntil([&]() { return done; }))) {
+    return nullptr;
   }
 
   return GetForCurrentThread();
 }
 
 // static
 void
 ChildImpl::CloseForCurrentThread()
--- a/layout/printing/ipc/RemotePrintJobChild.cpp
+++ b/layout/printing/ipc/RemotePrintJobChild.cpp
@@ -25,19 +25,17 @@ RemotePrintJobChild::InitializePrint(con
                                      const nsString& aPrintToFile,
                                      const int32_t& aStartPage,
                                      const int32_t& aEndPage)
 {
   // Print initialization can sometimes display a dialog in the parent, so we
   // need to spin a nested event loop until initialization completes.
   Unused << SendInitializePrint(aDocumentTitle, aPrintToFile, aStartPage,
                                 aEndPage);
-  while (!mPrintInitialized) {
-    Unused << NS_ProcessNextEvent();
-  }
+  mozilla::SpinEventLoopUntil([&]() { return mPrintInitialized; });
 
   return mInitializationResult;
 }
 
 mozilla::ipc::IPCResult
 RemotePrintJobChild::RecvPrintInitializationResult(const nsresult& aRv)
 {
   mPrintInitialized = true;
--- a/media/webrtc/signaling/test/signaling_unittests.cpp
+++ b/media/webrtc/signaling/test/signaling_unittests.cpp
@@ -3665,17 +3665,17 @@ int main(int argc, char **argv) {
 
   int result;
   gGtestThread->Dispatch(
     WrapRunnableNMRet(&result, gtest_main, argc, argv), NS_DISPATCH_NORMAL);
 
   // Here we handle the event queue for dispatches to the main thread
   // When the GTest thread is complete it will send one more dispatch
   // with gTestsComplete == true.
-  while (!gTestsComplete && NS_ProcessNextEvent());
+  SpinEventLoopUntil([&]() { return gTestsComplete; });
 
   gGtestThread->Shutdown();
 
   PeerConnectionCtx::Destroy();
   delete test_utils;
 
   return result;
 }
--- a/netwerk/base/ProxyAutoConfig.cpp
+++ b/netwerk/base/ProxyAutoConfig.cpp
@@ -422,18 +422,17 @@ ProxyAutoConfig::ResolveAddress(const ns
       mTimer->InitWithCallback(helper, aTimeout, nsITimer::TYPE_ONE_SHOT);
       helper->mTimer = mTimer;
     }
   }
 
   // Spin the event loop of the pac thread until lookup is complete.
   // nsPACman is responsible for keeping a queue and only allowing
   // one PAC execution at a time even when it is called re-entrantly.
-  while (helper->mRequest)
-    NS_ProcessNextEvent(NS_GetCurrentThread());
+  SpinEventLoopUntil([&, helper]() { return !helper->mRequest; });
 
   if (NS_FAILED(helper->mStatus) ||
       NS_FAILED(helper->mResponse->GetNextAddr(0, aNetAddr)))
     return false;
   return true;
 }
 
 static
--- a/netwerk/base/nsAsyncRedirectVerifyHelper.cpp
+++ b/netwerk/base/nsAsyncRedirectVerifyHelper.cpp
@@ -85,21 +85,18 @@ nsAsyncRedirectVerifyHelper::Init(nsICha
     if (synchronize)
       mWaitingForRedirectCallback = true;
 
     nsresult rv;
     rv = NS_DispatchToMainThread(this);
     NS_ENSURE_SUCCESS(rv, rv);
 
     if (synchronize) {
-      nsIThread *thread = NS_GetCurrentThread();
-      while (mWaitingForRedirectCallback) {
-        if (!NS_ProcessNextEvent(thread)) {
-          return NS_ERROR_UNEXPECTED;
-        }
+      if (!SpinEventLoopUntil([&]() { return !mWaitingForRedirectCallback; })) {
+        return NS_ERROR_UNEXPECTED;
       }
     }
 
     return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAsyncRedirectVerifyHelper::OnRedirectVerifyCallback(nsresult result)
--- a/netwerk/base/nsSyncStreamListener.cpp
+++ b/netwerk/base/nsSyncStreamListener.cpp
@@ -21,18 +21,19 @@ nsSyncStreamListener::Init()
                       false);
 }
 
 nsresult
 nsSyncStreamListener::WaitForData()
 {
     mKeepWaiting = true;
 
-    while (mKeepWaiting)
-        NS_ENSURE_STATE(NS_ProcessNextEvent(NS_GetCurrentThread()));
+    if (!mozilla::SpinEventLoopUntil([&]() { return !mKeepWaiting; })) {
+      return NS_ERROR_FAILURE;
+    }
 
     return NS_OK;
 }
 
 //-----------------------------------------------------------------------------
 // nsSyncStreamListener::nsISupports
 //-----------------------------------------------------------------------------
 
--- a/netwerk/protocol/http/nsHttpConnectionMgr.cpp
+++ b/netwerk/protocol/http/nsHttpConnectionMgr.cpp
@@ -214,19 +214,17 @@ nsHttpConnectionMgr::Shutdown()
 
         if (NS_FAILED(rv)) {
             NS_WARNING("unable to post SHUTDOWN message");
             return rv;
         }
     }
 
     // wait for shutdown event to complete
-    while (!shutdownWrapper->mBool) {
-        NS_ProcessNextEvent(NS_GetCurrentThread());
-    }
+    SpinEventLoopUntil([&, shutdownWrapper]() { return shutdownWrapper->mBool; });
 
     return NS_OK;
 }
 
 class ConnEvent : public Runnable
 {
 public:
     ConnEvent(nsHttpConnectionMgr *mgr,
--- a/netwerk/test/TestCommon.h
+++ b/netwerk/test/TestCommon.h
@@ -17,19 +17,17 @@ public:
   NS_DECL_THREADSAFE_ISUPPORTS
 
   void Wait(int pending)
   {
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(mPending == 0);
 
     mPending = pending;
-    while (mPending) {
-      NS_ProcessNextEvent();
-    }
+    mozilla::SpinEventLoopUntil([&]() { return !mPending; });
     NS_ProcessPendingEvents(nullptr);
   }
 
   void Notify() {
     NS_DispatchToMainThread(this);
   }
 
 private:
--- a/storage/mozStorageService.cpp
+++ b/storage/mozStorageService.cpp
@@ -926,33 +926,28 @@ Service::Observe(nsISupports *, const ch
   } else if (strcmp(aTopic, "xpcom-shutdown-threads") == 0) {
     nsCOMPtr<nsIObserverService> os =
       mozilla::services::GetObserverService();
 
     for (size_t i = 0; i < ArrayLength(sObserverTopics); ++i) {
       (void)os->RemoveObserver(this, sObserverTopics[i]);
     }
 
-    bool anyOpen = false;
-    do {
-      nsTArray<RefPtr<Connection> > connections;
-      getConnections(connections);
-      anyOpen = false;
-      for (uint32_t i = 0; i < connections.Length(); i++) {
-        RefPtr<Connection> &conn = connections[i];
-        if (conn->isClosing()) {
-          anyOpen = true;
-          break;
+    SpinEventLoopUntil([&]() -> bool {
+        // We must wait until all connections are closed.
+        nsTArray<RefPtr<Connection>> connections;
+        getConnections(connections);
+        for (auto& conn : connections) {
+          if (conn->isClosing()) {
+            return false;
+          }
         }
-      }
-      if (anyOpen) {
-        nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
-        NS_ProcessNextEvent(thread);
-      }
-    } while (anyOpen);
+
+        return true;
+      });
 
     if (gShutdownChecks == SCM_CRASH) {
       nsTArray<RefPtr<Connection> > connections;
       getConnections(connections);
       for (uint32_t i = 0, n = connections.Length(); i < n; i++) {
         if (!connections[i]->isClosed()) {
           MOZ_CRASH();
         }
--- a/toolkit/components/places/Database.cpp
+++ b/toolkit/components/places/Database.cpp
@@ -2603,19 +2603,19 @@ Database::Observe(nsISupports *aSubject,
       if (shutdownPhase) {
         shutdownPhase->RemoveBlocker(mClientsShutdown.get());
       }
       (void)mClientsShutdown->BlockShutdown(nullptr);
     }
 
     // Spin the events loop until the clients are done.
     // Note, this is just for tests, specifically test_clearHistory_shutdown.js
-    while (mClientsShutdown->State() != PlacesShutdownBlocker::States::RECEIVED_DONE) {
-      (void)NS_ProcessNextEvent();
-    }
+    SpinEventLoopUntil([&]() {
+	return mClientsShutdown->State() == PlacesShutdownBlocker::States::RECEIVED_DONE;
+      });
 
     {
       nsCOMPtr<nsIAsyncShutdownClient> shutdownPhase = GetProfileBeforeChangePhase();
       if (shutdownPhase) {
         shutdownPhase->RemoveBlocker(mConnectionShutdown.get());
       }
       (void)mConnectionShutdown->BlockShutdown(nullptr);
     }
--- a/toolkit/components/places/tests/gtest/places_test_harness.h
+++ b/toolkit/components/places/tests/gtest/places_test_harness.h
@@ -74,23 +74,34 @@ public:
   {
     nsCOMPtr<nsIObserverService> observerService =
       do_GetService(NS_OBSERVERSERVICE_CONTRACTID);
     do_check_true(observerService);
     (void)observerService->AddObserver(this, aTopic, false);
   }
 
   void Spin() {
-    while (!mTopicReceived) {
-      if ((PR_IntervalNow() - mStartTime) > (WAITFORTOPIC_TIMEOUT_SECONDS * PR_USEC_PER_SEC)) {
-        // Timed out waiting for the topic.
-        do_check_true(false);
-        break;
-      }
-      (void)NS_ProcessNextEvent();
+    bool timedOut = false;
+    mozilla::SpinEventLoopUntil([&]() -> bool {
+        if (mTopicReceived) {
+          return true;
+        }
+
+        if ((PR_IntervalNow() - mStartTime) >
+            (WAITFORTOPIC_TIMEOUT_SECONDS * PR_USEC_PER_SEC)) {
+          timedOut = true;
+          return true;
+        }
+
+        return false;
+      });
+
+    if (timedOut) {
+      // Timed out waiting for the topic.
+      do_check_true(false);
     }
   }
 
   NS_IMETHOD Observe(nsISupports* aSubject,
                      const char* aTopic,
                      const char16_t* aData) override
   {
     mTopicReceived = true;
--- a/toolkit/components/places/tests/gtest/places_test_harness_tail.h
+++ b/toolkit/components/places/tests/gtest/places_test_harness_tail.h
@@ -82,15 +82,13 @@ TEST(IHistory, Test)
   // Tinderboxes are constantly on idle.  Since idle tasks can interact with
   // tests, causing random failures, disable the idle service.
   disable_idle_service();
 
   do_test_pending();
   run_next_test();
 
   // Spin the event loop until we've run out of tests to run.
-  while (gPendingTests) {
-    (void)NS_ProcessNextEvent();
-  }
+  mozilla::SpinEventLoopUntil([&]() { return !gPendingTests; });
 
   // And let any other events finish before we quit.
   (void)NS_ProcessPendingEvents(nullptr);
 }
--- a/toolkit/components/places/tests/gtest/test_IHistory.cpp
+++ b/toolkit/components/places/tests/gtest/test_IHistory.cpp
@@ -65,19 +65,17 @@ public:
     do_check_true(observerService);
     (void)observerService->AddObserver(this,
                                        "uri-visit-saved",
                                        false);
   }
 
   void WaitForNotification()
   {
-    while (mVisits < mExpectedVisits) {
-      (void)NS_ProcessNextEvent();
-    }
+    SpinEventLoopUntil([&]() { return mVisits >= mExpectedVisits; });
   }
 
   NS_IMETHOD Observe(nsISupports* aSubject,
                      const char* aTopic,
                      const char16_t* aData) override
   {
     mVisits++;
 
@@ -406,19 +404,19 @@ test_observer_topic_dispatched()
   // Register our Links to be notified.
   nsCOMPtr<IHistory> history = do_get_IHistory();
   rv = history->RegisterVisitedCallback(visitedURI, visitedLink);
   do_check_success(rv);
   rv = history->RegisterVisitedCallback(notVisitedURI, notVisitedLink);
   do_check_success(rv);
 
   // Spin the event loop as long as we have not been properly notified.
-  while (!visitedNotified || !notVisitedNotified) {
-    (void)NS_ProcessNextEvent();
-  }
+  SpinEventLoopUntil([&]() {
+      return visitedNotified && notVisitedNotified;
+    });
 
   // Unregister our observer that would not have been released.
   rv = history->UnregisterVisitedCallback(notVisitedURI, notVisitedLink);
   do_check_success(rv);
 
   run_next_test();
 }
 
--- a/toolkit/components/printingui/ipc/nsPrintingProxy.cpp
+++ b/toolkit/components/printingui/ipc/nsPrintingProxy.cpp
@@ -112,19 +112,17 @@ nsPrintingProxy::ShowPrintDialog(mozIDOM
   // nested event loop while we wait for the results of the dialog
   // to be returned to us.
 
   RefPtr<PrintSettingsDialogChild> dialog = new PrintSettingsDialogChild();
   SendPPrintSettingsDialogConstructor(dialog);
 
   mozilla::Unused << SendShowPrintDialog(dialog, pBrowser, inSettings);
 
-  while(!dialog->returned()) {
-    NS_ProcessNextEvent(nullptr, true);
-  }
+  SpinEventLoopUntil([&, dialog]() { return dialog->returned(); });
 
   rv = dialog->result();
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = printSettingsSvc->DeserializeToPrintSettings(dialog->data(),
                                                     printSettings);
   return NS_OK;
 }
--- a/toolkit/components/url-classifier/tests/gtest/Common.cpp
+++ b/toolkit/components/url-classifier/tests/gtest/Common.cpp
@@ -51,27 +51,23 @@ nsresult SyncApplyUpdates(Classifier* aC
 
   nsCOMPtr<nsIThread> testingThread;
   NS_NewNamedThread("ApplyUpdates", getter_AddRefs(testingThread));
   if (!testingThread) {
     return NS_ERROR_FAILURE;
   }
 
   testingThread->Dispatch(r, NS_DISPATCH_NORMAL);
-  while (!done) {
-    // NS_NewCheckSummedOutputStream in HashStore::WriteFile
-    // will synchronously init NS_CRYPTO_HASH_CONTRACTID on
-    // the main thread. As a result we have to keep processing
-    // pending event until |done| becomes true. If there's no
-    // more pending event, what we only can do is wait.
-    // Condition variable doesn't work here because instrusively
-    // notifying the from NS_NewCheckSummedOutputStream() or
-    // HashStore::WriteFile() is weird.
-    MOZ_ALWAYS_TRUE(NS_ProcessNextEvent(NS_GetCurrentThread(), true));
-  }
+
+  // NS_NewCheckSummedOutputStream in HashStore::WriteFile
+  // will synchronously init NS_CRYPTO_HASH_CONTRACTID on
+  // the main thread. As a result we have to keep processing
+  // pending event until |done| becomes true. If there's no
+  // more pending event, what we only can do is wait.
+  MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() { return done; }));
 
   return ret;
 }
 
 already_AddRefed<nsIFile>
 GetFile(const nsTArray<nsString>& path)
 {
   nsCOMPtr<nsIFile> file;
--- a/toolkit/xre/ProfileReset.cpp
+++ b/toolkit/xre/ProfileReset.cpp
@@ -157,21 +157,18 @@ ProfileResetCleanup(nsIToolkitProfile* a
   nsCOMPtr<nsIThread> cleanupThread;
   rv = tm->NewThread(0, 0, getter_AddRefs(cleanupThread));
   if (NS_SUCCEEDED(rv)) {
     nsCOMPtr<nsIRunnable> runnable = new ProfileResetCleanupAsyncTask(profileDir, profileLocalDir,
                                                                       containerDest, leafName);
     cleanupThread->Dispatch(runnable, nsIThread::DISPATCH_NORMAL);
     // The result callback will shut down the worker thread.
 
-    nsIThread *thread = NS_GetCurrentThread();
     // Wait for the cleanup thread to complete.
-    while(!gProfileResetCleanupCompleted) {
-      NS_ProcessNextEvent(thread);
-    }
+    SpinEventLoopUntil([&]() { return gProfileResetCleanupCompleted; });
   } else {
     gProfileResetCleanupCompleted = true;
     NS_WARNING("Cleanup thread creation failed");
     return rv;
   }
   // Close the progress window now that the cleanup thread is done.
   auto* piWindow = nsPIDOMWindowOuter::From(progressWindow);
   piWindow->Close();
--- a/toolkit/xre/nsAppRunner.cpp
+++ b/toolkit/xre/nsAppRunner.cpp
@@ -621,20 +621,17 @@ ProcessDDE(nsINativeAppSupport* aNative,
   ar = CheckArgShell("requestpending");
   if (ar == ARG_FOUND) {
     aNative->Enable(); // enable win32 DDE responses
     if (aWait) {
       nsIThread *thread = NS_GetCurrentThread();
       // This is just a guesstimate based on testing different values.
       // If count is 8 or less windows will display an error dialog.
       int32_t count = 20;
-      while(--count >= 0) {
-        NS_ProcessNextEvent(thread);
-        PR_Sleep(PR_MillisecondsToInterval(1));
-      }
+      SpinEventLoopUntil([&]() { return --count < 0; });
     }
   }
 }
 #endif
 
 /**
  * Determines if there is support for showing the profile manager
  *
--- a/widget/windows/nsDataObj.cpp
+++ b/widget/windows/nsDataObj.cpp
@@ -153,20 +153,17 @@ NS_IMETHODIMP nsDataObj::CStream::OnStop
 }
 
 // Pumps thread messages while waiting for the async listener operation to
 // complete. Failing this call will fail the stream incall from Windows
 // and cancel the operation.
 nsresult nsDataObj::CStream::WaitForCompletion()
 {
   // We are guaranteed OnStopRequest will get called, so this should be ok.
-  while (!mChannelRead) {
-    // Pump messages
-    NS_ProcessNextEvent(nullptr, true);
-  }
+  SpinEventLoopUntil([&]() { return mChannelRead; });
 
   if (!mChannelData.Length())
     mChannelResult = NS_ERROR_FAILURE;
 
   return mChannelResult;
 }
 
 //-----------------------------------------------------------------------------
--- a/xpcom/threads/SharedThreadPool.cpp
+++ b/xpcom/threads/SharedThreadPool.cpp
@@ -72,20 +72,20 @@ SharedThreadPool::IsEmpty()
   return !sPools->Count();
 }
 
 /* static */
 void
 SharedThreadPool::SpinUntilEmpty()
 {
   MOZ_ASSERT(NS_IsMainThread());
-  while (!IsEmpty()) {
-    sMonitor->AssertNotCurrentThreadIn();
-    NS_ProcessNextEvent(NS_GetCurrentThread(), true);
-  }
+  SpinEventLoopUntil([]() -> bool {
+      sMonitor->AssertNotCurrentThreadIn();
+      return IsEmpty();
+  });
 }
 
 already_AddRefed<SharedThreadPool>
 SharedThreadPool::Get(const nsCString& aName, uint32_t aThreadLimit)
 {
   MOZ_ASSERT(sMonitor && sPools);
   ReentrantMonitorAutoEnter mon(*sMonitor);
   SharedThreadPool* pool = nullptr;
--- a/xpcom/threads/ThrottledEventQueue.cpp
+++ b/xpcom/threads/ThrottledEventQueue.cpp
@@ -253,19 +253,19 @@ public:
     MOZ_ASSERT(NS_IsMainThread());
     MOZ_ASSERT(!strcmp(aTopic, kShutdownTopic));
 
     MaybeStartShutdown();
 
     // Once shutdown begins we set the Atomic<bool> mShutdownStarted flag.
     // This prevents any new runnables from being dispatched into the
     // TaskQueue.  Therefore this loop should be finite.
-    while (!IsEmpty()) {
-      MOZ_ALWAYS_TRUE(NS_ProcessNextEvent());
-    }
+    MOZ_ALWAYS_TRUE(SpinEventLoopUntil([&]() -> bool {
+        return IsEmpty();
+    }));
 
     return NS_OK;
   }
 
   void
   MaybeStartShutdown()
   {
     // Any thread
--- a/xpcom/threads/nsThread.cpp
+++ b/xpcom/threads/nsThread.cpp
@@ -790,19 +790,20 @@ nsThread::DispatchInternal(already_AddRe
       // PutEvent leaked the wrapper runnable object on failure, so we
       // explicitly release this object once for that. Note that this
       // object will be released again soon because it exits the scope.
       wrapper.get()->Release();
       return rv;
     }
 
     // Allows waiting; ensure no locks are held that would deadlock us!
-    while (wrapper->IsPending()) {
-      NS_ProcessNextEvent(thread, true);
-    }
+    SpinEventLoopUntil([&, wrapper]() -> bool {
+        return !wrapper->IsPending();
+      }, thread);
+
     return NS_OK;
   }
 
   NS_ASSERTION(aFlags == NS_DISPATCH_NORMAL ||
                aFlags == NS_DISPATCH_AT_END, "unexpected dispatch flags");
   return PutEvent(event.take(), aTarget);
 }
 
@@ -989,19 +990,21 @@ nsThread::ShutdownComplete(NotNull<nsThr
   // Delete aContext.
   MOZ_ALWAYS_TRUE(
     aContext->mJoiningThread->mRequestedShutdownContexts.RemoveElement(aContext));
 }
 
 void
 nsThread::WaitForAllAsynchronousShutdowns()
 {
-  while (mRequestedShutdownContexts.Length()) {
-    NS_ProcessNextEvent(this, true);
-  }
+  // This is the motivating example for why SpinEventLoop has the template
+  // parameter we are providing here.
+  SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>([&]() {
+      return mRequestedShutdownContexts.IsEmpty();
+    }, this);
 }
 
 NS_IMETHODIMP
 nsThread::Shutdown()
 {
   LOG(("THRD(%p) sync shutdown\n", this));
 
   // XXX If we make this warn, then we hit that warning at xpcom shutdown while
@@ -1012,19 +1015,19 @@ nsThread::Shutdown()
   }
 
   nsThreadShutdownContext* maybeContext = ShutdownInternal(/* aSync = */ true);
   NS_ENSURE_TRUE(maybeContext, NS_ERROR_UNEXPECTED);
   NotNull<nsThreadShutdownContext*> context = WrapNotNull(maybeContext);
 
   // Process events on the current thread until we receive a shutdown ACK.
   // Allows waiting; ensure no locks are held that would deadlock us!
-  while (context->mAwaitingShutdownAck) {
-    NS_ProcessNextEvent(context->mJoiningThread, true);
-  }
+  SpinEventLoopUntil([&, context]() {
+      return !context->mAwaitingShutdownAck;
+    }, context->mJoiningThread);
 
   ShutdownComplete(context);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsThread::HasPendingEvents(bool* aResult)
--- a/xpcom/threads/nsThreadPool.cpp
+++ b/xpcom/threads/nsThreadPool.cpp
@@ -260,19 +260,19 @@ nsThreadPool::Dispatch(already_AddRefed<
     if (NS_WARN_IF(!thread)) {
       return NS_ERROR_NOT_AVAILABLE;
     }
 
     RefPtr<nsThreadSyncDispatch> wrapper =
       new nsThreadSyncDispatch(thread, Move(aEvent));
     PutEvent(wrapper);
 
-    while (wrapper->IsPending()) {
-      NS_ProcessNextEvent(thread);
-    }
+    SpinEventLoopUntil([&, wrapper]() -> bool {
+        return !wrapper->IsPending();
+      }, thread);
   } else {
     NS_ASSERTION(aFlags == NS_DISPATCH_NORMAL ||
                  aFlags == NS_DISPATCH_AT_END, "unexpected dispatch flags");
     PutEvent(Move(aEvent), aFlags);
   }
   return NS_OK;
 }
 
--- a/xpcom/threads/nsThreadUtils.h
+++ b/xpcom/threads/nsThreadUtils.h
@@ -170,16 +170,100 @@ extern bool NS_HasPendingEvents(nsIThrea
  *
  * @returns
  *   A boolean value that if "true" indicates that an event from the current
  *   thread's event queue was processed.
  */
 extern bool NS_ProcessNextEvent(nsIThread* aThread = nullptr,
                                 bool aMayWait = true);
 
+// A wrapper for nested event loops.
+//
+// This function is intended to make code more obvious (do you remember
+// what NS_ProcessNextEvent(nullptr, true) means?) and slightly more
+// efficient, as people often pass nullptr or NS_GetCurrentThread to
+// NS_ProcessNextEvent, which results in needless querying of the current
+// thread every time through the loop.
+//
+// You should use this function in preference to NS_ProcessNextEvent inside
+// a loop unless one of the following is true:
+//
+// * You need to pass `false` to NS_ProcessNextEvent; or
+// * You need to do unusual things around the call to NS_ProcessNextEvent,
+//   such as unlocking mutexes that you are holding.
+//
+// If you *do* need to call NS_ProcessNextEvent manually, please do call
+// NS_GetCurrentThread() outside of your loop and pass the returned pointer
+// into NS_ProcessNextEvent for a tiny efficiency win.
+namespace mozilla {
+
+// You should normally not need to deal with this template parameter.  If
+// you enjoy esoteric event loop details, read on.
+//
+// If you specify that NS_ProcessNextEvent wait for an event, it is possible
+// for NS_ProcessNextEvent to return false, i.e. to indicate that an event
+// was not processed.  This can only happen when the thread has been shut
+// down by another thread, but is still attempting to process events outside
+// of a nested event loop.
+//
+// This behavior is admittedly strange.  The scenario it deals with is the
+// following:
+//
+// * The current thread has been shut down by some owner thread.
+// * The current thread is spinning an event loop waiting for some condition
+//   to become true.
+// * Said condition is actually being fulfilled by another thread, so there
+//   are timing issues in play.
+//
+// Thus, there is a small window where the current thread's event loop
+// spinning can check the condition, find it false, and call
+// NS_ProcessNextEvent to wait for another event.  But we don't actually
+// want it to wait indefinitely, because there might not be any other events
+// in the event loop, and the current thread can't accept dispatched events
+// because it's being shut down.  Thus, actually blocking would hang the
+// thread, which is bad.  The solution, then, is to detect such a scenario
+// and not actually block inside NS_ProcessNextEvent.
+//
+// But this is a problem, because we want to return the status of
+// NS_ProcessNextEvent to the caller of SpinEventLoopUntil if possible.  In
+// the above scenario, however, we'd stop spinning prematurely and cause
+// all sorts of havoc.  We therefore have this template parameter to
+// control whether errors are ignored or passed out to the caller of
+// SpinEventLoopUntil.  The latter is the default; if you find yourself
+// wanting to use the former, you should think long and hard before doing
+// so, and write a comment like this defending your choice.
+
+enum class ProcessFailureBehavior {
+  IgnoreAndContinue,
+  ReportToCaller,
+};
+
+template<ProcessFailureBehavior Behavior = ProcessFailureBehavior::ReportToCaller,
+         typename Pred>
+bool
+SpinEventLoopUntil(Pred&& aPredicate, nsIThread* aThread = nullptr)
+{
+  nsIThread* thread = aThread ? aThread : NS_GetCurrentThread();
+
+  while (!aPredicate()) {
+    bool didSomething = NS_ProcessNextEvent(thread, true);
+
+    if (Behavior == ProcessFailureBehavior::IgnoreAndContinue) {
+      // Don't care what happened, continue on.
+      continue;
+    } else if (!didSomething) {
+      return false;
+    }
+  }
+
+  return true;
+}
+
+} // namespace mozilla
+
 /**
  * Returns true if we're in the compositor thread.
  *
  * We declare this here because the headers required to invoke
  * CompositorThreadHolder::IsInCompositorThread() also pull in a bunch of system
  * headers that #define various tokens in a way that can break the build.
  */
 extern bool NS_IsInCompositorThread();
--- a/xpfe/appshell/nsXULWindow.cpp
+++ b/xpfe/appshell/nsXULWindow.cpp
@@ -378,21 +378,17 @@ NS_IMETHODIMP nsXULWindow::ShowModal()
   nsCOMPtr<nsIXULWindow> tempRef = this;  
 
   window->SetModal(true);
   mContinueModalLoop = true;
   EnableParent(false);
 
   {
     AutoNoJSAPI nojsapi;
-    nsIThread *thread = NS_GetCurrentThread();
-    while (mContinueModalLoop) {
-      if (!NS_ProcessNextEvent(thread))
-        break;
-    }
+    SpinEventLoopUntil([&]() { return !mContinueModalLoop; });
   }
 
   mContinueModalLoop = false;
   window->SetModal(false);
   /*   Note there's no EnableParent(true) here to match the false one
      above. That's done in ExitModalLoop. It's important that the parent
      be re-enabled before this window is made invisible; to do otherwise
      causes bizarre z-ordering problems. At this point, the window is
@@ -2008,21 +2004,17 @@ NS_IMETHODIMP nsXULWindow::CreateNewCont
 
     chromeWindow->SetOpenerForInitialContentBrowser(aOpener);
   }
 
   xulWin->LockUntilChromeLoad();
 
   {
     AutoNoJSAPI nojsapi;
-    nsIThread *thread = NS_GetCurrentThread();
-    while (xulWin->IsLocked()) {
-      if (!NS_ProcessNextEvent(thread))
-        break;
-    }
+    SpinEventLoopUntil([&]() { return !xulWin->IsLocked(); });
   }
 
   NS_ENSURE_STATE(xulWin->mPrimaryContentShell || xulWin->mPrimaryTabParent);
   MOZ_ASSERT_IF(xulWin->mPrimaryContentShell, aNextTabParentId == 0);
 
   *_retval = newWindow;
   NS_ADDREF(*_retval);