Bug 1044074 - Don't run resize or scroll events while documents have events suppressed, r=smaug.
authorBrian Hackett <bhackett1024@gmail.com>
Tue, 18 Dec 2018 15:38:45 -1000
changeset 451301 91fb09d21018429082ba647b8079a7834ad48ee3
parent 451300 c859d4292a5b1c2c8b3e8b8510bee4b26dc3c22a
child 451302 6b4a345b50e94310091b01e8db32b3028ca58f89
child 451364 0e8024bf4e6d29ee36b7d98efb0f47412c5631e6
push id35233
push userebalazs@mozilla.com
push dateWed, 19 Dec 2018 15:46:33 +0000
treeherdermozilla-central@6b4a345b50e9 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1044074
milestone66.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 1044074 - Don't run resize or scroll events while documents have events suppressed, r=smaug.
dom/base/nsDocument.cpp
dom/base/nsIDocument.h
layout/base/PresShell.cpp
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/generic/nsGfxScrollFrame.cpp
layout/generic/nsGfxScrollFrame.h
--- a/dom/base/nsDocument.cpp
+++ b/dom/base/nsDocument.cpp
@@ -1315,16 +1315,17 @@ nsIDocument::nsIDocument()
       mParserAborted(false),
       mReportedUseCounters(false),
       mHasReportedShadowDOMUsage(false),
       mDocTreeHadAudibleMedia(false),
       mDocTreeHadPlayRevoked(false),
 #ifdef DEBUG
       mWillReparent(false),
 #endif
+      mHasDelayedRefreshEvent(false),
       mPendingFullscreenRequests(0),
       mXMLDeclarationBits(0),
       mOnloadBlockCount(0),
       mAsyncOnloadBlockCount(0),
       mCompatMode(eCompatibility_FullStandards),
       mReadyState(ReadyState::READYSTATE_UNINITIALIZED),
 #ifdef MOZILLA_INTERNAL_API
       mVisibilityState(dom::VisibilityState::Hidden),
@@ -8584,16 +8585,28 @@ void nsIDocument::UnsuppressEventHandlin
 
   if (!EventHandlingSuppressed()) {
     MOZ_ASSERT(NS_IsMainThread());
     nsTArray<RefPtr<net::ChannelEventQueue>> queues;
     mSuspendedQueues.SwapElements(queues);
     for (net::ChannelEventQueue* queue : queues) {
       queue->Resume();
     }
+
+    // If there have been any events driven by the refresh driver which were
+    // delayed due to events being suppressed in this document, make sure there
+    // is a refresh scheduled soon so the events will run.
+    if (mHasDelayedRefreshEvent) {
+      mHasDelayedRefreshEvent = false;
+
+      if (mPresShell) {
+        nsRefreshDriver* rd = mPresShell->GetPresContext()->RefreshDriver();
+        rd->RunDelayedEventsSoon();
+      }
+    }
   }
 }
 
 void nsIDocument::AddSuspendedChannelEventQueue(
     net::ChannelEventQueue* aQueue) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(EventHandlingSuppressed());
   mSuspendedQueues.AppendElement(aQueue);
--- a/dom/base/nsIDocument.h
+++ b/dom/base/nsIDocument.h
@@ -2451,16 +2451,20 @@ class nsIDocument : public nsINode,
   /**
    * Note a ChannelEventQueue which has been suspended on the document's behalf
    * to prevent XHRs from running content scripts while event handling is
    * suppressed. The document is responsible for resuming the queue after
    * event handling is unsuppressed.
    */
   void AddSuspendedChannelEventQueue(mozilla::net::ChannelEventQueue* aQueue);
 
+  void SetHasDelayedRefreshEvent() {
+    mHasDelayedRefreshEvent = true;
+  }
+
   /**
    * Increment https://html.spec.whatwg.org/#ignore-destructive-writes-counter
    */
   void IncrementIgnoreDestructiveWritesCounter() {
     ++mIgnoreDestructiveWritesCounter;
   }
 
   /**
@@ -3916,16 +3920,20 @@ class nsIDocument : public nsINode,
 
 #ifdef DEBUG
  public:
   bool mWillReparent : 1;
 
  protected:
 #endif
 
+  // Whether an event triggered by the refresh driver was delayed because this
+  // document has suppressed events.
+  bool mHasDelayedRefreshEvent : 1;
+
   uint8_t mPendingFullscreenRequests;
 
   uint8_t mXMLDeclarationBits;
 
   // Currently active onload blockers.
   uint32_t mOnloadBlockCount;
 
   // Onload blockers which haven't been activated yet.
--- a/layout/base/PresShell.cpp
+++ b/layout/base/PresShell.cpp
@@ -2025,16 +2025,28 @@ nsresult PresShell::ResizeReflowIgnoreOv
   return NS_OK;  // XXX this needs to be real. MMP
 }
 
 void PresShell::FireResizeEvent() {
   if (mIsDocumentGone) {
     return;
   }
 
+  // If event handling is suppressed, repost the resize event to the refresh
+  // driver. The event is marked as delayed so that the refresh driver does not
+  // continue ticking.
+  if (mDocument->EventHandlingSuppressed()) {
+    if (MOZ_LIKELY(!mDocument->GetBFCacheEntry())) {
+      mDocument->SetHasDelayedRefreshEvent();
+      mPresContext->RefreshDriver()->AddResizeEventFlushObserver
+          (this, /* aDelayed = */ true);
+    }
+    return;
+  }
+
   mResizeEventPending = false;
 
   // Send resize event from here.
   WidgetEvent event(true, mozilla::eResize);
   nsEventStatus status = nsEventStatus_eIgnore;
 
   if (nsPIDOMWindowOuter* window = mDocument->GetWindow()) {
     EventDispatcher::Dispatch(window, mPresContext, &event, nullptr, &status);
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -1139,19 +1139,23 @@ void nsRefreshDriver::AddTimerAdjustment
 }
 
 void nsRefreshDriver::RemoveTimerAdjustmentObserver(
     nsATimerAdjustmentObserver* aObserver) {
   MOZ_ASSERT(mTimerAdjustmentObservers.Contains(aObserver));
   mTimerAdjustmentObservers.RemoveElement(aObserver);
 }
 
-void nsRefreshDriver::PostScrollEvent(mozilla::Runnable* aScrollEvent) {
-  mScrollEvents.AppendElement(aScrollEvent);
-  EnsureTimerStarted();
+void nsRefreshDriver::PostScrollEvent(mozilla::Runnable* aScrollEvent, bool aDelayed) {
+  if (aDelayed) {
+    mDelayedScrollEvents.AppendElement(aScrollEvent);
+  } else {
+    mScrollEvents.AppendElement(aScrollEvent);
+    EnsureTimerStarted();
+  }
 }
 
 void nsRefreshDriver::DispatchScrollEvents() {
   // Scroll events are one-shot, so after running them we can drop them.
   // However, dispatching a scroll event can potentially cause more scroll
   // events to be posted, so we move the initial set into a temporary array
   // first. (Newly posted scroll events will be dispatched on the next tick.)
   ScrollEventArray events;
@@ -1205,16 +1209,31 @@ void nsRefreshDriver::NotifyDOMContentLo
   // flushed now.
   if (!HasObservers()) {
     GetPresContext()->NotifyDOMContentFlushed();
   } else {
     mNotifyDOMContentFlushed = true;
   }
 }
 
+void nsRefreshDriver::RunDelayedEventsSoon() {
+  // Place entries for delayed events into their corresponding normal list,
+  // and schedule a refresh. When these delayed events run, if their document
+  // still has events suppressed then they will be readded to the delayed
+  // events list.
+
+  mScrollEvents.AppendElements(mDelayedScrollEvents);
+  mDelayedScrollEvents.Clear();
+
+  mResizeEventFlushObservers.AppendElements(mDelayedResizeEventFlushObservers);
+  mDelayedResizeEventFlushObservers.Clear();
+
+  EnsureTimerStarted();
+}
+
 void nsRefreshDriver::EnsureTimerStarted(EnsureTimerStartedFlags aFlags) {
   // FIXME: Bug 1346065: We should also assert the case where we have
   // STYLO_THREADS=1.
   MOZ_ASSERT(!ServoStyleSet::IsInServoTraversal() || NS_IsMainThread(),
              "EnsureTimerStarted should be called only when we are not "
              "in servo traversal or on the main-thread");
 
   if (mTestControllingRefreshes) return;
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -141,17 +141,17 @@ class nsRefreshDriver final : public moz
                              mozilla::FlushType aFlushType);
   /**
    * Add / remove an observer wants to know the time when the refresh driver
    * updated the most recent refresh time due to its active timer changes.
    */
   void AddTimerAdjustmentObserver(nsATimerAdjustmentObserver* aObserver);
   void RemoveTimerAdjustmentObserver(nsATimerAdjustmentObserver* aObserver);
 
-  void PostScrollEvent(mozilla::Runnable* aScrollEvent);
+  void PostScrollEvent(mozilla::Runnable* aScrollEvent, bool aDelayed = false);
   void DispatchScrollEvents();
 
   /**
    * Add an observer that will be called after each refresh. The caller
    * must remove the observer before it is deleted. This does not trigger
    * refresh driver ticks.
    */
   void AddPostRefreshObserver(nsAPostRefreshObserver* aObserver);
@@ -172,25 +172,31 @@ class nsRefreshDriver final : public moz
    * @returns whether the operation succeeded, or void in the case of removal.
    */
   bool AddImageRequest(imgIRequest* aRequest);
   void RemoveImageRequest(imgIRequest* aRequest);
 
   /**
    * Add / remove presshells which have pending resize event.
    */
-  void AddResizeEventFlushObserver(nsIPresShell* aShell) {
-    MOZ_DIAGNOSTIC_ASSERT(!mResizeEventFlushObservers.Contains(aShell),
+  void AddResizeEventFlushObserver(nsIPresShell* aShell, bool aDelayed = false) {
+    MOZ_DIAGNOSTIC_ASSERT(!mResizeEventFlushObservers.Contains(aShell) &&
+                          !mDelayedResizeEventFlushObservers.Contains(aShell),
                           "Double-adding resize event flush observer");
-    mResizeEventFlushObservers.AppendElement(aShell);
-    EnsureTimerStarted();
+    if (aDelayed) {
+      mDelayedResizeEventFlushObservers.AppendElement(aShell);
+    } else {
+      mResizeEventFlushObservers.AppendElement(aShell);
+      EnsureTimerStarted();
+    }
   }
 
   void RemoveResizeEventFlushObserver(nsIPresShell* aShell) {
     mResizeEventFlushObservers.RemoveElement(aShell);
+    mDelayedResizeEventFlushObservers.RemoveElement(aShell);
   }
 
   /**
    * Add / remove presshells that we should flush style and layout on
    */
   void AddStyleFlushObserver(nsIPresShell* aShell) {
     MOZ_DIAGNOSTIC_ASSERT(!mStyleFlushObservers.Contains(aShell),
                           "Double-adding style flush observer");
@@ -395,16 +401,19 @@ class nsRefreshDriver final : public moz
   static mozilla::Maybe<mozilla::TimeStamp> GetNextTickHint();
 
   static void DispatchIdleRunnableAfterTick(nsIRunnable* aRunnable,
                                             uint32_t aDelay);
   static void CancelIdleRunnable(nsIRunnable* aRunnable);
 
   void NotifyDOMContentLoaded();
 
+  // Schedule a refresh so that any delayed events will run soon.
+  void RunDelayedEventsSoon();
+
  private:
   typedef nsTObserverArray<nsARefreshObserver*> ObserverArray;
   typedef nsTArray<RefPtr<mozilla::Runnable>> ScrollEventArray;
   typedef nsTHashtable<nsISupportsHashKey> RequestTable;
   struct ImageStartData {
     ImageStartData() {}
 
     mozilla::Maybe<mozilla::TimeStamp> mStartTime;
@@ -521,17 +530,21 @@ class nsRefreshDriver final : public moz
   // to be called when the timer is re-started and should not influence its
   // starting or stopping.
   nsTObserverArray<nsATimerAdjustmentObserver*> mTimerAdjustmentObservers;
   RequestTable mRequests;
   ImageStartTable mStartTable;
   AutoTArray<nsCOMPtr<nsIRunnable>, 16> mEarlyRunners;
   ScrollEventArray mScrollEvents;
 
+  // Scroll events on documents that might have events suppressed.
+  ScrollEventArray mDelayedScrollEvents;
+
   AutoTArray<nsIPresShell*, 16> mResizeEventFlushObservers;
+  AutoTArray<nsIPresShell*, 16> mDelayedResizeEventFlushObservers;
   AutoTArray<nsIPresShell*, 16> mStyleFlushObservers;
   AutoTArray<nsIPresShell*, 16> mLayoutFlushObservers;
   // nsTArray on purpose, because we want to be able to swap.
   nsTArray<nsIDocument*> mFrameRequestCallbackDocs;
   nsTArray<nsIDocument*> mThrottledFrameRequestCallbackDocs;
   nsTObserverArray<nsAPostRefreshObserver*> mPostRefreshObservers;
   nsTArray<mozilla::UniquePtr<mozilla::PendingFullscreenEvent>>
       mPendingFullscreenEvents;
--- a/layout/generic/nsGfxScrollFrame.cpp
+++ b/layout/generic/nsGfxScrollFrame.cpp
@@ -4840,19 +4840,19 @@ void ScrollFrameHelper::CurPosAttributeC
         isSmooth ? nsIScrollableFrame::SMOOTH : nsIScrollableFrame::INSTANT,
         nsGkAtoms::scrollbars, &allowedRange);
   }
   // 'this' might be destroyed here
 }
 
 /* ============= Scroll events ========== */
 
-ScrollFrameHelper::ScrollEvent::ScrollEvent(ScrollFrameHelper* aHelper)
+ScrollFrameHelper::ScrollEvent::ScrollEvent(ScrollFrameHelper* aHelper, bool aDelayed)
     : Runnable("ScrollFrameHelper::ScrollEvent"), mHelper(aHelper) {
-  mHelper->mOuter->PresContext()->RefreshDriver()->PostScrollEvent(this);
+  mHelper->mOuter->PresContext()->RefreshDriver()->PostScrollEvent(this, aDelayed);
 }
 
 NS_IMETHODIMP
 ScrollFrameHelper::ScrollEvent::Run() {
   if (mHelper) {
     mHelper->FireScrollEvent();
   }
   return NS_OK;
@@ -4878,16 +4878,26 @@ void ScrollFrameHelper::FireScrollEvent(
   nsCOMPtr<nsIDocShell> docShell = prescontext->GetDocShell();
   AUTO_PROFILER_TRACING_DOCSHELL("Paint", "FireScrollEvent", docShell);
 #endif
 
   MOZ_ASSERT(mScrollEvent);
   mScrollEvent->Revoke();
   mScrollEvent = nullptr;
 
+  // If event handling is suppressed, keep posting the scroll event to the
+  // refresh driver until it is unsuppressed. The event is marked as delayed so
+  // that the refresh driver does not continue ticking.
+  if (content->GetComposedDoc() &&
+      content->GetComposedDoc()->EventHandlingSuppressed()) {
+    content->GetComposedDoc()->SetHasDelayedRefreshEvent();
+    PostScrollEvent(/* aDelayed = */ true);
+    return;
+  }
+
   ActiveLayerTracker::SetCurrentScrollHandlerFrame(mOuter);
   WidgetGUIEvent event(true, eScroll, nullptr);
   nsEventStatus status = nsEventStatus_eIgnore;
   // Fire viewport scroll events at the document (where they
   // will bubble to the window)
   mozilla::layers::ScrollLinkedEffectDetector detector(
       content->GetComposedDoc());
   if (mIsRoot) {
@@ -4899,23 +4909,23 @@ void ScrollFrameHelper::FireScrollEvent(
     // scroll events fired at elements don't bubble (although scroll events
     // fired at documents do, to the window)
     event.mFlags.mBubbles = false;
     EventDispatcher::Dispatch(content, prescontext, &event, nullptr, &status);
   }
   ActiveLayerTracker::SetCurrentScrollHandlerFrame(nullptr);
 }
 
-void ScrollFrameHelper::PostScrollEvent() {
+void ScrollFrameHelper::PostScrollEvent(bool aDelayed) {
   if (mScrollEvent) {
     return;
   }
 
   // The ScrollEvent constructor registers itself with the refresh driver.
-  mScrollEvent = new ScrollEvent(this);
+  mScrollEvent = new ScrollEvent(this, aDelayed);
 }
 
 NS_IMETHODIMP
 ScrollFrameHelper::AsyncScrollPortEvent::Run() {
   if (mHelper) {
     mHelper->mOuter->PresContext()->Document()->FlushPendingNotifications(
         FlushType::InterruptibleLayout);
   }
--- a/layout/generic/nsGfxScrollFrame.h
+++ b/layout/generic/nsGfxScrollFrame.h
@@ -92,17 +92,17 @@ class ScrollFrameHelper : public nsIRefl
   virtual void ReflowCallbackCanceled() override;
 
   /**
    * @note This method might destroy the frame, pres shell and other objects.
    * Called when the 'curpos' attribute on one of the scrollbars changes.
    */
   void CurPosAttributeChanged(nsIContent* aChild, bool aDoScroll = true);
 
-  void PostScrollEvent();
+  void PostScrollEvent(bool aDelayed = false);
   void FireScrollEvent();
   void PostScrolledAreaEvent();
   void FireScrolledAreaEvent();
 
   bool IsSmoothScrollingEnabled();
 
   /**
    * This class handles the dispatching of scroll events to content.
@@ -122,17 +122,17 @@ class ScrollFrameHelper : public nsIRefl
    * causes the refresh driver to tick.
    *
    * ScrollEvents are one-shot runnables; the refresh driver drops them after
    * running them.
    */
   class ScrollEvent : public Runnable {
    public:
     NS_DECL_NSIRUNNABLE
-    explicit ScrollEvent(ScrollFrameHelper* aHelper);
+    explicit ScrollEvent(ScrollFrameHelper* aHelper, bool aDelayed);
     void Revoke() { mHelper = nullptr; }
 
    private:
     ScrollFrameHelper* mHelper;
   };
 
   class ScrollEndEvent : public Runnable {
    public: