Bug 1406922 - Make CycleCollectedJSContext to handle microtasks and make MutationObserver to use them, r=baku,bevis
authorOlli Pettay <Olli.Pettay@helsinki.fi>
Wed, 11 Oct 2017 15:31:38 +0300
changeset 385548 374ab23d114ef1f74bedadb30a4181b3c2f12168
parent 385547 b895364379c64adc7c83be00d582efe542e64e2e
child 385549 2b5bef1c1595fe9dd0cc8c30944af31504123ad3
push id32662
push userryanvm@gmail.com
push dateWed, 11 Oct 2017 21:53:47 +0000
treeherdermozilla-central@3d918ff5d634 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, bevis
bugs1406922
milestone58.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 1406922 - Make CycleCollectedJSContext to handle microtasks and make MutationObserver to use them, r=baku,bevis
dom/base/nsDOMMutationObserver.cpp
dom/base/nsDOMMutationObserver.h
dom/base/test/test_mutationobservers.html
xpcom/base/CycleCollectedJSContext.cpp
xpcom/base/CycleCollectedJSContext.h
--- a/dom/base/nsDOMMutationObserver.cpp
+++ b/dom/base/nsDOMMutationObserver.cpp
@@ -27,18 +27,16 @@ using mozilla::Move;
 using mozilla::NonOwningAnimationTarget;
 using mozilla::dom::TreeOrderComparator;
 using mozilla::dom::Animation;
 using mozilla::dom::Element;
 
 AutoTArray<RefPtr<nsDOMMutationObserver>, 4>*
   nsDOMMutationObserver::sScheduledMutationObservers = nullptr;
 
-nsDOMMutationObserver* nsDOMMutationObserver::sCurrentObserver = nullptr;
-
 uint32_t nsDOMMutationObserver::sMutationLevel = 0;
 uint64_t nsDOMMutationObserver::sCount = 0;
 
 AutoTArray<AutoTArray<RefPtr<nsDOMMutationObserver>, 4>, 4>*
 nsDOMMutationObserver::sCurrentlyHandlingObservers = nullptr;
 
 nsINodeList*
 nsDOMMutationRecord::AddedNodes()
@@ -594,20 +592,42 @@ nsDOMMutationObserver::ScheduleForRun()
 
   if (mWaitingForRun) {
     return;
   }
   mWaitingForRun = true;
   RescheduleForRun();
 }
 
+class MutationObserverMicroTask final : public MicroTaskRunnable
+{
+public:
+  virtual void Run(AutoSlowOperation& aAso) override
+  {
+    nsDOMMutationObserver::HandleMutations(aAso);
+  }
+
+  virtual bool Suppressed() override
+  {
+    return nsDOMMutationObserver::AllScheduledMutationObserversAreSuppressed();
+  }
+};
+
 void
 nsDOMMutationObserver::RescheduleForRun()
 {
   if (!sScheduledMutationObservers) {
+    CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
+    if (!ccjs) {
+      return;
+    }
+
+    RefPtr<MutationObserverMicroTask> momt =
+      new MutationObserverMicroTask();
+    ccjs->DispatchMicroTaskRunnable(momt.forget());
     sScheduledMutationObservers = new AutoTArray<RefPtr<nsDOMMutationObserver>, 4>;
   }
 
   bool didInsert = false;
   for (uint32_t i = 0; i < sScheduledMutationObservers->Length(); ++i) {
     if (static_cast<nsDOMMutationObserver*>((*sScheduledMutationObservers)[i])
           ->mId > mId) {
       sScheduledMutationObservers->InsertElementAt(i, this);
@@ -859,79 +879,51 @@ nsDOMMutationObserver::HandleMutation()
       current.swap(next);
     }
   }
   ClearPendingRecords();
 
   mCallback->Call(this, mutations, *this);
 }
 
-class AsyncMutationHandler : public mozilla::Runnable
-{
-public:
-  AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}
-  NS_IMETHOD Run() override
-  {
-    nsDOMMutationObserver::HandleMutations();
-    return NS_OK;
-  }
-};
-
 void
-nsDOMMutationObserver::HandleMutationsInternal()
+nsDOMMutationObserver::HandleMutationsInternal(AutoSlowOperation& aAso)
 {
-  if (!nsContentUtils::IsSafeToRunScript()) {
-    nsContentUtils::AddScriptRunner(new AsyncMutationHandler());
-    return;
-  }
-  static RefPtr<nsDOMMutationObserver> sCurrentObserver;
-  if (sCurrentObserver && !sCurrentObserver->Suppressed()) {
-    // In normal cases sScheduledMutationObservers will be handled
-    // after previous mutations are handled. But in case some
-    // callback calls a sync API, which spins the eventloop, we need to still
-    // process other mutations happening during that sync call.
-    // This does *not* catch all cases, but should work for stuff running
-    // in separate tabs.
-    return;
-  }
-
-  mozilla::AutoSlowOperation aso;
-
   nsTArray<RefPtr<nsDOMMutationObserver> >* suppressedObservers = nullptr;
 
   while (sScheduledMutationObservers) {
     AutoTArray<RefPtr<nsDOMMutationObserver>, 4>* observers =
       sScheduledMutationObservers;
     sScheduledMutationObservers = nullptr;
     for (uint32_t i = 0; i < observers->Length(); ++i) {
-      sCurrentObserver = static_cast<nsDOMMutationObserver*>((*observers)[i]);
-      if (!sCurrentObserver->Suppressed()) {
-        sCurrentObserver->HandleMutation();
+      RefPtr<nsDOMMutationObserver> currentObserver =
+        static_cast<nsDOMMutationObserver*>((*observers)[i]);
+      if (!currentObserver->Suppressed()) {
+        currentObserver->HandleMutation();
       } else {
         if (!suppressedObservers) {
           suppressedObservers = new nsTArray<RefPtr<nsDOMMutationObserver> >;
         }
-        if (!suppressedObservers->Contains(sCurrentObserver)) {
-          suppressedObservers->AppendElement(sCurrentObserver);
+        if (!suppressedObservers->Contains(currentObserver)) {
+          suppressedObservers->AppendElement(currentObserver);
         }
       }
     }
     delete observers;
-    aso.CheckForInterrupt();
+    aAso.CheckForInterrupt();
   }
 
   if (suppressedObservers) {
     for (uint32_t i = 0; i < suppressedObservers->Length(); ++i) {
       static_cast<nsDOMMutationObserver*>(suppressedObservers->ElementAt(i))->
         RescheduleForRun();
     }
     delete suppressedObservers;
     suppressedObservers = nullptr;
   }
-  sCurrentObserver = nullptr;
 }
 
 nsDOMMutationRecord*
 nsDOMMutationObserver::CurrentRecord(nsAtom* aType)
 {
   NS_ASSERTION(sMutationLevel > 0, "Unexpected mutation level!");
 
   while (mCurrentMutations.Length() < sMutationLevel) {
--- a/dom/base/nsDOMMutationObserver.h
+++ b/dom/base/nsDOMMutationObserver.h
@@ -570,21 +570,37 @@ public:
   void ClearPendingRecords()
   {
     mFirstPendingMutation = nullptr;
     mLastPendingMutation = nullptr;
     mPendingMutationCount = 0;
   }
 
   // static methods
-  static void HandleMutations()
+  static void HandleMutations(mozilla::AutoSlowOperation& aAso)
+  {
+    if (sScheduledMutationObservers) {
+      HandleMutationsInternal(aAso);
+    }
+  }
+
+  static bool AllScheduledMutationObserversAreSuppressed()
   {
     if (sScheduledMutationObservers) {
-      HandleMutationsInternal();
+      uint32_t len = sScheduledMutationObservers->Length();
+      if (len > 0) {
+        for (uint32_t i = 0; i < len; ++i) {
+          if (!(*sScheduledMutationObservers)[i]->Suppressed()) {
+            return false;
+          }
+        }
+        return true;
+      }
     }
+    return false;
   }
 
   static void EnterMutationHandling();
   static void LeaveMutationHandling();
 
   static void Shutdown();
 protected:
   virtual ~nsDOMMutationObserver();
@@ -608,17 +624,17 @@ protected:
   nsDOMMutationRecord* CurrentRecord(nsAtom* aType);
   bool HasCurrentRecord(const nsAString& aType);
 
   bool Suppressed()
   {
     return mOwner && nsGlobalWindow::Cast(mOwner)->IsInSyncOperation();
   }
 
-  static void HandleMutationsInternal();
+  static void HandleMutationsInternal(mozilla::AutoSlowOperation& aAso);
 
   static void AddCurrentlyHandlingObserver(nsDOMMutationObserver* aObserver,
                                            uint32_t aMutationLevel);
 
   nsCOMPtr<nsPIDOMWindowInner>                       mOwner;
 
   nsCOMArray<nsMutationReceiver>                     mReceivers;
   nsClassHashtable<nsISupportsHashKey,
@@ -636,17 +652,16 @@ protected:
   bool                                               mWaitingForRun;
   bool                                               mIsChrome;
   bool                                               mMergeAttributeRecords;
 
   uint64_t                                           mId;
 
   static uint64_t                                    sCount;
   static AutoTArray<RefPtr<nsDOMMutationObserver>, 4>* sScheduledMutationObservers;
-  static nsDOMMutationObserver*                      sCurrentObserver;
 
   static uint32_t                                    sMutationLevel;
   static AutoTArray<AutoTArray<RefPtr<nsDOMMutationObserver>, 4>, 4>*
                                                      sCurrentlyHandlingObservers;
 };
 
 NS_DEFINE_STATIC_IID_ACCESSOR(nsDOMMutationObserver, NS_DOM_MUTATION_OBSERVER_IID)
 
--- a/dom/base/test/test_mutationobservers.html
+++ b/dom/base/test/test_mutationobservers.html
@@ -357,29 +357,60 @@ function testChildList5() {
       is(records[5].removedNodes[0], c4, "");
       is(records[5].addedNodes.length, 3, "");
       is(records[5].addedNodes[0], dfc1, "");
       is(records[5].addedNodes[1], dfc2, "");
       is(records[5].addedNodes[2], dfc3, "");
       is(records[5].previousSibling, c3, "");
       is(records[5].nextSibling, c5, "");
       observer.disconnect();
-      then(testAdoptNode);
+      then(testNestedMutations);
       m = null;
     });
   m.observe(div, { childList: true, subtree: true });
   m.observe(div2, { childList: true, subtree: true });
   div.replaceChild(c2, c1);
   div.replaceChild(c3, c2);
   div.appendChild(c4);
   div.appendChild(c5);
   div.replaceChild(df, c4);
   div.appendChild(emptyDF); // empty document shouldn't cause mutation records
 }
 
+function testNestedMutations() {
+  div.textContent = null;
+  div.appendChild(document.createTextNode("foo"));
+  var m2WasCalled = false;
+  m = new M(function(records, observer) {
+    is(records[0].type, "characterData", "Should have got characterData");
+    observer.disconnect();
+    m = null;
+    m3 = new M(function(records, observer) {
+      ok(m2WasCalled, "m2 should have been called before m3!");
+      is(records[0].type, "characterData", "Should have got characterData");
+      observer.disconnect();
+      then(testAdoptNode);
+      m3 = null;
+    });
+    m3.observe(div, { characterData: true, subtree: true});
+    div.firstChild.data = "foo";
+  });
+  m2 = new M(function(records, observer) {
+    m2WasCalled = true;
+    is(records[0].type, "characterData", "Should have got characterData");
+    observer.disconnect();
+    m2 = null;
+  });
+  m2.observe(div, { characterData: true, subtree: true});
+  div.appendChild(document.createTextNode("foo"));
+  m.observe(div, { characterData: true, subtree: true });
+
+  div.firstChild.data = "bar";
+}
+
 function testAdoptNode() {
   var d1 = document.implementation.createHTMLDocument(null);
   var d2 = document.implementation.createHTMLDocument(null);
   var addedNode;
   m = new M(function(records, observer) {
       is(records.length, 3, "Should have 2 records");
       is(records[0].target.ownerDocument, d1, "ownerDocument should be the initial document")
       is(records[1].target.ownerDocument, d2, "ownerDocument should be the new document");
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -54,16 +54,17 @@ namespace mozilla {
 
 CycleCollectedJSContext::CycleCollectedJSContext()
   : mIsPrimaryContext(true)
   , mRuntime(nullptr)
   , mJSContext(nullptr)
   , mDoingStableStates(false)
   , mDisableMicroTaskCheckpoint(false)
   , mMicroTaskLevel(0)
+  , mMicroTaskRecursionDepth(0)
 {
   MOZ_COUNT_CTOR(CycleCollectedJSContext);
   nsCOMPtr<nsIThread> thread = do_GetCurrentThread();
   mOwningThread = thread.forget().downcast<nsThread>().take();
   MOZ_RELEASE_ASSERT(mOwningThread);
 }
 
 CycleCollectedJSContext::~CycleCollectedJSContext()
@@ -354,18 +355,18 @@ CycleCollectedJSContext::AfterProcessTas
   // See HTML 6.1.4.2 Processing model
 
   // Execute any events that were waiting for a microtask to complete.
   // This is not (yet) in the spec.
   ProcessMetastableStateQueue(aRecursionDepth);
 
   // Step 4.1: Execute microtasks.
   if (!mDisableMicroTaskCheckpoint) {
+    PerformMicroTaskCheckPoint();
     if (NS_IsMainThread()) {
-      PerformMainThreadMicroTaskCheckpoint();
       Promise::PerformMicroTaskCheckpoint();
     } else {
       Promise::PerformWorkerMicroTaskCheckpoint();
     }
   }
 
   // Step 4.2 Execute any events that were waiting for a stable state.
   ProcessStableStateQueue();
@@ -433,17 +434,77 @@ CycleCollectedJSContext::DispatchToMicro
   RefPtr<nsIRunnable> runnable(aRunnable);
 
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(runnable);
 
   mPromiseMicroTaskQueue.push(runnable.forget());
 }
 
+class AsyncMutationHandler final : public mozilla::Runnable
+{
+public:
+  AsyncMutationHandler() : mozilla::Runnable("AsyncMutationHandler") {}
+
+  NS_IMETHOD Run() override
+  {
+    CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();
+    if (ccjs) {
+      ccjs->PerformMicroTaskCheckPoint();
+    }
+    return NS_OK;
+  }
+};
+
 void
-CycleCollectedJSContext::PerformMainThreadMicroTaskCheckpoint()
+CycleCollectedJSContext::PerformMicroTaskCheckPoint()
 {
-  MOZ_ASSERT(NS_IsMainThread());
+  if (mPendingMicroTaskRunnables.empty()) {
+    // Nothing to do, return early.
+    return;
+  }
+
+  uint32_t currentDepth = RecursionDepth();
+  if (mMicroTaskRecursionDepth >= currentDepth) {
+    // We are already executing microtasks for the current recursion depth.
+    return;
+  }
+
+  if (NS_IsMainThread() && !nsContentUtils::IsSafeToRunScript()) {
+    // Special case for main thread where DOM mutations may happen when
+    // it is not safe to run scripts.
+    nsContentUtils::AddScriptRunner(new AsyncMutationHandler());
+    return;
+  }
+
+  mozilla::AutoRestore<uint32_t> restore(mMicroTaskRecursionDepth);
+  MOZ_ASSERT(currentDepth > 0);
+  mMicroTaskRecursionDepth = currentDepth;
+
+  AutoSlowOperation aso;
 
-  nsDOMMutationObserver::HandleMutations();
+  std::queue<RefPtr<MicroTaskRunnable>> suppressed;
+  while (!mPendingMicroTaskRunnables.empty()) {
+    RefPtr<MicroTaskRunnable> runnable =
+      mPendingMicroTaskRunnables.front().forget();
+    mPendingMicroTaskRunnables.pop();
+    if (runnable->Suppressed()) {
+      suppressed.push(runnable);
+    } else {
+      runnable->Run(aso);
+    }
+  }
+
+  // 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);
+}
+
+void
+CycleCollectedJSContext::DispatchMicroTaskRunnable(
+  already_AddRefed<MicroTaskRunnable> aRunnable)
+{
+  mPendingMicroTaskRunnables.push(aRunnable);
 }
 
 } // namespace mozilla
--- a/xpcom/base/CycleCollectedJSContext.h
+++ b/xpcom/base/CycleCollectedJSContext.h
@@ -22,16 +22,17 @@
 
 class nsCycleCollectionNoteRootCallback;
 class nsIException;
 class nsIRunnable;
 class nsThread;
 class nsWrapperCache;
 
 namespace mozilla {
+class AutoSlowOperation;
 
 class CycleCollectedJSRuntime;
 
 // Contains various stats about the cycle collection.
 struct CycleCollectorResults
 {
   CycleCollectorResults()
   {
@@ -61,16 +62,27 @@ struct CycleCollectorResults
   uint32_t mVisitedRefCounted;
   uint32_t mVisitedGCed;
   uint32_t mFreedRefCounted;
   uint32_t mFreedGCed;
   uint32_t mFreedJSZones;
   uint32_t mNumSlices;
 };
 
+class MicroTaskRunnable
+{
+public:
+  MicroTaskRunnable() {}
+  NS_INLINE_DECL_REFCOUNTING(MicroTaskRunnable)
+  virtual void Run(AutoSlowOperation& aAso) = 0;
+  virtual bool Suppressed() { return false; }
+protected:
+  virtual ~MicroTaskRunnable() {}
+};
+
 class CycleCollectedJSContext
   : public LinkedListElement<CycleCollectedJSContext>
 {
   friend class CycleCollectedJSRuntime;
 
 protected:
   CycleCollectedJSContext();
   virtual ~CycleCollectedJSContext();
@@ -202,17 +214,17 @@ public:
   void EnterMicroTask()
   {
     ++mMicroTaskLevel;
   }
 
   void LeaveMicroTask()
   {
     if (--mMicroTaskLevel == 0) {
-      PerformMainThreadMicroTaskCheckpoint();
+      PerformMicroTaskCheckPoint();
     }
   }
 
   bool IsInMicroTask()
   {
     return mMicroTaskLevel != 0;
   }
 
@@ -221,17 +233,19 @@ public:
     return mMicroTaskLevel;
   }
 
   void SetMicroTaskLevel(uint32_t aLevel)
   {
     mMicroTaskLevel = aLevel;
   }
 
-  void PerformMainThreadMicroTaskCheckpoint();
+  void PerformMicroTaskCheckPoint();
+
+  void DispatchMicroTaskRunnable(already_AddRefed<MicroTaskRunnable> aRunnable);
 
   // Storage for watching rejected promises waiting for some client to
   // consume their rejection.
   // Promises in this list have been rejected in the last turn of the
   // event loop without the rejection being handled.
   // Note that this can contain nullptrs in place of promises removed because
   // they're consumed before it'd be reported.
   JS::PersistentRooted<JS::GCVector<JSObject*, 0, js::SystemAllocPolicy>> mUncaughtRejections;
@@ -265,16 +279,19 @@ private:
   nsTArray<nsCOMPtr<nsIRunnable>> mStableStateEvents;
   nsTArray<RunInMetastableStateData> mMetastableStateEvents;
   uint32_t mBaseRecursionDepth;
   bool mDoingStableStates;
 
   bool mDisableMicroTaskCheckpoint;
 
   uint32_t mMicroTaskLevel;
+  std::queue<RefPtr<MicroTaskRunnable>> mPendingMicroTaskRunnables;
+
+  uint32_t mMicroTaskRecursionDepth;
 };
 
 class MOZ_STACK_CLASS nsAutoMicroTask
 {
 public:
   nsAutoMicroTask()
   {
     CycleCollectedJSContext* ccjs = CycleCollectedJSContext::Get();