Bug 1724777, optimize suppressed MicroTask handling, r=mccr8
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Thu, 12 Aug 2021 16:01:08 +0000
changeset 588696 2f14f5616dca4f3f6008b2e6f162d0a8d3b5ccb3
parent 588695 c26228e5a3047a5debb9d401efca6fc7acb4883c
child 588697 d2b290bff1276bc5a7f5a1892720887d1669e157
push id38700
push usernbeleuzu@mozilla.com
push dateThu, 12 Aug 2021 21:41:21 +0000
treeherdermozilla-central@eacd6f08df5e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmccr8
bugs1724777
milestone93.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 1724777, optimize suppressed MicroTask handling, r=mccr8 The test is in theory racy, but trying to limit the cases when it might behave badly by running it on opt desktop builds only. Without the patch the 'period' check takes over 400ms locally and with the patch 1-3ms. The changes are just trying to optimize execution, not change the behavior. Use of SuppressedMicroTasks is perhaps a bit odd, but it helps keeping SavedMicroTaskQueue and similar code simple. Differential Revision: https://phabricator.services.mozilla.com/D122290
dom/base/Document.cpp
dom/base/Document.h
dom/base/test/mochitest.ini
dom/base/test/test_suppressed_microtasks.html
dom/workers/RuntimeService.cpp
dom/workers/WorkerPrivate.cpp
dom/worklet/WorkletThread.cpp
xpcom/base/CycleCollectedJSContext.cpp
xpcom/base/CycleCollectedJSContext.h
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -15703,16 +15703,28 @@ nsAutoSyncOperation::~nsAutoSyncOperatio
   }
   if (mBrowsingContext &&
       mSyncBehavior == SyncOperationBehavior::eSuspendInput &&
       InputTaskManager::CanSuspendInputEvent()) {
     mBrowsingContext->Group()->DecInputEventSuspensionLevel();
   }
 }
 
+void Document::SetIsInSyncOperation(bool aSync) {
+  if (CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get()) {
+    ccjs->UpdateMicroTaskSuppressionGeneration();
+  }
+
+  if (aSync) {
+    ++mInSyncOperationCount;
+  } else {
+    --mInSyncOperationCount;
+  }
+}
+
 gfxUserFontSet* Document::GetUserFontSet() {
   if (!mFontFaceSet) {
     return nullptr;
   }
 
   return mFontFaceSet->GetUserFontSet();
 }
 
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -3218,23 +3218,17 @@ class Document : public nsINode,
   void SetMayHaveDOMMutationObservers() { mMayHaveDOMMutationObservers = true; }
 
   bool MayHaveAnimationObservers() { return mMayHaveAnimationObservers; }
 
   void SetMayHaveAnimationObservers() { mMayHaveAnimationObservers = true; }
 
   bool IsInSyncOperation() { return mInSyncOperationCount != 0; }
 
-  void SetIsInSyncOperation(bool aSync) {
-    if (aSync) {
-      ++mInSyncOperationCount;
-    } else {
-      --mInSyncOperationCount;
-    }
-  }
+  void SetIsInSyncOperation(bool aSync);
 
   bool CreatingStaticClone() const { return mCreatingStaticClone; }
 
   /**
    * Creates a new element in the HTML namespace with a local name given by
    * aTag.
    */
   already_AddRefed<Element> CreateHTMLElement(nsAtom* aTag);
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -768,16 +768,18 @@ skip-if = debug == false
 [test_settimeout_extra_arguments.html]
 [test_settimeout_inner.html]
 [test_setTimeoutWith0.html]
 [test_setting_opener.html]
 [test_shared_compartment1.html]
 [test_shared_compartment2.html]
 [test_structuredclone_backref.html]
 [test_style_cssText.html]
+[test_suppressed_microtasks.html]
+skip-if = debug || asan || verify || toolkit == 'android' # The test needs to run reasonably fast.
 [test_text_wholeText.html]
 [test_textnode_normalize_in_selection.html]
 [test_textnode_split_in_selection.html]
 [test_timeout_clamp.html]
 [test_timer_flood.html]
 [test_title.html]
 support-files = file_title.xhtml
 [test_treewalker_nextsibling.xml]
new file mode 100644
--- /dev/null
+++ b/dom/base/test/test_suppressed_microtasks.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <meta charset="utf-8">
+  <title>Test microtask suppression</title>
+  <script src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+  <script>
+    SimpleTest.waitForExplicitFinish();
+
+    var previousTask = -1;
+    function test() {
+      let win = window.open("about:blank");
+      win.onload = function() {
+        win.onmessage = function() {
+          win.start = win.performance.now();
+          win.onmessage = function() {
+            let period = win.performance.now() - win.start;
+            win.opener.ok(
+              period < 200,
+              "Running a task should be fast. Took " + period + "ms.");
+            win.onmessage = null;
+          }
+          win.postMessage("measurementMessage", "*");
+        }
+        win.postMessage("initialMessage", "*");
+
+        const last = 500000;
+        for (let i = 0; i < last + 1; ++i) {
+          window.queueMicrotask(function() {
+            // Check that once microtasks are unsuppressed, they are handled in
+            // the correct order.
+            if (previousTask !=  i - 1) {
+              // Explicitly optimize out cases which pass.
+              ok(false, "Microtasks should be handled in order.");
+            }
+            previousTask = i;
+            if (i == last) {
+              win.close();
+              SimpleTest.finish();
+            }
+          });
+        }
+
+        // Synchronous XMLHttpRequest suppresses microtasks.
+        var xhr = new XMLHttpRequest();
+        xhr.open("GET", "slow.sjs", false);
+        xhr.send();
+      }
+    }
+  </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
--- a/dom/workers/RuntimeService.cpp
+++ b/dom/workers/RuntimeService.cpp
@@ -926,17 +926,17 @@ class WorkerJSContext final : public moz
 
   virtual void DispatchToMicroTask(
       already_AddRefed<MicroTaskRunnable> aRunnable) override {
     RefPtr<MicroTaskRunnable> runnable(aRunnable);
 
     MOZ_ASSERT(!NS_IsMainThread());
     MOZ_ASSERT(runnable);
 
-    std::queue<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr;
+    std::deque<RefPtr<MicroTaskRunnable>>* microTaskQueue = nullptr;
 
     JSContext* cx = Context();
     NS_ASSERTION(cx, "This should never be null!");
 
     JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
     NS_ASSERTION(global, "This should never be null!");
 
     // On worker threads, if the current global is the worker global, we use the
@@ -948,17 +948,17 @@ class WorkerJSContext final : public moz
     } else {
       MOZ_ASSERT(IsWorkerDebuggerGlobal(global) ||
                  IsWorkerDebuggerSandbox(global));
 
       microTaskQueue = &GetDebuggerMicroTaskQueue();
     }
 
     JS::JobQueueMayNotBeEmpty(cx);
-    microTaskQueue->push(std::move(runnable));
+    microTaskQueue->push_back(std::move(runnable));
   }
 
   bool IsSystemCaller() const override {
     return mWorkerPrivate->UsesSystemPrincipal();
   }
 
   void ReportError(JSErrorReport* aReport,
                    JS::ConstUTF8CharsZ aToStringResult) override {
--- a/dom/workers/WorkerPrivate.cpp
+++ b/dom/workers/WorkerPrivate.cpp
@@ -4321,17 +4321,17 @@ void WorkerPrivate::EnterDebuggerEventLo
     if (!debuggerRunnablesPending) {
       SetGCTimerMode(IdleTimer);
     }
 
     // Wait for something to do
     {
       MutexAutoLock lock(mMutex);
 
-      std::queue<RefPtr<MicroTaskRunnable>>& debuggerMtQueue =
+      std::deque<RefPtr<MicroTaskRunnable>>& debuggerMtQueue =
           ccjscx->GetDebuggerMicroTaskQueue();
       while (mControlQueue.IsEmpty() &&
              !(debuggerRunnablesPending = !mDebuggerQueue.IsEmpty()) &&
              debuggerMtQueue.empty()) {
         WaitForWorkerEvents();
       }
 
       ProcessAllControlRunnablesLocked();
--- a/dom/worklet/WorkletThread.cpp
+++ b/dom/worklet/WorkletThread.cpp
@@ -154,17 +154,17 @@ class WorkletJSContext final : public Cy
     MOZ_ASSERT(cx);
 
 #ifdef DEBUG
     JS::Rooted<JSObject*> global(cx, JS::CurrentGlobalOrNull(cx));
     MOZ_ASSERT(global);
 #endif
 
     JS::JobQueueMayNotBeEmpty(cx);
-    GetMicroTaskQueue().push(std::move(runnable));
+    GetMicroTaskQueue().push_back(std::move(runnable));
   }
 
   bool IsSystemCaller() const override {
     // Currently no support for special system worklet privileges.
     return false;
   }
 
   void ReportError(JSErrorReport* aReport,
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -56,16 +56,17 @@ using namespace mozilla::dom;
 namespace mozilla {
 
 CycleCollectedJSContext::CycleCollectedJSContext()
     : mRuntime(nullptr),
       mJSContext(nullptr),
       mDoingStableStates(false),
       mTargetedMicroTaskRecursionDepth(0),
       mMicroTaskLevel(0),
+      mSuppressionGeneration(0),
       mDebuggerRecursionDepth(0),
       mMicroTaskRecursionDepth(0),
       mFinalizationRegistryCleanup(this) {
   MOZ_COUNT_CTOR(CycleCollectedJSContext);
 
   nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
   mOwningThread = thread.forget().downcast<nsThread>().take();
   MOZ_RELEASE_ASSERT(mOwningThread);
@@ -286,17 +287,17 @@ class CycleCollectedJSContext::SavedMicr
     MOZ_RELEASE_ASSERT(ccjs->mPendingMicroTaskRunnables.empty());
     MOZ_RELEASE_ASSERT(ccjs->mDebuggerRecursionDepth);
     ccjs->mDebuggerRecursionDepth--;
     ccjs->mPendingMicroTaskRunnables.swap(mQueue);
   }
 
  private:
   CycleCollectedJSContext* ccjs;
-  std::queue<RefPtr<MicroTaskRunnable>> mQueue;
+  std::deque<RefPtr<MicroTaskRunnable>> mQueue;
 };
 
 js::UniquePtr<JS::JobQueue::SavedJobQueue>
 CycleCollectedJSContext::saveJobQueue(JSContext* cx) {
   auto saved = js::MakeUnique<SavedMicroTaskQueue>(this);
   if (!saved) {
     // When MakeUnique's allocation fails, the SavedMicroTaskQueue constructor
     // is never called, so mPendingMicroTaskRunnables is still initialized.
@@ -374,23 +375,23 @@ already_AddRefed<Exception> CycleCollect
   return out.forget();
 }
 
 void CycleCollectedJSContext::SetPendingException(Exception* aException) {
   MOZ_ASSERT(mJSContext);
   mPendingException = aException;
 }
 
-std::queue<RefPtr<MicroTaskRunnable>>&
+std::deque<RefPtr<MicroTaskRunnable>>&
 CycleCollectedJSContext::GetMicroTaskQueue() {
   MOZ_ASSERT(mJSContext);
   return mPendingMicroTaskRunnables;
 }
 
-std::queue<RefPtr<MicroTaskRunnable>>&
+std::deque<RefPtr<MicroTaskRunnable>>&
 CycleCollectedJSContext::GetDebuggerMicroTaskQueue() {
   MOZ_ASSERT(mJSContext);
   return mDebuggerMicroTaskQueue;
 }
 
 void CycleCollectedJSContext::ProcessStableStateQueue() {
   MOZ_ASSERT(mJSContext);
   MOZ_RELEASE_ASSERT(!mDoingStableStates);
@@ -557,17 +558,17 @@ void CycleCollectedJSContext::DispatchTo
   RefPtr<MicroTaskRunnable> runnable(aRunnable);
 
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(runnable);
 
   JS::JobQueueMayNotBeEmpty(Context());
 
   LogMicroTaskRunnable::LogDispatch(runnable.get());
-  mPendingMicroTaskRunnables.push(std::move(runnable));
+  mPendingMicroTaskRunnables.push_back(std::move(runnable));
 }
 
 class AsyncMutationHandler final : public mozilla::Runnable {
  public:
   AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}
 
   // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT.  See
   // bug 1535398.
@@ -576,16 +577,35 @@ class AsyncMutationHandler final : publi
     CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
     if (ccjs) {
       ccjs->PerformMicroTaskCheckPoint();
     }
     return NS_OK;
   }
 };
 
+SuppressedMicroTasks::SuppressedMicroTasks(CycleCollectedJSContext* aContext)
+    : mContext(aContext),
+      mSuppressionGeneration(aContext->mSuppressionGeneration) {}
+
+bool SuppressedMicroTasks::Suppressed() {
+  if (mSuppressionGeneration == mContext->mSuppressionGeneration) {
+    return true;
+  }
+
+  for (std::deque<RefPtr<MicroTaskRunnable>>::reverse_iterator it =
+           mSuppressedMicroTaskRunnables.rbegin();
+       it != mSuppressedMicroTaskRunnables.rend(); ++it) {
+    mContext->GetMicroTaskQueue().push_front(*it);
+  }
+  mContext->mSuppressedMicroTasks = nullptr;
+
+  return false;
+}
+
 bool CycleCollectedJSContext::PerformMicroTaskCheckPoint(bool aForce) {
   if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
     AfterProcessMicrotasks();
     // Nothing to do, return early.
     return false;
   }
 
   uint32_t currentDepth = RecursionDepth();
@@ -611,82 +631,89 @@ bool CycleCollectedJSContext::PerformMic
   MOZ_ASSERT(aForce ? currentDepth == 0 : currentDepth > 0);
   mMicroTaskRecursionDepth = currentDepth;
 
   AUTO_PROFILER_TRACING_MARKER("JS", "Perform microtasks", JS);
 
   bool didProcess = false;
   AutoSlowOperation aso;
 
-  std::queue<RefPtr<MicroTaskRunnable>> suppressed;
   for (;;) {
     RefPtr<MicroTaskRunnable> runnable;
     if (!mDebuggerMicroTaskQueue.empty()) {
       runnable = std::move(mDebuggerMicroTaskQueue.front());
-      mDebuggerMicroTaskQueue.pop();
+      mDebuggerMicroTaskQueue.pop_front();
     } else if (!mPendingMicroTaskRunnables.empty()) {
       runnable = std::move(mPendingMicroTaskRunnables.front());
-      mPendingMicroTaskRunnables.pop();
+      mPendingMicroTaskRunnables.pop_front();
     } else {
       break;
     }
 
     if (runnable->Suppressed()) {
       // Microtasks in worker shall never be suppressed.
       // Otherwise, mPendingMicroTaskRunnables will be replaced later with
       // all suppressed tasks in mDebuggerMicroTaskQueue unexpectedly.
       MOZ_ASSERT(NS_IsMainThread());
       JS::JobQueueMayNotBeEmpty(Context());
-      suppressed.push(runnable);
+      if (runnable != mSuppressedMicroTasks) {
+        if (!mSuppressedMicroTasks) {
+          mSuppressedMicroTasks = new SuppressedMicroTasks(this);
+        }
+        mSuppressedMicroTasks->mSuppressedMicroTaskRunnables.push_back(
+            runnable);
+      }
     } else {
       if (mPendingMicroTaskRunnables.empty() &&
-          mDebuggerMicroTaskQueue.empty() && suppressed.empty()) {
+          mDebuggerMicroTaskQueue.empty() && !mSuppressedMicroTasks) {
         JS::JobQueueIsEmpty(Context());
       }
       didProcess = true;
 
       LogMicroTaskRunnable::Run log(runnable.get());
       runnable->Run(aso);
       runnable = nullptr;
     }
   }
 
   // Put back the suppressed microtasks so that they will be run later.
   // Note, it is possible that we end up keeping these suppressed tasks around
   // for some time, but no longer than spinning the event loop nestedly
   // (sync XHR, alert, etc.)
-  mPendingMicroTaskRunnables.swap(suppressed);
+  if (mSuppressedMicroTasks) {
+    mPendingMicroTaskRunnables.push_back(mSuppressedMicroTasks);
+  }
 
   AfterProcessMicrotasks();
 
   return didProcess;
 }
 
 void CycleCollectedJSContext::PerformDebuggerMicroTaskCheckpoint() {
   // Don't do normal microtask handling checks here, since whoever is calling
   // this method is supposed to know what they are doing.
 
   AutoSlowOperation aso;
   for (;;) {
     // For a debugger microtask checkpoint, we always use the debugger microtask
     // queue.
-    std::queue<RefPtr<MicroTaskRunnable>>* microtaskQueue =
+    std::deque<RefPtr<MicroTaskRunnable>>* microtaskQueue =
         &GetDebuggerMicroTaskQueue();
 
     if (microtaskQueue->empty()) {
       break;
     }
 
     RefPtr<MicroTaskRunnable> runnable = std::move(microtaskQueue->front());
     MOZ_ASSERT(runnable);
 
     LogMicroTaskRunnable::Run log(runnable.get());
 
     // This function can re-enter, so we remove the element before calling.
-    microtaskQueue->pop();
+    microtaskQueue->pop_front();
 
     if (mPendingMicroTaskRunnables.empty() && mDebuggerMicroTaskQueue.empty()) {
       JS::JobQueueIsEmpty(Context());
     }
     runnable->Run(aso);
     runnable = nullptr;
   }
 
--- a/xpcom/base/CycleCollectedJSContext.h
+++ b/xpcom/base/CycleCollectedJSContext.h
@@ -2,17 +2,17 @@
 /* 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 mozilla_CycleCollectedJSContext_h
 #define mozilla_CycleCollectedJSContext_h
 
-#include <queue>
+#include <deque>
 
 #include "mozilla/Attributes.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/dom/AtomList.h"
 #include "mozilla/dom/Promise.h"
 #include "js/GCVector.h"
 #include "js/Promise.h"
 
@@ -76,16 +76,30 @@ class MicroTaskRunnable {
   NS_INLINE_DECL_REFCOUNTING(MicroTaskRunnable)
   MOZ_CAN_RUN_SCRIPT virtual void Run(AutoSlowOperation& aAso) = 0;
   virtual bool Suppressed() { return false; }
 
  protected:
   virtual ~MicroTaskRunnable() = default;
 };
 
+// Store the suppressed mictotasks in another microtask so that operations
+// for the microtask queue as a whole keep working.
+class SuppressedMicroTasks : public MicroTaskRunnable {
+ public:
+  explicit SuppressedMicroTasks(CycleCollectedJSContext* aContext);
+
+  MOZ_CAN_RUN_SCRIPT_BOUNDARY void Run(AutoSlowOperation& aAso) final {}
+  virtual bool Suppressed();
+
+  CycleCollectedJSContext* mContext;
+  uint64_t mSuppressionGeneration;
+  std::deque<RefPtr<MicroTaskRunnable>> mSuppressedMicroTaskRunnables;
+};
+
 // Support for JS FinalizationRegistry objects, which allow a JS callback to be
 // registered that is called when objects die.
 //
 // We keep a vector of functions that call back into the JS engine along
 // with their associated incumbent globals, one per FinalizationRegistry object
 // that has pending cleanup work. These are run in their own task.
 class FinalizationRegistryCleanup {
  public:
@@ -112,16 +126,17 @@ class FinalizationRegistryCleanup {
   CycleCollectedJSContext* mContext;
 
   using CallbackVector = JS::GCVector<Callback, 0, InfallibleAllocPolicy>;
   JS::PersistentRooted<CallbackVector> mCallbacks;
 };
 
 class CycleCollectedJSContext : dom::PerThreadAtomCache, private JS::JobQueue {
   friend class CycleCollectedJSRuntime;
+  friend class SuppressedMicroTasks;
 
  protected:
   CycleCollectedJSContext();
   virtual ~CycleCollectedJSContext();
 
   MOZ_IS_CLASS_INIT
   nsresult Initialize(JSRuntime* aParentRuntime, uint32_t aMaxBytes);
 
@@ -161,33 +176,35 @@ class CycleCollectedJSContext : dom::Per
   CycleCollectedJSRuntime* Runtime() const {
     MOZ_ASSERT(mRuntime);
     return mRuntime;
   }
 
   already_AddRefed<dom::Exception> GetPendingException() const;
   void SetPendingException(dom::Exception* aException);
 
-  std::queue<RefPtr<MicroTaskRunnable>>& GetMicroTaskQueue();
-  std::queue<RefPtr<MicroTaskRunnable>>& GetDebuggerMicroTaskQueue();
+  std::deque<RefPtr<MicroTaskRunnable>>& GetMicroTaskQueue();
+  std::deque<RefPtr<MicroTaskRunnable>>& GetDebuggerMicroTaskQueue();
 
   JSContext* Context() const {
     MOZ_ASSERT(mJSContext);
     return mJSContext;
   }
 
   JS::RootingContext* RootingCx() const {
     MOZ_ASSERT(mJSContext);
     return JS::RootingContext::get(mJSContext);
   }
 
   void SetTargetedMicroTaskRecursionDepth(uint32_t aDepth) {
     mTargetedMicroTaskRecursionDepth = aDepth;
   }
 
+  void UpdateMicroTaskSuppressionGeneration() { ++mSuppressionGeneration; }
+
  protected:
   JSContext* MaybeContext() const { return mJSContext; }
 
  public:
   // nsThread entrypoints
   //
   // MOZ_CAN_RUN_SCRIPT_BOUNDARY so we don't need to annotate
   // nsThread::ProcessNextEvent and all its callers MOZ_CAN_RUN_SCRIPT for now.
@@ -311,18 +328,20 @@ class CycleCollectedJSContext : dom::Per
   bool mDoingStableStates;
 
   // If set to none 0, microtasks will be processed only when recursion depth
   // is the set value.
   uint32_t mTargetedMicroTaskRecursionDepth;
 
   uint32_t mMicroTaskLevel;
 
-  std::queue<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables;
-  std::queue<RefPtr<MicroTaskRunnable>> mDebuggerMicroTaskQueue;
+  std::deque<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables;
+  std::deque<RefPtr<MicroTaskRunnable>> mDebuggerMicroTaskQueue;
+  RefPtr<SuppressedMicroTasks> mSuppressedMicroTasks;
+  uint64_t mSuppressionGeneration;
 
   // How many times the debugger has interrupted execution, possibly creating
   // microtask checkpoints in places that they would not normally occur.
   uint32_t mDebuggerRecursionDepth;
 
   uint32_t mMicroTaskRecursionDepth;
 
   // This implements about-to-be-notified rejected promises list in the spec.