Bug 1337963 - Coalesce wheel events in the content process so that long wheel event handlers don't hang the content process. r=smaug
authorStone Shih <sshih@mozilla.com>
Tue, 21 Feb 2017 17:56:46 +0800
changeset 397711 1b47b363a447fbe4906b4ba71369d1c6f8c32795
parent 397710 a4f60181551b8bb66f6623b0bc65008ca16dc755
child 397712 11ce882384cc268ad2be6c6a2c2083b3c0bc81bf
push id1490
push usermtabara@mozilla.com
push dateMon, 31 Jul 2017 14:08:16 +0000
treeherdermozilla-release@70e32e6bf15e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug
bugs1337963
milestone55.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 1337963 - Coalesce wheel events in the content process so that long wheel event handlers don't hang the content process. r=smaug
dom/events/test/test_bug574663.html
dom/ipc/CoalescedWheelData.cpp
dom/ipc/CoalescedWheelData.h
dom/ipc/TabChild.cpp
dom/ipc/TabChild.h
dom/ipc/moz.build
--- a/dom/events/test/test_bug574663.html
+++ b/dom/events/test/test_bug574663.html
@@ -56,31 +56,33 @@ function forceScrollAndWait(scrollbox, c
     waitForPaint(win, utils, callback);
   }
   SpecialPowers.Services.obs.addObserver(postApzFlush, "apz-repaints-flushed", false);
   if (!utils.flushApzRepaints()) {
     postApzFlush();
   }
 }
 
+var kExtraEvents = 5;
+var kDelta = 3;
+
 function sendTouchpadScrollMotion(scrollbox, direction, ctrl, momentum, callback) {
   var win = scrollbox.ownerDocument.defaultView;
   let event = {
     deltaMode: WheelEvent.DOM_DELTA_PIXEL,
-    deltaY: direction * 3,
+    deltaY: direction * kDelta,
     lineOrPageDeltaY: direction,
     ctrlKey: ctrl,
     isMomentum: momentum
   };
-
-  let kExtraEvents = 5;
-
-  var received = 0;
-  var onwheel = function() {
-    if (++received == 1 + kExtraEvents) {
+  let received = 0;
+  let deltaY = 0;
+  var onwheel = function(e) {
+    deltaY += e.deltaY;
+    if (++received == 2) {
       // We have captured all the outstanding wheel events. Wait for the
       // animation to add itself to the refresh driver.
       scrollbox.removeEventListener("wheel", onwheel);
       setTimeout(function() {
         forceScrollAndWait(scrollbox, callback);
       }, 0);
     }
   };
@@ -118,24 +120,26 @@ function runTest() {
       let scrollTopBefore = scrollbox.scrollTop;
       let zoomFactorBefore = winUtils.fullZoom;
 
       let check = function() {
         if (!ctrlKey) {
           let postfix = isMomentum ? ", even after releasing the touchpad" : "";
           // Normal scroll: scroll
           is(winUtils.fullZoom, zoomFactorBefore, "Normal scrolling shouldn't change zoom" + postfix);
-          isnot(scrollbox.scrollTop, scrollTopBefore, "Normal scrolling should scroll" + postfix);
+          is(scrollbox.scrollTop, scrollTopBefore + kDelta * (kExtraEvents + 1),
+             "Normal scrolling should scroll" + postfix);
         } else {
           if (!isMomentum) {
             isnot(winUtils.fullZoom, zoomFactorBefore, "Ctrl-scrolling should zoom while the user is touching the touchpad");
             is(scrollbox.scrollTop, scrollTopBefore, "Ctrl-scrolling shouldn't scroll while the user is touching the touchpad");
           } else {
             is(winUtils.fullZoom, zoomFactorBefore, "Momentum scrolling shouldn't zoom, even when pressing Ctrl");
-            isnot(scrollbox.scrollTop, scrollTopBefore, "Momentum scrolling should scroll, even when pressing Ctrl");
+            is(scrollbox.scrollTop, scrollTopBefore + kDelta * (kExtraEvents + 1),
+               "Momentum scrolling should scroll, even when pressing Ctrl");
           }
         }
 
         if (!outstandingTests.length) {
           winUtils.restoreNormalRefresh();
           win.close();
           SimpleTest.finish();
           return;
new file mode 100644
--- /dev/null
+++ b/dom/ipc/CoalescedWheelData.cpp
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+#include "base/basictypes.h"
+
+#include "CoalescedWheelData.h"
+#include "FrameMetrics.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+void
+CoalescedWheelData::Coalesce(const WidgetWheelEvent& aEvent,
+                             const ScrollableLayerGuid& aGuid,
+                             const uint64_t& aInputBlockId)
+{
+  if (IsEmpty()) {
+    mCoalescedWheelEvent = MakeUnique<WidgetWheelEvent>(aEvent);
+    mGuid = aGuid;
+    mInputBlockId = aInputBlockId;
+  } else {
+    MOZ_ASSERT(mGuid == aGuid);
+    MOZ_ASSERT(mInputBlockId == aInputBlockId);
+    MOZ_ASSERT(mCoalescedWheelEvent->mModifiers == aEvent.mModifiers);
+    MOZ_ASSERT(mCoalescedWheelEvent->mDeltaMode == aEvent.mDeltaMode);
+    MOZ_ASSERT(mCoalescedWheelEvent->mCanTriggerSwipe ==
+               aEvent.mCanTriggerSwipe);
+    mCoalescedWheelEvent->mDeltaX += aEvent.mDeltaX;
+    mCoalescedWheelEvent->mDeltaY += aEvent.mDeltaY;
+    mCoalescedWheelEvent->mDeltaZ += aEvent.mDeltaZ;
+    mCoalescedWheelEvent->mLineOrPageDeltaX += aEvent.mLineOrPageDeltaX;
+    mCoalescedWheelEvent->mLineOrPageDeltaY += aEvent.mLineOrPageDeltaY;
+    mCoalescedWheelEvent->mTimeStamp = aEvent.mTimeStamp;
+  }
+}
+
+bool
+CoalescedWheelData::CanCoalesce(const WidgetWheelEvent& aEvent,
+                                const ScrollableLayerGuid& aGuid,
+                                const uint64_t& aInputBlockId)
+{
+  MOZ_ASSERT(!IsEmpty());
+  return !mCoalescedWheelEvent ||
+         (mCoalescedWheelEvent->mRefPoint == aEvent.mRefPoint &&
+          mCoalescedWheelEvent->mModifiers == aEvent.mModifiers &&
+          mCoalescedWheelEvent->mDeltaMode == aEvent.mDeltaMode &&
+          mCoalescedWheelEvent->mCanTriggerSwipe == aEvent.mCanTriggerSwipe &&
+          mGuid == aGuid &&
+          mInputBlockId == aInputBlockId);
+}
new file mode 100644
--- /dev/null
+++ b/dom/ipc/CoalescedWheelData.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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_dom_CoalescedWheelData_h
+#define mozilla_dom_CoalescedWheelData_h
+
+#include "mozilla/MouseEvents.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class CoalescedWheelData final
+{
+protected:
+  typedef mozilla::layers::ScrollableLayerGuid ScrollableLayerGuid;
+
+public:
+  UniquePtr<WidgetWheelEvent> mCoalescedWheelEvent;
+  ScrollableLayerGuid mGuid;
+  uint64_t mInputBlockId;
+
+  CoalescedWheelData()
+    : mInputBlockId(0)
+  {
+  }
+
+  void Coalesce(const WidgetWheelEvent& aEvent,
+                const ScrollableLayerGuid& aGuid,
+                const uint64_t& aInputBlockId);
+  void Reset()
+  {
+    mCoalescedWheelEvent = nullptr;
+  }
+
+  bool IsEmpty()
+  {
+    return !mCoalescedWheelEvent;
+  }
+
+  bool CanCoalesce(const WidgetWheelEvent& aEvent,
+                   const ScrollableLayerGuid& aGuid,
+                   const uint64_t& aInputBlockId);
+
+  const WidgetWheelEvent* GetCoalescedWheelEvent()
+  {
+    return mCoalescedWheelEvent.get();
+  }
+
+  ScrollableLayerGuid GetScrollableLayerGuid()
+  {
+    return mGuid;
+  }
+
+  uint64_t GetInputBlockId()
+  {
+    return mInputBlockId;
+  }
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_CoalescedWheelData_h
--- a/dom/ipc/TabChild.cpp
+++ b/dom/ipc/TabChild.cpp
@@ -1645,40 +1645,120 @@ TabChild::RecvRealMouseButtonEvent(const
   APZCCallbackHelper::DispatchWidgetEvent(localEvent);
 
   if (aInputBlockId && aEvent.mFlags.mHandledByAPZ) {
     mAPZEventState->ProcessMouseEvent(aEvent, aGuid, aInputBlockId);
   }
   return IPC_OK();
 }
 
-mozilla::ipc::IPCResult
-TabChild::RecvMouseWheelEvent(const WidgetWheelEvent& aEvent,
-                              const ScrollableLayerGuid& aGuid,
-                              const uint64_t& aInputBlockId)
+
+// In case handling repeated mouse wheel takes much time, we skip firing current
+// wheel event if it may be coalesced to the next one.
+bool
+TabChild::MaybeCoalesceWheelEvent(const WidgetWheelEvent& aEvent,
+                                  const ScrollableLayerGuid& aGuid,
+                                  const uint64_t& aInputBlockId,
+                                  bool* aIsNextWheelEvent)
 {
+  MOZ_ASSERT(aIsNextWheelEvent);
+  if (aEvent.mMessage == eWheel) {
+    GetIPCChannel()->PeekMessages(
+        [aIsNextWheelEvent](const IPC::Message& aMsg) -> bool {
+          if (aMsg.type() == mozilla::dom::PBrowser::Msg_MouseWheelEvent__ID) {
+            *aIsNextWheelEvent = true;
+          }
+          return false; // Stop peeking.
+        });
+    // We only coalesce the current event when
+    // 1. It's eWheel (we don't coalesce eOperationStart and eWheelOperationEnd)
+    // 2. It's not the first wheel event.
+    // 3. It's not the last wheel event.
+    // 4. It's dispatched before the last wheel event was processed.
+    // 5. It has same attributes as the coalesced wheel event which is not yet
+    //    fired.
+    if (!mLastWheelProcessedTimeFromParent.IsNull() &&
+        *aIsNextWheelEvent &&
+        aEvent.mTimeStamp < mLastWheelProcessedTimeFromParent &&
+        (mCoalescedWheelData.IsEmpty() ||
+         mCoalescedWheelData.CanCoalesce(aEvent, aGuid, aInputBlockId))) {
+      mCoalescedWheelData.Coalesce(aEvent, aGuid, aInputBlockId);
+      return true;
+    }
+  }
+  return false;
+}
+
+void
+TabChild::MaybeDispatchCoalescedWheelEvent()
+{
+  if (mCoalescedWheelData.IsEmpty()) {
+    return;
+  }
+  const WidgetWheelEvent* wheelEvent =
+    mCoalescedWheelData.GetCoalescedWheelEvent();
+  MOZ_ASSERT(wheelEvent);
+  DispatchWheelEvent(*wheelEvent,
+                     mCoalescedWheelData.GetScrollableLayerGuid(),
+                     mCoalescedWheelData.GetInputBlockId());
+  mCoalescedWheelData.Reset();
+}
+
+void
+TabChild::DispatchWheelEvent(const WidgetWheelEvent& aEvent,
+                                  const ScrollableLayerGuid& aGuid,
+                                  const uint64_t& aInputBlockId)
+{
+  WidgetWheelEvent localEvent(aEvent);
   if (aInputBlockId && aEvent.mFlags.mHandledByAPZ) {
     nsCOMPtr<nsIDocument> document(GetDocument());
     APZCCallbackHelper::SendSetTargetAPZCNotification(
       mPuppetWidget, document, aEvent, aGuid, aInputBlockId);
   }
 
-  WidgetWheelEvent localEvent(aEvent);
   localEvent.mWidget = mPuppetWidget;
   APZCCallbackHelper::ApplyCallbackTransform(localEvent, aGuid,
                                              mPuppetWidget->GetDefaultScale());
   APZCCallbackHelper::DispatchWidgetEvent(localEvent);
 
   if (localEvent.mCanTriggerSwipe) {
     SendRespondStartSwipeEvent(aInputBlockId, localEvent.TriggersSwipe());
   }
 
   if (aInputBlockId && aEvent.mFlags.mHandledByAPZ) {
     mAPZEventState->ProcessWheelEvent(localEvent, aGuid, aInputBlockId);
   }
+}
+
+mozilla::ipc::IPCResult
+TabChild::RecvMouseWheelEvent(const WidgetWheelEvent& aEvent,
+                              const ScrollableLayerGuid& aGuid,
+                              const uint64_t& aInputBlockId)
+{
+  bool isNextWheelEvent = false;
+  if (MaybeCoalesceWheelEvent(aEvent, aGuid, aInputBlockId,
+                              &isNextWheelEvent)) {
+    return IPC_OK();
+  }
+  if (isNextWheelEvent) {
+    // Update mLastWheelProcessedTimeFromParent so that we can compare the end
+    // time of the current event with the dispatched time of the next event.
+    mLastWheelProcessedTimeFromParent = aEvent.mTimeStamp;
+    mozilla::TimeStamp beforeDispatchingTime = TimeStamp::Now();
+    MaybeDispatchCoalescedWheelEvent();
+    DispatchWheelEvent(aEvent, aGuid, aInputBlockId);
+    mLastWheelProcessedTimeFromParent +=
+      (TimeStamp::Now() - beforeDispatchingTime);
+  } else {
+    // This is the last wheel event. Set mLastWheelProcessedTimeFromParent to
+    // null moment to avoid coalesce the next incoming wheel event.
+    mLastWheelProcessedTimeFromParent = TimeStamp();
+    MaybeDispatchCoalescedWheelEvent();
+    DispatchWheelEvent(aEvent, aGuid, aInputBlockId);
+  }
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
 TabChild::RecvRealTouchEvent(const WidgetTouchEvent& aEvent,
                              const ScrollableLayerGuid& aGuid,
                              const uint64_t& aInputBlockId,
                              const nsEventStatus& aApzResponse)
--- a/dom/ipc/TabChild.h
+++ b/dom/ipc/TabChild.h
@@ -23,16 +23,17 @@
 #include "nsFrameMessageManager.h"
 #include "nsIPresShell.h"
 #include "nsIScriptObjectPrincipal.h"
 #include "nsWeakReference.h"
 #include "nsITabChild.h"
 #include "nsITooltipListener.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/dom/TabContext.h"
+#include "mozilla/dom/CoalescedWheelData.h"
 #include "mozilla/DOMEventTargetHelper.h"
 #include "mozilla/EventDispatcher.h"
 #include "mozilla/EventForwards.h"
 #include "mozilla/layers/CompositorTypes.h"
 #include "mozilla/layers/APZCCallbackHelper.h"
 #include "mozilla/layers/CompositorOptions.h"
 #include "nsIWebBrowserChrome3.h"
 #include "mozilla/dom/ipc/IdType.h"
@@ -62,16 +63,17 @@ namespace widget {
 struct AutoCacheNativeKeyCommands;
 } // namespace widget
 
 namespace dom {
 
 class TabChild;
 class TabGroup;
 class ClonedMessageData;
+class CoalescedWheelData;
 class TabChildBase;
 
 class TabChildGlobal : public DOMEventTargetHelper,
                        public nsIContentFrameMessageManager,
                        public nsIScriptObjectPrincipal,
                        public nsIGlobalObject,
                        public nsSupportsWeakReference
 {
@@ -250,16 +252,17 @@ class TabChild final : public TabChildBa
                        public nsSupportsWeakReference,
                        public nsITabChild,
                        public nsIObserver,
                        public TabContext,
                        public nsITooltipListener,
                        public mozilla::ipc::IShmemAllocator
 {
   typedef mozilla::dom::ClonedMessageData ClonedMessageData;
+  typedef mozilla::dom::CoalescedWheelData CoalescedWheelData;
   typedef mozilla::layout::RenderFrameChild RenderFrameChild;
   typedef mozilla::layers::APZEventState APZEventState;
   typedef mozilla::layers::SetAllowedTouchBehaviorCallback SetAllowedTouchBehaviorCallback;
 
 public:
   /**
    * Find TabChild of aTabId in the same content process of the
    * caller.
@@ -746,16 +749,27 @@ private:
   {
     mUnscaledInnerSize = aSize;
   }
 
   bool SkipRepeatedKeyEvent(const WidgetKeyboardEvent& aEvent);
 
   void UpdateRepeatedKeyEventEndTime(const WidgetKeyboardEvent& aEvent);
 
+  bool MaybeCoalesceWheelEvent(const WidgetWheelEvent& aEvent,
+                               const ScrollableLayerGuid& aGuid,
+                               const uint64_t& aInputBlockId,
+                               bool* aIsNextWheelEvent);
+
+  void MaybeDispatchCoalescedWheelEvent();
+
+  void DispatchWheelEvent(const WidgetWheelEvent& aEvent,
+                          const ScrollableLayerGuid& aGuid,
+                          const uint64_t& aInputBlockId);
+
   class DelayedDeleteRunnable;
 
   TextureFactoryIdentifier mTextureFactoryIdentifier;
   nsCOMPtr<nsIWebNavigation> mWebNav;
   RefPtr<PuppetWidget> mPuppetWidget;
   nsCOMPtr<nsIURI> mLastURI;
   RenderFrameChild* mRemoteFrame;
   RefPtr<nsIContentChild> mManager;
@@ -801,16 +815,22 @@ private:
 
   bool mSkipKeyPress;
 
   // Store the end time of the handling of the last repeated keydown/keypress
   // event so that in case event handling takes time, some repeated events can
   // be skipped to not flood child process.
   mozilla::TimeStamp mRepeatedKeyEventTime;
 
+  // Similar to mRepeatedKeyEventTime, store the end time (from parent process)
+  // of handling the last repeated wheel event so that in case event handling
+  // takes time, some repeated events can be skipped to not flood child process.
+  mozilla::TimeStamp mLastWheelProcessedTimeFromParent;
+  CoalescedWheelData mCoalescedWheelData;
+
   AutoTArray<bool, NUMBER_OF_AUDIO_CHANNELS> mAudioChannelsActive;
 
   RefPtr<layers::IAPZCTreeManager> mApzcTreeManager;
 
   // The most recently seen layer observer epoch in RecvSetDocShellIsActive.
   uint64_t mLayerObserverEpoch;
 
 #if defined(XP_WIN) && defined(ACCESSIBILITY)
--- a/dom/ipc/moz.build
+++ b/dom/ipc/moz.build
@@ -14,16 +14,17 @@ XPIDL_SOURCES += [
 XPIDL_MODULE = 'dom'
 
 EXPORTS.mozilla.dom.ipc += [
     'IdType.h',
     'StructuredCloneData.h',
 ]
 
 EXPORTS.mozilla.dom += [
+    'CoalescedWheelData.h',
     'ContentBridgeChild.h',
     'ContentBridgeParent.h',
     'ContentChild.h',
     'ContentParent.h',
     'ContentPrefs.h',
     'ContentProcess.h',
     'ContentProcessManager.h',
     'CPOWManagerGetter.h',
@@ -44,16 +45,17 @@ EXPORTS.mozilla.dom += [
 EXPORTS.mozilla += [
     'PreallocatedProcessManager.h',
     'ProcessHangMonitor.h',
     'ProcessHangMonitorIPC.h',
     'ProcessPriorityManager.h',
 ]
 
 UNIFIED_SOURCES += [
+    'CoalescedWheelData.cpp',
     'ColorPickerParent.cpp',
     'ContentBridgeChild.cpp',
     'ContentBridgeParent.cpp',
     'ContentParent.cpp',
     'ContentPrefs.cpp',
     'ContentProcess.cpp',
     'ContentProcessManager.cpp',
     'DatePickerParent.cpp',