Bug 1534012 - Use a low priority ThrottledEventQueue for postMessages during page load r=smaug
☠☠ backed out by b35cf8000e7a ☠ ☠
authorSean Feng <sefeng@mozilla.com>
Thu, 09 May 2019 14:43:40 +0000
changeset 532104 7fa8e33635fcd787ff275d92fd4e9c8df6127d78
parent 532103 749343db5bbd4d56eab91abf0a475e0d9970eff4
child 532105 6225b9daa9d3e537a24e85a7f36dae882ff0d90a
push id11265
push userffxbld-merge
push dateMon, 13 May 2019 10:53:39 +0000
treeherdermozilla-beta@77e0fe8dbdd3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1534012
milestone68.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 1534012 - Use a low priority ThrottledEventQueue for postMessages during page load r=smaug Differential Revision: https://phabricator.services.mozilla.com/D27386
dom/base/TabGroup.cpp
dom/base/TabGroup.h
dom/base/nsGlobalWindowInner.cpp
dom/base/nsGlobalWindowOuter.cpp
dom/tests/mochitest/dom-level0/file_separate_post_message_queue.html
dom/tests/mochitest/dom-level0/mochitest.ini
dom/tests/mochitest/dom-level0/test_separate_post_message_queue.html
layout/base/nsDocumentViewer.cpp
modules/libpref/init/StaticPrefList.h
testing/web-platform/meta/FileAPI/url/sandboxed-iframe.html.ini
testing/web-platform/tests/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
uriloader/base/nsDocLoader.cpp
xpcom/threads/ThreadEventQueue.cpp
xpcom/threads/ThrottledEventQueue.cpp
xpcom/threads/ThrottledEventQueue.h
xpcom/threads/nsIRunnable.idl
--- a/dom/base/TabGroup.cpp
+++ b/dom/base/TabGroup.cpp
@@ -293,16 +293,55 @@ bool TabGroup::IsBackground() const {
     }
   }
   MOZ_ASSERT(foregrounded == mForegroundCount);
 #endif
 
   return mForegroundCount == 0;
 }
 
+nsresult TabGroup::QueuePostMessageEvent(
+    already_AddRefed<nsIRunnable>&& aRunnable) {
+  if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) {
+    if (!mPostMessageEventQueue) {
+      nsCOMPtr<nsISerialEventTarget> target = GetMainThreadSerialEventTarget();
+      mPostMessageEventQueue = ThrottledEventQueue::Create(
+          target, nsIRunnablePriority::PRIORITY_DEFERRED_TIMERS);
+      nsresult rv = mPostMessageEventQueue->SetIsPaused(false);
+      MOZ_ALWAYS_SUCCEEDS(rv);
+    }
+
+    // Ensure the queue is enabled
+    if (mPostMessageEventQueue->IsPaused()) {
+      nsresult rv = mPostMessageEventQueue->SetIsPaused(false);
+      MOZ_ALWAYS_SUCCEEDS(rv);
+    }
+
+    if (mPostMessageEventQueue) {
+      mPostMessageEventQueue->Dispatch(std::move(aRunnable),
+                                       NS_DISPATCH_NORMAL);
+      return NS_OK;
+    }
+  }
+  return NS_ERROR_FAILURE;
+}
+
+void TabGroup::FlushPostMessageEvents() {
+  if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) {
+    if (mPostMessageEventQueue) {
+      nsresult rv = mPostMessageEventQueue->SetIsPaused(true);
+      MOZ_ALWAYS_SUCCEEDS(rv);
+      nsCOMPtr<nsIRunnable> event;
+      while ((event = mPostMessageEventQueue->GetEvent())) {
+        Dispatch(TaskCategory::Other, event.forget());
+      }
+    }
+  }
+}
+
 uint32_t TabGroup::Count(bool aActiveOnly) const {
   if (!aActiveOnly) {
     return mDocGroups.Count();
   }
 
   uint32_t count = 0;
   for (auto iter = mDocGroups.ConstIter(); !iter.Done(); iter.Next()) {
     if (iter.Get()->mDocGroup->IsActive()) {
--- a/dom/base/TabGroup.h
+++ b/dom/base/TabGroup.h
@@ -18,16 +18,17 @@
 #include "mozilla/RefPtr.h"
 
 class mozIDOMWindowProxy;
 class nsIDocShellTreeItem;
 class nsPIDOMWindowOuter;
 
 namespace mozilla {
 class AbstractThread;
+class ThrottledEventQueue;
 namespace dom {
 class Document;
 class BrowserChild;
 
 // Two browsing contexts are considered "related" if they are reachable from one
 // another through window.opener, window.parent, or window.frames. This is the
 // spec concept of a "unit of related browsing contexts"
 //
@@ -138,16 +139,20 @@ class TabGroup final : public SchedulerG
 
   static LinkedList<TabGroup>* GetTabGroupList() { return sTabGroups; }
 
   // This returns true if all the window objects in all the TabGroups are
   // either inactive (for example in bfcache) or are in background tabs which
   // can be throttled.
   static bool HasOnlyThrottableTabs();
 
+  nsresult QueuePostMessageEvent(already_AddRefed<nsIRunnable>&& aRunnable);
+
+  void FlushPostMessageEvents();
+
  private:
   virtual AbstractThread* AbstractMainThreadForImpl(
       TaskCategory aCategory) override;
 
   TabGroup* AsTabGroup() override { return this; }
 
   void EnsureThrottledEventQueues();
 
@@ -161,14 +166,18 @@ class TabGroup final : public SchedulerG
   const bool mIsChrome;
 
   // Main thread only
   DocGroupMap mDocGroups;
   nsTArray<nsPIDOMWindowOuter*> mWindows;
   uint32_t mForegroundCount;
 
   static LinkedList<TabGroup>* sTabGroups;
+
+  // A queue to store postMessage events during page load, the queue will be
+  // flushed once the page is loaded
+  RefPtr<mozilla::ThrottledEventQueue> mPostMessageEventQueue;
 };
 
 }  // namespace dom
 }  // namespace mozilla
 
 #endif  // defined(TabGroup_h)
--- a/dom/base/nsGlobalWindowInner.cpp
+++ b/dom/base/nsGlobalWindowInner.cpp
@@ -2477,16 +2477,28 @@ bool nsPIDOMWindowInner::HasOpenWebSocke
 void nsPIDOMWindowInner::SetAudioCapture(bool aCapture) {
   RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
   if (service) {
     service->SetWindowAudioCaptured(GetOuterWindow(), mWindowID, aCapture);
   }
 }
 
 void nsPIDOMWindowInner::SetActiveLoadingState(bool aIsLoading) {
+  if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) {
+    if (!aIsLoading) {
+      Document* doc = GetExtantDoc();
+      if (doc) {
+        if (doc->IsTopLevelContentDocument()) {
+          mozilla::dom::TabGroup* tabGroup = doc->GetDocGroup()->GetTabGroup();
+          tabGroup->FlushPostMessageEvents();
+        }
+      }
+    }
+  }
+
   if (!nsGlobalWindowInner::Cast(this)->IsChromeWindow()) {
     mTimeoutManager->SetLoading(aIsLoading);
   }
 
   if (!aIsLoading) {
     for (uint32_t i = 0; i < mAfterLoadRunners.Length(); ++i) {
       NS_DispatchToCurrentThread(mAfterLoadRunners[i].forget());
     }
--- a/dom/base/nsGlobalWindowOuter.cpp
+++ b/dom/base/nsGlobalWindowOuter.cpp
@@ -6052,16 +6052,29 @@ void nsGlobalWindowOuter::PostMessageMoz
       sourceBc, origin, this, providedPrincipal,
       callerInnerWindow ? callerInnerWindow->WindowID() : 0, callerDocumentURI);
 
   event->Write(aCx, aMessage, aTransfer, aError);
   if (NS_WARN_IF(aError.Failed())) {
     return;
   }
 
+  if (StaticPrefs::dom_separate_event_queue_for_post_message_enabled()) {
+    if (mDoc) {
+      Document* doc = mDoc->GetTopLevelContentDocument();
+      if (doc && doc->GetReadyStateEnum() < Document::READYSTATE_COMPLETE) {
+        // As long as the top level is loading, we can dispatch events to the
+        // queue because the queue will be flushed eventually
+        mozilla::dom::TabGroup* tabGroup = TabGroup();
+        aError = tabGroup->QueuePostMessageEvent(event.forget());
+        return;
+      }
+    }
+  }
+
   aError = Dispatch(TaskCategory::Other, event.forget());
 }
 
 class nsCloseEvent : public Runnable {
   RefPtr<nsGlobalWindowOuter> mWindow;
   bool mIndirect;
 
   nsCloseEvent(nsGlobalWindowOuter* aWindow, bool aIndirect)
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/dom-level0/file_separate_post_message_queue.html
@@ -0,0 +1,39 @@
+<script>
+
+let runnable1 = {
+  run() {
+    window.opener.callOrder.push("Runnable1");
+  }
+}
+
+let runnable2 = {
+  run() {
+    window.opener.callOrder.push("Runnable2");
+  }
+}
+
+let runnable3 = {
+  run() {
+    window.opener.callOrder.push("Runnable3");
+  }
+}
+
+window.onmessage = function () {
+  window.opener.callOrder.push("PostMessage");
+  if (window.loadCount == 1) {
+    window.loadCount += 1;
+    location.reload();
+  } else {
+    window.opener.onDone();
+  }
+};
+
+// Pushed to normal queue
+SpecialPowers.Services.tm.dispatchToMainThread(runnable1);
+// Pushed to idle queue
+window.postMessage("bar", "*");
+// Pushed to normal queue
+SpecialPowers.Services.tm.dispatchToMainThread(runnable2);
+// Pushed to normal queue
+SpecialPowers.Services.tm.dispatchToMainThread(runnable3);
+</script>
--- a/dom/tests/mochitest/dom-level0/mochitest.ini
+++ b/dom/tests/mochitest/dom-level0/mochitest.ini
@@ -1,21 +1,23 @@
 [DEFAULT]
 support-files =
   child_ip_address.html
   file_crossdomainprops_inner.html
   file_location.html
+  file_separate_post_message_queue.html
   framed_location.html
   idn_child.html
   innerWidthHeight_script.html
   iframe1_location_setters.html
   iframe2_location_setters.html
   iframe3_location_setters.html
 
 [test_crossdomainprops.html]
 [test_innerWidthHeight_script.html]
 [test_location.html]
 [test_location_framed.html]
 [test_location_getters.html]
 [test_location_sandboxed.html]
 [test_location_setters.html]
 [test_setting_document.domain_idn.html]
 [test_setting_document.domain_to_shortened_ipaddr.html]
+[test_separate_post_message_queue.html]
new file mode 100644
--- /dev/null
+++ b/dom/tests/mochitest/dom-level0/test_separate_post_message_queue.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>Test for using separate event queue for post messages during page load behaviors</title>
+  <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+var tab;
+var callOrder = [];
+function onDone() {
+  tab.close();
+  isDeeply(callOrder, ["Runnable1", "Runnable2", "Runnable3", "PostMessage",
+    "Runnable1", "Runnable2", "Runnable3", "PostMessage"], "Runnables should be fired prior to PostMessage");
+  SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv({"set":[["dom.separate_event_queue_for_post_message.enabled", true]]}, function () {
+  tab = window.open('file_separate_post_message_queue.html');
+  tab.loadCount = 1;
+});
+
+</script>
+</pre>
+</body>
+</html>
--- a/layout/base/nsDocumentViewer.cpp
+++ b/layout/base/nsDocumentViewer.cpp
@@ -17,16 +17,17 @@
 #include "nsReadableUtils.h"
 #include "nsIContent.h"
 #include "nsIContentViewer.h"
 #include "nsIDocumentViewerPrint.h"
 #include "mozilla/dom/BeforeUnloadEvent.h"
 #include "mozilla/dom/PopupBlocker.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/DocGroup.h"
 #include "nsPresContext.h"
 #include "nsIFrame.h"
 #include "nsIWritablePropertyBag2.h"
 #include "nsSubDocumentFrame.h"
 #include "nsGenericHTMLElement.h"
 #include "nsStubMutationObserver.h"
 
 #include "nsILinkHandler.h"
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -721,16 +721,22 @@ VARCACHE_PREF(
 )
 
 VARCACHE_PREF(
   "dom.metaElement.setCookie.allowed",
    dom_metaElement_setCookie_allowed,
   bool, false
 )
 
+VARCACHE_PREF(
+  "dom.separate_event_queue_for_post_message.enabled",
+  dom_separate_event_queue_for_post_message_enabled,
+  bool, true
+)
+
 //---------------------------------------------------------------------------
 // Extension prefs
 //---------------------------------------------------------------------------
 
 #ifdef ANDROID
 // Private browsing opt-in is only supported on Firefox desktop.
 # define PREF_VALUE true
 #else
--- a/testing/web-platform/meta/FileAPI/url/sandboxed-iframe.html.ini
+++ b/testing/web-platform/meta/FileAPI/url/sandboxed-iframe.html.ini
@@ -1,9 +1,10 @@
 [sandboxed-iframe.html]
+  prefs: [dom.separate_event_queue_for_post_message.enabled:false] # Bug in WPT https://github.com/web-platform-tests/wpt/issues/16540
   disabled:
     if (os == "mac"): https://bugzilla.mozilla.org/show_bug.cgi?id=1433190
   [Blob URL parses correctly]
     expected:
       if (os == "mac"): FAIL
 
   [Only exact matches should revoke URLs, using XHR]
     expected: FAIL
--- a/testing/web-platform/tests/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
+++ b/testing/web-platform/tests/content-security-policy/embedded-enforcement/support/testharness-helper.sub.js
@@ -123,17 +123,17 @@ function assert_iframe_with_csp(t, url, 
       if (e.source != i.contentWindow)
           return;
       assert_unreached('No message should be sent from the frame.');
     });
     i.onload = t.step_func(function () {
       // Delay the check until after the postMessage has a chance to execute.
       setTimeout(t.step_func_done(function () {
         assert_equals(loaded[urlId], undefined);
-      }), 1);
+      }), 500);
       assert_throws("SecurityError", () => {
         var x = i.contentWindow.location.href;
       });
     });
   } else if (blockedURI) {
     // Assert iframe loads with an expected violation.
     window.addEventListener('message', t.step_func(e => {
       if (e.source != i.contentWindow)
--- a/uriloader/base/nsDocLoader.cpp
+++ b/uriloader/base/nsDocLoader.cpp
@@ -25,16 +25,17 @@
 #include "nsCOMPtr.h"
 #include "nscore.h"
 #include "nsIWeakReferenceUtils.h"
 #include "nsAutoPtr.h"
 #include "nsQueryObject.h"
 
 #include "nsIDOMWindow.h"
 #include "nsPIDOMWindow.h"
+#include "nsGlobalWindow.h"
 
 #include "nsIStringBundle.h"
 #include "nsIScriptSecurityManager.h"
 
 #include "nsITransport.h"
 #include "nsISocketTransport.h"
 #include "nsIDocShell.h"
 #include "mozilla/dom/Document.h"
--- a/xpcom/threads/ThreadEventQueue.cpp
+++ b/xpcom/threads/ThreadEventQueue.cpp
@@ -84,16 +84,20 @@ bool ThreadEventQueue<InnerQueueT>::PutE
         uint32_t prio = nsIRunnablePriority::PRIORITY_NORMAL;
         runnablePrio->GetPriority(&prio);
         if (prio == nsIRunnablePriority::PRIORITY_HIGH) {
           aPriority = EventQueuePriority::High;
         } else if (prio == nsIRunnablePriority::PRIORITY_INPUT) {
           aPriority = EventQueuePriority::Input;
         } else if (prio == nsIRunnablePriority::PRIORITY_MEDIUMHIGH) {
           aPriority = EventQueuePriority::MediumHigh;
+        } else if (prio == nsIRunnablePriority::PRIORITY_DEFERRED_TIMERS) {
+          aPriority = EventQueuePriority::DeferredTimers;
+        } else if (prio == nsIRunnablePriority::PRIORITY_IDLE) {
+          aPriority = EventQueuePriority::Idle;
         }
       }
     }
 
     MutexAutoLock lock(mLock);
 
     if (mEventsAreDoomed) {
       return false;
--- a/xpcom/threads/ThrottledEventQueue.cpp
+++ b/xpcom/threads/ThrottledEventQueue.cpp
@@ -168,21 +168,24 @@ class ThrottledEventQueue::Inner final :
 #ifdef DEBUG
     bool currentThread = false;
     mBaseTarget->IsOnCurrentThread(&currentThread);
     MOZ_ASSERT(currentThread);
 #endif
 
     {
       MutexAutoLock lock(mMutex);
-
-      // We only check the name of an executor runnable when we know there is
-      // something in the queue, so this should never fail.
       event = mEventQueue.PeekEvent(lock);
-      MOZ_ALWAYS_TRUE(event);
+      // It is possible that mEventQueue wasn't empty when the executor
+      // was added to the queue, but someone processed events from mEventQueue
+      // before the executor, this is why mEventQueue is empty here
+      if (!event) {
+        aName.AssignLiteral("no runnables left in the ThrottledEventQueue");
+        return NS_OK;
+      }
     }
 
     if (nsCOMPtr<nsINamed> named = do_QueryInterface(event)) {
       nsresult rv = named->GetName(aName);
       return rv;
     }
 
     aName.AssignLiteral("non-nsINamed ThrottledEventQueue runnable");
@@ -260,16 +263,21 @@ class ThrottledEventQueue::Inner final :
   }
 
   uint32_t Length() const {
     // Any thread
     MutexAutoLock lock(mMutex);
     return mEventQueue.Count(lock);
   }
 
+  already_AddRefed<nsIRunnable> GetEvent() {
+    MutexAutoLock lock(mMutex);
+    return mEventQueue.GetEvent(nullptr, lock);
+  }
+
   void AwaitIdle() const {
     // Any thread, except the main thread or our base target.  Blocking the
     // main thread is forbidden.  Blocking the base target is guaranteed to
     // produce a deadlock.
     MOZ_ASSERT(!NS_IsMainThread());
 #ifdef DEBUG
     bool onBaseTarget = false;
     Unused << mBaseTarget->IsOnCurrentThread(&onBaseTarget);
@@ -367,16 +375,21 @@ already_AddRefed<ThrottledEventQueue> Th
   RefPtr<ThrottledEventQueue> ref = new ThrottledEventQueue(inner.forget());
   return ref.forget();
 }
 
 bool ThrottledEventQueue::IsEmpty() const { return mInner->IsEmpty(); }
 
 uint32_t ThrottledEventQueue::Length() const { return mInner->Length(); }
 
+// Get the next runnable from the queue
+already_AddRefed<nsIRunnable> ThrottledEventQueue::GetEvent() {
+  return mInner->GetEvent();
+}
+
 void ThrottledEventQueue::AwaitIdle() const { return mInner->AwaitIdle(); }
 
 nsresult ThrottledEventQueue::SetIsPaused(bool aIsPaused) {
   return mInner->SetIsPaused(aIsPaused);
 }
 
 bool ThrottledEventQueue::IsPaused() const { return mInner->IsPaused(); }
 
--- a/xpcom/threads/ThrottledEventQueue.h
+++ b/xpcom/threads/ThrottledEventQueue.h
@@ -72,16 +72,18 @@ class ThrottledEventQueue final : public
       uint32_t aPriority = nsIRunnablePriority::PRIORITY_NORMAL);
 
   // Determine if there are any events pending in the queue.
   bool IsEmpty() const;
 
   // Determine how many events are pending in the queue.
   uint32_t Length() const;
 
+  already_AddRefed<nsIRunnable> GetEvent();
+
   // Block the current thread until the queue is empty. This may not be called
   // on the main thread or the base target. The ThrottledEventQueue must not be
   // paused.
   void AwaitIdle() const;
 
   // If |aIsPaused| is true, pause execution of events from this queue. No
   // events from this queue will be run until this is called with |aIsPaused|
   // false.
--- a/xpcom/threads/nsIRunnable.idl
+++ b/xpcom/threads/nsIRunnable.idl
@@ -20,10 +20,12 @@ interface nsIRunnable : nsISupports
 
 [scriptable, uuid(e75aa42a-80a9-11e6-afb5-e89d87348e2c)]
 interface nsIRunnablePriority : nsISupports
 {
     const unsigned short PRIORITY_NORMAL = 0;
     const unsigned short PRIORITY_INPUT = 1;
     const unsigned short PRIORITY_HIGH = 2;
     const unsigned short PRIORITY_MEDIUMHIGH = 3;
+    const unsigned short PRIORITY_IDLE = 4;
+    const unsigned short PRIORITY_DEFERRED_TIMERS = 5;
     readonly attribute unsigned long priority;
 };