Bug 1730117 - Part 2: Make EnterModalState suppress event handling for the nested in-process documents; r=smaug
authorEdgar Chen <echen@mozilla.com>
Wed, 22 Sep 2021 14:50:56 +0000
changeset 592953 698c991acc52fd4d7b2c8a4df328b4e063012101
parent 592952 0cc6e1e626becc04e268da06a24b50d068451e97
child 592954 03a33d194255338561b228f6d27347d4e76767a1
push id150201
push userechen@mozilla.com
push dateWed, 22 Sep 2021 14:53:23 +0000
treeherderautoland@698c991acc52 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1730117
milestone94.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 1730117 - Part 2: Make EnterModalState suppress event handling for the nested in-process documents; r=smaug This also makes nsIDOMWindowUtils::SuppressEventHandling work properly. Differential Revision: https://phabricator.services.mozilla.com/D125615
dom/base/nsDOMWindowUtils.cpp
dom/base/nsGlobalWindowOuter.cpp
dom/base/nsGlobalWindowOuter.h
dom/base/nsPIDOMWindow.h
dom/base/test/file_suppressed_events_top.html
dom/base/test/file_suppressed_events_top_modalstate.html
dom/base/test/mochitest.ini
dom/base/test/test_suppressed_events_nested_iframe.html
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1630,23 +1630,23 @@ nsDOMWindowUtils::DisableNonTestMouseEve
   PresShell* presShell = docShell->GetPresShell();
   NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
   presShell->DisableNonTestMouseEvents(aDisable);
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDOMWindowUtils::SuppressEventHandling(bool aSuppress) {
-  nsCOMPtr<Document> doc = GetDocument();
-  NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
+  nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow);
+  NS_ENSURE_STATE(window);
 
   if (aSuppress) {
-    doc->SuppressEventHandling();
+    window->SuppressEventHandling();
   } else {
-    doc->UnsuppressEventHandlingAndFireEvents(true);
+    window->UnsuppressEventHandling();
   }
 
   return NS_OK;
 }
 
 static nsresult getScrollXYAppUnits(const nsWeakPtr& aWindow, bool aFlushLayout,
                                     nsPoint& aScrollPos) {
   nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(aWindow);
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -1487,17 +1487,17 @@ void nsGlobalWindowOuter::ShutDown() {
 
   delete sOuterWindowsById;
   sOuterWindowsById = nullptr;
 }
 
 void nsGlobalWindowOuter::DropOuterWindowDocs() {
   MOZ_ASSERT_IF(mDoc, !mDoc->EventHandlingSuppressed());
   mDoc = nullptr;
-  mSuspendedDoc = nullptr;
+  mSuspendedDocs.Clear();
 }
 
 void nsGlobalWindowOuter::CleanUp() {
   // Guarantee idempotence.
   if (mCleanedUp) return;
   mCleanedUp = true;
 
   StartDying();
@@ -1592,17 +1592,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_
   }
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContext)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mControllers)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mArguments)
 
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLocalStorage)
-  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSuspendedDoc)
+  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSuspendedDocs)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentStoragePrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentPartitionedPrincipal)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc)
 
   // Traverse stuff from nsPIDOMWindow
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mChromeEventHandler)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParentTarget)
@@ -1624,17 +1624,17 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(ns
   }
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mContext)
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mControllers)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mArguments)
 
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocalStorage)
-  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSuspendedDoc)
+  NS_IMPL_CYCLE_COLLECTION_UNLINK(mSuspendedDocs)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentPrincipal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentStoragePrincipal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentPartitionedPrincipal)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDoc)
 
   // Unlink stuff from nsPIDOMWindow
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mChromeEventHandler)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mParentTarget)
@@ -2152,19 +2152,19 @@ nsresult nsGlobalWindowOuter::SetNewDocu
   nsDocShell::Cast(mDocShell)->MaybeRestoreWindowName();
 
   // We drop the print request for the old document on the floor, it never made
   // it. We don't close the window here either even if we were asked to.
   mShouldDelayPrintUntilAfterLoad = true;
   mDelayedCloseForPrinting = false;
   mDelayedPrintUntilAfterLoad = false;
 
-  // Take this opportunity to clear mSuspendedDoc. Our old inner window is now
+  // Take this opportunity to clear mSuspendedDocs. Our old inner window is now
   // responsible for unsuspending it.
-  mSuspendedDoc = nullptr;
+  mSuspendedDocs.Clear();
 
 #ifdef DEBUG
   mLastOpenedURI = aDocument->GetDocumentURI();
 #endif
 
   RefPtr<nsGlobalWindowInner> currentInner = GetCurrentInnerWindowInternal();
 
   if (currentInner && currentInner->mNavigator) {
@@ -6351,16 +6351,54 @@ void nsGlobalWindowOuter::ReallyCloseWin
   if (!treeOwnerAsWin) {
     return;
   }
 
   treeOwnerAsWin->Destroy();
   CleanUp();
 }
 
+void nsGlobalWindowOuter::SuppressEventHandling() {
+  if (mSuppressEventHandlingDepth == 0) {
+    if (BrowsingContext* bc = GetBrowsingContext()) {
+      bc->PreOrderWalk([&](BrowsingContext* aBC) {
+        if (nsCOMPtr<nsPIDOMWindowOuter> win = aBC->GetDOMWindow()) {
+          if (RefPtr<Document> doc = win->GetExtantDoc()) {
+            mSuspendedDocs.AppendElement(doc);
+            // Note: Document::SuppressEventHandling will also automatically
+            // suppress event handling for any in-process sub-documents.
+            // However, since we need to deal with cases where remote
+            // BrowsingContexts may be interleaved with in-process ones, we
+            // still need to walk the entire tree ourselves. This may be
+            // slightly redundant in some cases, but since event handling
+            // suppressions maintain a count of current blockers, it does not
+            // cause any problems.
+            doc->SuppressEventHandling();
+          }
+        }
+      });
+    }
+  }
+  mSuppressEventHandlingDepth++;
+}
+
+void nsGlobalWindowOuter::UnsuppressEventHandling() {
+  MOZ_ASSERT(mSuppressEventHandlingDepth != 0);
+  mSuppressEventHandlingDepth--;
+
+  if (mSuppressEventHandlingDepth == 0 && mSuspendedDocs.Length()) {
+    RefPtr<Document> currentDoc = GetExtantDoc();
+    bool fireEvent = currentDoc == mSuspendedDocs[0];
+    nsTArray<RefPtr<Document>> suspendedDocs = std::move(mSuspendedDocs);
+    for (const auto& doc : suspendedDocs) {
+      doc->UnsuppressEventHandlingAndFireEvents(fireEvent);
+    }
+  }
+}
+
 nsGlobalWindowOuter* nsGlobalWindowOuter::EnterModalState() {
   // GetInProcessScriptableTop, not GetInProcessTop, so that EnterModalState
   // works properly with <iframe mozbrowser>.
   nsGlobalWindowOuter* topWin = GetInProcessScriptableTopInternal();
 
   if (!topWin) {
     NS_ERROR("Uh, EnterModalState() called w/o a reachable top window?");
     return nullptr;
@@ -6401,22 +6439,17 @@ nsGlobalWindowOuter* nsGlobalWindowOuter
   Document* topDoc = topWin->GetExtantDoc();
   nsIContent* capturingContent = PresShell::GetCapturingContent();
   if (capturingContent && topDoc &&
       nsContentUtils::ContentIsCrossDocDescendantOf(capturingContent, topDoc)) {
     PresShell::ReleaseCapturingContent();
   }
 
   if (topWin->mModalStateDepth == 0) {
-    NS_ASSERTION(!topWin->mSuspendedDoc, "Shouldn't have mSuspendedDoc here!");
-
-    topWin->mSuspendedDoc = topDoc;
-    if (topDoc) {
-      topDoc->SuppressEventHandling();
-    }
+    topWin->SuppressEventHandling();
 
     if (nsGlobalWindowInner* inner = topWin->GetCurrentInnerWindowInternal()) {
       inner->Suspend();
     }
   }
   topWin->mModalStateDepth++;
   return topWin;
 }
@@ -6440,22 +6473,17 @@ void nsGlobalWindowOuter::LeaveModalStat
   mModalStateDepth--;
 
   nsGlobalWindowInner* inner = GetCurrentInnerWindowInternal();
   if (mModalStateDepth == 0) {
     if (inner) {
       inner->Resume();
     }
 
-    if (mSuspendedDoc) {
-      nsCOMPtr<Document> currentDoc = GetExtantDoc();
-      mSuspendedDoc->UnsuppressEventHandlingAndFireEvents(currentDoc ==
-                                                          mSuspendedDoc);
-      mSuspendedDoc = nullptr;
-    }
+    UnsuppressEventHandling();
   }
 
   // Remember the time of the last dialog quit.
   if (auto* bcg = GetBrowsingContextGroup()) {
     bcg->SetLastDialogQuitTime(TimeStamp::Now());
   }
 
   if (mModalStateDepth == 0) {
@@ -7706,16 +7734,17 @@ mozilla::dom::DocGroup* nsPIDOMWindowOut
     return doc->GetDocGroup();
   }
   return nullptr;
 }
 
 nsPIDOMWindowOuter::nsPIDOMWindowOuter(uint64_t aWindowID)
     : mFrameElement(nullptr),
       mModalStateDepth(0),
+      mSuppressEventHandlingDepth(0),
       mIsBackground(false),
       mMediaSuspend(StaticPrefs::media_block_autoplay_until_in_foreground()
                         ? nsISuspendedTypes::SUSPENDED_BLOCK
                         : nsISuspendedTypes::NONE_SUSPENDED),
       mDesktopModeViewport(false),
       mIsRootOuterWindow(false),
       mInnerWindow(nullptr),
       mWindowID(aWindowID),
--- a/dom/base/nsGlobalWindowOuter.h
+++ b/dom/base/nsGlobalWindowOuter.h
@@ -323,16 +323,19 @@ class nsGlobalWindowOuter final : public
   static void PrepareForProcessChange(JSObject* aProxy);
 
   // Outer windows only.
   void DispatchDOMWindowCreated();
 
   // Outer windows only.
   virtual void EnsureSizeAndPositionUpToDate() override;
 
+  virtual void SuppressEventHandling() override;
+  virtual void UnsuppressEventHandling() override;
+
   MOZ_CAN_RUN_SCRIPT_BOUNDARY virtual nsGlobalWindowOuter* EnterModalState()
       override;
   virtual void LeaveModalState() override;
 
   // Outer windows only.
   virtual bool CanClose() override;
   virtual void ForceClose() override;
 
@@ -1123,20 +1126,20 @@ class nsGlobalWindowOuter final : public
   bool mSetOpenerWindowCalled;
   nsCOMPtr<nsIURI> mLastOpenedURI;
 #endif
 
   bool mCleanedUp;
 
   // It's useful when we get matched EnterModalState/LeaveModalState calls, in
   // which case the outer window is responsible for unsuspending events on the
-  // document. If we don't (for example, if the outer window is closed before
-  // the LeaveModalState call), then the inner window whose mDoc is our
-  // mSuspendedDoc is responsible for unsuspending it.
-  RefPtr<Document> mSuspendedDoc;
+  // documents. If we don't (for example, if the outer window is closed before
+  // the LeaveModalState call), then the inner window whose mDoc is in our
+  // mSuspendedDocs is responsible for unsuspending.
+  nsTArray<RefPtr<Document>> mSuspendedDocs;
 
   // This is the CC generation the last time we called CanSkip.
   uint32_t mCanSkipCCGeneration;
 
   // When non-zero, the document should receive a vrdisplayactivate event
   // after loading.  The value is the ID of the VRDisplay that content should
   // begin presentation on.
   uint32_t mAutoActivateVRDisplayID;
--- a/dom/base/nsPIDOMWindow.h
+++ b/dom/base/nsPIDOMWindow.h
@@ -906,16 +906,23 @@ class nsPIDOMWindowOuter : public mozIDO
   /**
    * Ensure the size and position of this window are up-to-date by doing
    * a layout flush in the parent (which will in turn, do a layout flush
    * in its parent, etc.).
    */
   virtual void EnsureSizeAndPositionUpToDate() = 0;
 
   /**
+   * Suppresses/unsuppresses user initiated event handling in window's document
+   * and all in-process descendant documents.
+   */
+  virtual void SuppressEventHandling() = 0;
+  virtual void UnsuppressEventHandling() = 0;
+
+  /**
    * Callback for notifying a window about a modal dialog being
    * opened/closed with the window as a parent.
    *
    * If any script can run between the enter and leave modal states, and the
    * window isn't top, the LeaveModalState() should be called on the window
    * returned by EnterModalState().
    */
   virtual nsPIDOMWindowOuter* EnterModalState() = 0;
@@ -1119,16 +1126,18 @@ class nsPIDOMWindowOuter : public mozIDO
   nsCOMPtr<mozilla::dom::Element> mFrameElement;
 
   // These references are used by nsGlobalWindow.
   nsCOMPtr<nsIDocShell> mDocShell;
   RefPtr<mozilla::dom::BrowsingContext> mBrowsingContext;
 
   uint32_t mModalStateDepth;
 
+  uint32_t mSuppressEventHandlingDepth;
+
   // Tracks whether our docshell is active.  If it is, mIsBackground
   // is false.  Too bad we have so many different concepts of
   // "active".
   bool mIsBackground;
 
   /**
    * The suspended types can be "disposable" or "permanent". This varable only
    * stores the value about permanent suspend.
copy from dom/base/test/file_suppressed_events_top_xhr.html
copy to dom/base/test/file_suppressed_events_top.html
--- a/dom/base/test/file_suppressed_events_top_xhr.html
+++ b/dom/base/test/file_suppressed_events_top.html
@@ -41,26 +41,23 @@ waitForMessage("ready", async function(e
   let innerDiv = innerWin.document.querySelector("div");
 
   let eventCount = 0;
   innerDiv.addEventListener("mousemove", function() {
     eventCount++;
   });
 
   // Test that event handling is suppressed.
-  let xhr = new XMLHttpRequest();
-  opener.info("xhr open");
-  xhr.open('GET', 'slow.sjs', false);
-
+  let utils = SpecialPowers.getDOMWindowUtils(window);
+  utils.suppressEventHandling(true);
   const TOTAL = 100;
   for (let i = 0; i < TOTAL; i++) {
     synthesizeMouseAtCenter(innerDiv, { type: "mousemove" }, innerWin);
   }
-  xhr.send();
-  opener.info(`xhr done`);
+  utils.suppressEventHandling(false);
 
   // Wait for click event to ensure we have received all mousemove events.
   await waitForClickEvent(innerDiv, innerWin);
   opener.info(`eventCount: ${eventCount}`);
   opener.ok(eventCount < TOTAL, "event should be suspressed");
 
   // Test that event handling is not suppressed.
   eventCount = 0;
copy from dom/base/test/file_suppressed_events_top_xhr.html
copy to dom/base/test/file_suppressed_events_top_modalstate.html
--- a/dom/base/test/file_suppressed_events_top_xhr.html
+++ b/dom/base/test/file_suppressed_events_top_modalstate.html
@@ -41,26 +41,23 @@ waitForMessage("ready", async function(e
   let innerDiv = innerWin.document.querySelector("div");
 
   let eventCount = 0;
   innerDiv.addEventListener("mousemove", function() {
     eventCount++;
   });
 
   // Test that event handling is suppressed.
-  let xhr = new XMLHttpRequest();
-  opener.info("xhr open");
-  xhr.open('GET', 'slow.sjs', false);
-
+  let utils = SpecialPowers.getDOMWindowUtils(window);
+  utils.enterModalState();
   const TOTAL = 100;
   for (let i = 0; i < TOTAL; i++) {
     synthesizeMouseAtCenter(innerDiv, { type: "mousemove" }, innerWin);
   }
-  xhr.send();
-  opener.info(`xhr done`);
+  utils.leaveModalState();
 
   // Wait for click event to ensure we have received all mousemove events.
   await waitForClickEvent(innerDiv, innerWin);
   opener.info(`eventCount: ${eventCount}`);
   opener.ok(eventCount < TOTAL, "event should be suspressed");
 
   // Test that event handling is not suppressed.
   eventCount = 0;
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -773,16 +773,18 @@ skip-if = debug == false
 [test_style_cssText.html]
 [test_suppressed_events_and_scrolling.html]
 support-files =
   file_suppressed_events_and_scrolling.html
 [test_suppressed_events_nested_iframe.html]
 skip-if = os == "android"
 support-files =
   file_suppressed_events_top_xhr.html
+  file_suppressed_events_top_modalstate.html
+  file_suppressed_events_top.html
   file_suppressed_events_middle.html
   file_suppressed_events_inner.html
   !/gfx/layers/apz/test/mochitest/apz_test_utils.js
 [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]
--- a/dom/base/test/test_suppressed_events_nested_iframe.html
+++ b/dom/base/test/test_suppressed_events_nested_iframe.html
@@ -38,12 +38,34 @@ add_task(async function test_sync_xhr() 
     ["dom.events.coalesce.mousemove", false],
   ]});
 
   let w = window.open("file_suppressed_events_top_xhr.html");
   await waitForMessage("done");
   w.close();
 });
 
+add_task(async function test_modalstate() {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["test.events.async.enabled", false],
+    ["dom.events.coalesce.mousemove", false],
+  ]});
+
+  let w = window.open("file_suppressed_events_top_modalstate.html");
+  await waitForMessage("done");
+  w.close();
+});
+
+add_task(async function test_suppress_event_handling() {
+  await SpecialPowers.pushPrefEnv({"set": [
+    ["test.events.async.enabled", false],
+    ["dom.events.coalesce.mousemove", false],
+  ]});
+
+  let w = window.open("file_suppressed_events_top.html");
+  await waitForMessage("done");
+  w.close();
+});
+
 </script>
 </pre>
 </body>
 </html>