Bug 1451461 - Make pinch locking unaffected by input sensitivity. r=botond
authorjlogandavison <jlogandavison@gmail.com>
Wed, 30 Jan 2019 03:26:04 +0000
changeset 520650 5b3afa9f33be6486d774d079e3d0a51e316bbd18
parent 520649 73824b8dd85567364b7e852b418bdf2312815e6a
child 520651 a6f8093bf1a2a2705bf5b24f21df7c6f140e136d
push id10862
push userffxbld-merge
push dateMon, 11 Mar 2019 13:01:11 +0000
treeherdermozilla-beta@a2e7f5c935da [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbotond
bugs1451461
milestone67.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 1451461 - Make pinch locking unaffected by input sensitivity. r=botond Implements a 50ms buffer of scale events in APZC, so pinch locking code can consider gesture movement over a fixed length of time. Previously, pinch locking was sensitive to event frequency (which is determined by the sensitivity of the input device). * New class InputBuffer wraps std::deque * New field APZC::mPinchEventBuffer * New gfxPref APZPinchLockBufferMaxAge
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/RecentEventsBuffer.h
gfx/layers/apz/test/gtest/APZTestCommon.h
gfx/layers/apz/test/gtest/InputUtils.h
gfx/layers/apz/test/gtest/TestGestureDetector.cpp
gfx/layers/apz/test/gtest/TestPinching.cpp
gfx/thebes/gfxPrefs.h
modules/libpref/init/all.js
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -410,16 +410,23 @@ typedef PlatformSpecificStateBase
  * Distance in inches the user must pinch before lock can be broken.\n
  * Units: (real-world, i.e. screen) inches measured between two touch points
  *
  * \li\b apz.pinch_lock.span_lock_threshold
  * Pinch locking is triggered if the user pinches less than this distance
  * and scrolls more than apz.pinch_lock.scroll_lock_threshold.\n
  * Units: (real-world, i.e. screen) inches measured between two touch points
  *
+ * \li\b apz.pinch_lock.buffer_max_age
+ * To ensure that pinch locking threshold calculations are not affected by
+ * variations in touch screen sensitivity, calculations draw from a buffer of
+ * recent events. This preference specifies the maximum time that events are
+ * held in this buffer.
+ * Units: milliseconds
+ *
  * \li\b apz.popups.enabled
  * Determines whether APZ is used for XUL popup widgets with remote content.
  * Ideally, this should always be true, but it is currently not well tested, and
  * has known issues, so needs to be prefable.
  *
  * \li\b apz.record_checkerboarding
  * Whether or not to record detailed info on checkerboarding events.
  *
@@ -795,16 +802,18 @@ AsyncPanZoomController::AsyncPanZoomCont
       // mTreeManager must be initialized before GetFrameTime() is called
       mTreeManager(aTreeManager),
       mRecursiveMutex("AsyncPanZoomController"),
       mLastContentPaintMetrics(mLastContentPaintMetadata.GetMetrics()),
       mX(this),
       mY(this),
       mPanDirRestricted(false),
       mPinchLocked(false),
+      mPinchEventBuffer(
+          TimeDuration::FromMilliseconds(gfxPrefs::APZPinchLockBufferMaxAge())),
       mZoomConstraints(false, false,
                        Metrics().GetDevPixelsPerCSSPixel() * kViewportMinScale /
                            ParentLayerToScreenScale(1),
                        Metrics().GetDevPixelsPerCSSPixel() * kViewportMaxScale /
                            ParentLayerToScreenScale(1)),
       mLastSampleTime(GetFrameTime()),
       mLastCheckerboardReport(GetFrameTime()),
       mOverscrollEffect(MakeUnique<OverscrollEffect>(*this)),
@@ -1510,47 +1519,35 @@ nsEventStatus AsyncPanZoomController::On
   }
 
   SetState(PINCHING);
   mX.SetVelocity(0);
   mY.SetVelocity(0);
   mLastZoomFocus =
       aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
 
+  mPinchEventBuffer.push(aEvent);
+
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnScale(const PinchGestureInput& aEvent) {
   APZC_LOG("%p got a scale in state %d\n", this, mState);
 
   if (HasReadyTouchBlock() &&
       !GetCurrentTouchBlock()->TouchActionAllowsPinchZoom()) {
     return nsEventStatus_eIgnore;
   }
 
   if (mState != PINCHING) {
     return nsEventStatus_eConsumeNoDefault;
   }
 
-  ParentLayerCoord spanDistance =
-      fabsf(aEvent.mPreviousSpan - aEvent.mCurrentSpan);
-  ParentLayerPoint focusPoint, focusChange;
-  {
-    RecursiveMutexAutoLock lock(mRecursiveMutex);
-
-    focusPoint =
-        aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
-    focusChange = mLastZoomFocus - focusPoint;
-    mLastZoomFocus = focusPoint;
-  }
-
-  HandlePinchLocking(
-      ToScreenCoordinates(ParentLayerPoint(0, spanDistance), focusPoint)
-          .Length(),
-      ToScreenCoordinates(focusChange, focusPoint));
+  mPinchEventBuffer.push(aEvent);
+  HandlePinchLocking();
   bool allowZoom = mZoomConstraints.mAllowZoom && !mPinchLocked;
 
   // If zooming is not allowed, this is a two-finger pan.
   // Tracking panning distance and velocity.
   // UpdateWithTouchAtDevicePoint() acquires the tree lock, so
   // it cannot be called while the mRecursiveMutex lock is held.
   if (!allowZoom) {
     mX.UpdateWithTouchAtDevicePoint(aEvent.mLocalFocusPoint.x, aEvent.mTime);
@@ -1576,18 +1573,22 @@ nsEventStatus AsyncPanZoomController::On
   // page rect and the composition bounds).
   MOZ_ASSERT(Metrics().IsRootContent());
   MOZ_ASSERT(Metrics().GetZoom().AreScalesSame());
 
   {
     RecursiveMutexAutoLock lock(mRecursiveMutex);
 
     CSSToParentLayerScale userZoom = Metrics().GetZoom().ToScaleFactor();
+    ParentLayerPoint focusPoint =
+        aEvent.mLocalFocusPoint - Metrics().GetCompositionBounds().TopLeft();
     CSSPoint cssFocusPoint = focusPoint / Metrics().GetZoom();
 
+    ParentLayerPoint focusChange = mLastZoomFocus - focusPoint;
+    mLastZoomFocus = focusPoint;
     // If displacing by the change in focus point will take us off page bounds,
     // then reduce the displacement such that it doesn't.
     focusChange.x -= mX.DisplacementWillOverscrollAmount(focusChange.x);
     focusChange.y -= mY.DisplacementWillOverscrollAmount(focusChange.y);
     ScrollBy(focusChange / userZoom);
 
     // If the span is zero or close to it, we don't want to process this zoom
     // change because we're going to get wonky numbers for the spanRatio. So
@@ -1696,16 +1697,18 @@ nsEventStatus AsyncPanZoomController::On
 
   {
     RecursiveMutexAutoLock lock(mRecursiveMutex);
     ScheduleComposite();
     RequestContentRepaint();
     UpdateSharedCompositorFrameMetrics();
   }
 
+  mPinchEventBuffer.clear();
+
   // Non-negative focus point would indicate that one finger is still down
   if (aEvent.mLocalFocusPoint != PinchGestureInput::BothFingersLifted()) {
     if (mZoomConstraints.mAllowZoom) {
       mPanDirRestricted = false;
       mX.StartTouch(aEvent.mLocalFocusPoint.x, aEvent.mTime);
       mY.StartTouch(aEvent.mLocalFocusPoint.y, aEvent.mTime);
       SetState(TOUCHING);
     } else {
@@ -2927,18 +2930,48 @@ void AsyncPanZoomController::HandlePanni
           mX.SetAxisLocked(false);
           SetState(PANNING);
         }
       }
     }
   }
 }
 
-void AsyncPanZoomController::HandlePinchLocking(ScreenCoord spanDistance,
-                                                ScreenPoint focusChange) {
+void AsyncPanZoomController::HandlePinchLocking() {
+  // Focus change and span distance calculated from an event buffer
+  // Used to handle pinch locking irrespective of touch screen sensitivity
+  // Note: both values fall back to the same value as
+  //       their un-buffered counterparts if there is only one (the latest)
+  //       event in the buffer. ie: when the touch screen is dispatching
+  //       events slower than the lifetime of the buffer
+  ParentLayerCoord bufferedSpanDistance;
+  ParentLayerPoint focusPoint, bufferedFocusChange;
+  {
+    RecursiveMutexAutoLock lock(mRecursiveMutex);
+
+    focusPoint = mPinchEventBuffer.back().mLocalFocusPoint -
+                 Metrics().GetCompositionBounds().TopLeft();
+    ParentLayerPoint bufferedLastZoomFocus =
+        (mPinchEventBuffer.size() > 1)
+            ? mPinchEventBuffer.front().mLocalFocusPoint -
+                  Metrics().GetCompositionBounds().TopLeft()
+            : mLastZoomFocus;
+
+    bufferedFocusChange = bufferedLastZoomFocus - focusPoint;
+    bufferedSpanDistance = fabsf(mPinchEventBuffer.front().mPreviousSpan -
+                                 mPinchEventBuffer.back().mCurrentSpan);
+  }
+
+  // Convert to screen coordinates
+  ScreenCoord spanDistance =
+      ToScreenCoordinates(ParentLayerPoint(0, bufferedSpanDistance), focusPoint)
+          .Length();
+  ScreenPoint focusChange =
+      ToScreenCoordinates(bufferedFocusChange, focusPoint);
+
   if (mPinchLocked) {
     if (GetPinchLockMode() == PINCH_STICKY) {
       ScreenCoord spanBreakoutThreshold =
           gfxPrefs::APZPinchLockSpanBreakoutThreshold() * GetDPI();
       mPinchLocked = !(spanDistance > spanBreakoutThreshold);
     }
   } else {
     if (GetPinchLockMode() != PINCH_FREE) {
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -23,16 +23,17 @@
 #include "APZUtils.h"
 #include "Layers.h"  // for Layer::ScrollDirection
 #include "LayersTypes.h"
 #include "mozilla/gfx/Matrix.h"
 #include "nsIScrollableFrame.h"
 #include "nsRegion.h"
 #include "nsTArray.h"
 #include "PotentialCheckerboardDurationTracker.h"
+#include "RecentEventsBuffer.h"  // for RecentEventsBuffer
 
 #include "base/message_loop.h"
 
 namespace mozilla {
 
 namespace ipc {
 
 class SharedMemoryBasic;
@@ -790,17 +791,17 @@ class AsyncPanZoomController {
   /**
    * Update the panning state and axis locks.
    */
   void HandlePanningUpdate(const ScreenPoint& aDelta);
 
   /**
    * Set and update the pinch lock
    */
-  void HandlePinchLocking(ScreenCoord spanDistance, ScreenPoint focusChange);
+  void HandlePinchLocking();
 
   /**
    * Sets up anything needed for panning. This takes us out of the "TOUCHING"
    * state and starts actually panning us. We provide the physical pixel
    * position of the start point so that the pan gesture is calculated
    * regardless of if the window/GeckoView moved during the pan.
    */
   nsEventStatus StartPanning(const ExternalPoint& aStartPoint);
@@ -978,16 +979,22 @@ class AsyncPanZoomController {
   // This flag is set to true when we are in a axis-locked pan as a result of
   // the touch-action CSS property.
   bool mPanDirRestricted;
 
   // This flag is set to true when we are in a pinch-locked state. ie: user
   // is performing a two-finger pan rather than a pinch gesture
   bool mPinchLocked;
 
+  // Stores the pinch events that occured within a given timeframe. Used to
+  // calculate the focusChange and spanDistance within a fixed timeframe.
+  // RecentEventsBuffer is not threadsafe. Should only be accessed on the
+  // controller thread.
+  RecentEventsBuffer<PinchGestureInput> mPinchEventBuffer;
+
   // Most up-to-date constraints on zooming. These should always be reasonable
   // values; for example, allowing a min zoom of 0.0 can cause very bad things
   // to happen.
   ZoomConstraints mZoomConstraints;
 
   // The last time the compositor has sampled the content transform for this
   // frame.
   TimeStamp mLastSampleTime;
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/RecentEventsBuffer.h
@@ -0,0 +1,83 @@
+/* -*- 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_layers_RecentEventsBuffer_h
+#define mozilla_layers_RecentEventsBuffer_h
+
+#include <deque>
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+namespace layers {
+/**
+ * RecentEventsBuffer: maintains an age constrained buffer of events
+ *
+ * Intended for use with elements of type InputData, but the only requirement
+ * is a member "mTimeStamp" of type TimeStamp
+ */
+template <typename Event>
+class RecentEventsBuffer {
+ public:
+  explicit RecentEventsBuffer(TimeDuration maxAge);
+
+  void push(Event event);
+  void clear();
+
+  typedef typename std::deque<Event>::size_type size_type;
+  size_type size() { return mBuffer.size(); }
+
+  // Delegate to container for iterators
+  typedef typename std::deque<Event>::iterator iterator;
+  typedef typename std::deque<Event>::const_iterator const_iterator;
+  iterator begin() { return mBuffer.begin(); }
+  iterator end() { return mBuffer.end(); }
+  const_iterator cbegin() const { return mBuffer.cbegin(); }
+  const_iterator cend() const { return mBuffer.cend(); }
+
+  // Also delegate for front/back
+  typedef typename std::deque<Event>::reference reference;
+  typedef typename std::deque<Event>::const_reference const_reference;
+  reference front() { return mBuffer.front(); }
+  reference back() { return mBuffer.back(); }
+  const_reference front() const { return mBuffer.front(); }
+  const_reference back() const { return mBuffer.back(); }
+
+ private:
+  TimeDuration mMaxAge;
+  std::deque<Event> mBuffer;
+};
+
+template <typename Event>
+RecentEventsBuffer<Event>::RecentEventsBuffer(TimeDuration maxAge)
+    : mMaxAge(maxAge), mBuffer() {}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::push(Event event) {
+  // Events must be pushed in chronological order
+  MOZ_ASSERT(mBuffer.empty() || mBuffer.back().mTimeStamp <= event.mTimeStamp);
+
+  mBuffer.push_back(event);
+
+  // Flush all events older than the given lifetime
+  TimeStamp bound = event.mTimeStamp - mMaxAge;
+  while (!mBuffer.empty()) {
+    if (mBuffer.front().mTimeStamp >= bound) {
+      break;
+    }
+    mBuffer.pop_front();
+  }
+}
+
+template <typename Event>
+void RecentEventsBuffer<Event>::clear() {
+  mBuffer.clear();
+}
+
+}  // namespace layers
+}  // namespace mozilla
+
+#endif  // mozilla_layers_RecentEventsBuffer_h
--- a/gfx/layers/apz/test/gtest/APZTestCommon.h
+++ b/gfx/layers/apz/test/gtest/APZTestCommon.h
@@ -57,16 +57,25 @@ inline SingleTouchData CreateSingleTouch
 
 // Convenience wrapper for CreateSingleTouchData() that takes loose coordinates.
 inline SingleTouchData CreateSingleTouchData(int32_t aIdentifier,
                                              ScreenIntCoord aX,
                                              ScreenIntCoord aY) {
   return CreateSingleTouchData(aIdentifier, ScreenIntPoint(aX, aY));
 }
 
+inline PinchGestureInput CreatePinchGestureInput(
+    PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus,
+    float aCurrentSpan, float aPreviousSpan, TimeStamp timestamp) {
+  ParentLayerPoint localFocus(aFocus.x, aFocus.y);
+  PinchGestureInput result(aType, 0, timestamp, ExternalPoint(0, 0), aFocus,
+                           aCurrentSpan, aPreviousSpan, 0);
+  return result;
+}
+
 template <class SetArg, class Storage>
 class ScopedGfxSetting {
  public:
   ScopedGfxSetting(SetArg (*aGetPrefFunc)(void), void (*aSetPrefFunc)(SetArg),
                    SetArg aVal)
       : mSetPrefFunc(aSetPrefFunc) {
     mOldVal = aGetPrefFunc();
     aSetPrefFunc(aVal);
@@ -445,16 +454,28 @@ class APZCTesterBase : public ::testing:
       PinchOptions aOptions = PinchOptions::LiftBothFingers);
 
   template <class InputReceiver>
   void PinchWithTouchInputAndCheckStatus(
       const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
       float aScale, int& inputId, bool aShouldTriggerPinch,
       nsTArray<uint32_t>* aAllowedTouchBehaviors);
 
+  template <class InputReceiver>
+  void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
+                           const ScreenIntPoint& aFocus,
+                           const ScreenIntPoint& aSecondFocus, float aScale,
+                           nsEventStatus (*aOutEventStatuses)[3] = nullptr);
+
+  template <class InputReceiver>
+  void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
+                                         const ScreenIntPoint& aFocus,
+                                         float aScale,
+                                         bool aShouldTriggerPinch);
+
  protected:
   RefPtr<MockContentControllerDelayed> mcc;
 };
 
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PanOptions)
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(APZCTesterBase::PinchOptions)
 
 template <class InputReceiver>
@@ -793,16 +814,67 @@ void APZCTesterBase::PinchWithTouchInput
   nsEventStatus expectedMoveStatus = aShouldTriggerPinch
                                          ? nsEventStatus_eConsumeDoDefault
                                          : nsEventStatus_eIgnore;
   EXPECT_EQ(nsEventStatus_eConsumeDoDefault, statuses[0]);
   EXPECT_EQ(expectedMoveStatus, statuses[1]);
   EXPECT_EQ(expectedMoveStatus, statuses[2]);
 }
 
+template <class InputReceiver>
+void APZCTesterBase::PinchWithPinchInput(
+    const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+    const ScreenIntPoint& aSecondFocus, float aScale,
+    nsEventStatus (*aOutEventStatuses)[3]) {
+  const TimeDuration TIME_BETWEEN_PINCH_INPUT =
+      TimeDuration::FromMilliseconds(50);
+
+  nsEventStatus actualStatus = aTarget->ReceiveInputEvent(
+      CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, aFocus,
+                              10.0, 10.0, mcc->Time()),
+      nullptr);
+  if (aOutEventStatuses) {
+    (*aOutEventStatuses)[0] = actualStatus;
+  }
+  mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
+
+  actualStatus = aTarget->ReceiveInputEvent(
+      CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
+                              aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()),
+      nullptr);
+  if (aOutEventStatuses) {
+    (*aOutEventStatuses)[1] = actualStatus;
+  }
+  mcc->AdvanceBy(TIME_BETWEEN_PINCH_INPUT);
+
+  actualStatus = aTarget->ReceiveInputEvent(
+      CreatePinchGestureInput(
+          PinchGestureInput::PINCHGESTURE_END,
+          PinchGestureInput::BothFingersLifted<ScreenPixel>(), 10.0 * aScale,
+          10.0 * aScale, mcc->Time()),
+      nullptr);
+  if (aOutEventStatuses) {
+    (*aOutEventStatuses)[2] = actualStatus;
+  }
+}
+
+template <class InputReceiver>
+void APZCTesterBase::PinchWithPinchInputAndCheckStatus(
+    const RefPtr<InputReceiver>& aTarget, const ScreenIntPoint& aFocus,
+    float aScale, bool aShouldTriggerPinch) {
+  nsEventStatus statuses[3];  // scalebegin, scale, scaleend
+  PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
+
+  nsEventStatus expectedStatus = aShouldTriggerPinch
+                                     ? nsEventStatus_eConsumeNoDefault
+                                     : nsEventStatus_eIgnore;
+  EXPECT_EQ(expectedStatus, statuses[0]);
+  EXPECT_EQ(expectedStatus, statuses[1]);
+}
+
 AsyncPanZoomController* TestAPZCTreeManager::NewAPZCInstance(
     LayersId aLayersId, GeckoContentController* aController) {
   MockContentControllerDelayed* mcc =
       static_cast<MockContentControllerDelayed*>(aController);
   return new TestAsyncPanZoomController(
       aLayersId, mcc, this, AsyncPanZoomController::USE_GESTURE_DETECTOR);
 }
 
--- a/gfx/layers/apz/test/gtest/InputUtils.h
+++ b/gfx/layers/apz/test/gtest/InputUtils.h
@@ -23,25 +23,16 @@
  * void SetAllowedTouchBehavior(uint64_t aInputBlockId,
  *                              const nsTArray<uint32_t>& aBehaviours);
  * The classes that currently implement these are APZCTreeManager and
  * TestAsyncPanZoomController. Using this template allows us to test individual
  * APZC instances in isolation and also an entire APZ tree, while using the same
  * code to dispatch input events.
  */
 
-inline PinchGestureInput CreatePinchGestureInput(
-    PinchGestureInput::PinchGestureType aType, const ScreenPoint& aFocus,
-    float aCurrentSpan, float aPreviousSpan) {
-  ParentLayerPoint localFocus(aFocus.x, aFocus.y);
-  PinchGestureInput result(aType, 0, TimeStamp(), ExternalPoint(0, 0), aFocus,
-                           aCurrentSpan, aPreviousSpan, 0);
-  return result;
-}
-
 template <class InputReceiver>
 void SetDefaultAllowedTouchBehavior(const RefPtr<InputReceiver>& aTarget,
                                     uint64_t aInputBlockId,
                                     int touchPoints = 1) {
   nsTArray<uint32_t> defaultBehaviors;
   // use the default value where everything is allowed
   for (int i = 0; i < touchPoints; i++) {
     defaultBehaviors.AppendElement(
@@ -82,60 +73,16 @@ nsEventStatus TouchUp(const RefPtr<Input
                       const ScreenIntPoint& aPoint, TimeStamp aTime) {
   MultiTouchInput mti =
       CreateMultiTouchInput(MultiTouchInput::MULTITOUCH_END, aTime);
   mti.mTouches.AppendElement(CreateSingleTouchData(0, aPoint));
   return aTarget->ReceiveInputEvent(mti, nullptr, nullptr);
 }
 
 template <class InputReceiver>
-void PinchWithPinchInput(const RefPtr<InputReceiver>& aTarget,
-                         const ScreenIntPoint& aFocus,
-                         const ScreenIntPoint& aSecondFocus, float aScale,
-                         nsEventStatus (*aOutEventStatuses)[3] = nullptr) {
-  nsEventStatus actualStatus = aTarget->ReceiveInputEvent(
-      CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, aFocus,
-                              10.0, 10.0),
-      nullptr);
-  if (aOutEventStatuses) {
-    (*aOutEventStatuses)[0] = actualStatus;
-  }
-  actualStatus = aTarget->ReceiveInputEvent(
-      CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
-                              aSecondFocus, 10.0 * aScale, 10.0),
-      nullptr);
-  if (aOutEventStatuses) {
-    (*aOutEventStatuses)[1] = actualStatus;
-  }
-  actualStatus = aTarget->ReceiveInputEvent(
-      CreatePinchGestureInput(
-          PinchGestureInput::PINCHGESTURE_END,
-          PinchGestureInput::BothFingersLifted<ScreenPixel>(), 10.0 * aScale,
-          10.0 * aScale),
-      nullptr);
-  if (aOutEventStatuses) {
-    (*aOutEventStatuses)[2] = actualStatus;
-  }
-}
-
-template <class InputReceiver>
-void PinchWithPinchInputAndCheckStatus(const RefPtr<InputReceiver>& aTarget,
-                                       const ScreenIntPoint& aFocus,
-                                       float aScale, bool aShouldTriggerPinch) {
-  nsEventStatus statuses[3];  // scalebegin, scale, scaleend
-  PinchWithPinchInput(aTarget, aFocus, aFocus, aScale, &statuses);
-
-  nsEventStatus expectedStatus = aShouldTriggerPinch
-                                     ? nsEventStatus_eConsumeNoDefault
-                                     : nsEventStatus_eIgnore;
-  EXPECT_EQ(expectedStatus, statuses[0]);
-  EXPECT_EQ(expectedStatus, statuses[1]);
-}
-
-template <class InputReceiver>
 nsEventStatus Wheel(const RefPtr<InputReceiver>& aTarget,
                     const ScreenIntPoint& aPoint, const ScreenPoint& aDelta,
                     TimeStamp aTime, uint64_t* aOutInputBlockId = nullptr) {
   ScrollWheelInput input(MillisecondsSinceStartup(aTime), aTime, 0,
                          ScrollWheelInput::SCROLLMODE_INSTANT,
                          ScrollWheelInput::SCROLLDELTA_PIXEL, aPoint, aDelta.x,
                          aDelta.y, false, WheelDeltaAdjustmentStrategy::eNone);
   return aTarget->ReceiveInputEvent(input, nullptr, aOutInputBlockId);
--- a/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
+++ b/gfx/layers/apz/test/gtest/TestGestureDetector.cpp
@@ -37,78 +37,87 @@ TEST_F(APZCGestureDetectorTester, Pan_Af
 
   // Test parameters
   float zoomAmount = 1.25;
   float pinchLength = 100.0;
   float pinchLengthScaled = pinchLength * zoomAmount;
   int focusX = 250;
   int focusY = 300;
   int panDistance = 20;
+  const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+      TimeDuration::FromMilliseconds(50);
 
   int firstFingerId = 0;
   int secondFingerId = firstFingerId + 1;
 
   // Put fingers down
   MultiTouchInput mti =
-      MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+      MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX, focusY));
   mti.mTouches.AppendElement(
       CreateSingleTouchData(secondFingerId, focusX, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // Spread fingers out to enter the pinch state
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX - pinchLength, focusY));
   mti.mTouches.AppendElement(
       CreateSingleTouchData(secondFingerId, focusX + pinchLength, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // Do the actual pinch of 1.25x
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
   mti.mTouches.AppendElement(CreateSingleTouchData(
       secondFingerId, focusX + pinchLengthScaled, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // Verify that the zoom changed, just to make sure our code above did what it
   // was supposed to.
   FrameMetrics zoomedMetrics = apzc->GetFrameMetrics();
   float newZoom = zoomedMetrics.GetZoom().ToScaleFactor().scale;
   EXPECT_EQ(originalMetrics.GetZoom().ToScaleFactor().scale * zoomAmount,
             newZoom);
 
   // Now we lift one finger...
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(CreateSingleTouchData(
       secondFingerId, focusX + pinchLengthScaled, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // ... and pan with the remaining finger. This pan just breaks through the
   // distance threshold.
   focusY += 40;
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // This one does an actual pan of 20 pixels
   focusY += panDistance;
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // Lift the remaining finger
-  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+  mti = MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
   mti.mTouches.AppendElement(
       CreateSingleTouchData(firstFingerId, focusX - pinchLengthScaled, focusY));
   apzc->ReceiveInputEvent(mti, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   // Verify that we scrolled
   FrameMetrics finalMetrics = apzc->GetFrameMetrics();
   EXPECT_EQ(zoomedMetrics.GetScrollOffset().y - (panDistance / newZoom),
             finalMetrics.GetScrollOffset().y);
 
   // Clear out any remaining fling animation and pending tasks
   apzc->AdvanceAnimationsUntilEnd();
--- a/gfx/layers/apz/test/gtest/TestPinching.cpp
+++ b/gfx/layers/apz/test/gtest/TestPinching.cpp
@@ -149,54 +149,57 @@ class APZCPinchLockingTester : public AP
   virtual void SetUp() {
     APZCPinchTester::SetUp();
     tm->SetDPI(mDPI);
     apzc->SetFrameMetrics(GetPinchableFrameMetrics());
     MakeApzcZoomable();
 
     apzc->ReceiveInputEvent(
         CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, mFocus,
-                                mSpan, mSpan),
+                                mSpan, mSpan, mcc->Time()),
         nullptr);
+    mcc->AdvanceBy(TimeDuration::FromMilliseconds(51));
   }
 
   void twoFingerPan() {
     ScreenCoord panDistance =
         gfxPrefs::APZPinchLockScrollLockThreshold() * 1.2 * tm->GetDPI();
 
     mFocus = ScreenIntPoint((int)(mFocus.x + panDistance), (int)(mFocus.y));
 
     apzc->ReceiveInputEvent(
         CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus,
-                                mSpan, mSpan),
+                                mSpan, mSpan, mcc->Time()),
         nullptr);
+    mcc->AdvanceBy(TimeDuration::FromMilliseconds(51));
   }
 
   void twoFingerZoom() {
     float pinchDistance =
         gfxPrefs::APZPinchLockSpanBreakoutThreshold() * 1.2 * tm->GetDPI();
 
     float newSpan = mSpan + pinchDistance;
 
     apzc->ReceiveInputEvent(
         CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus,
-                                newSpan, mSpan),
+                                newSpan, mSpan, mcc->Time()),
         nullptr);
+    mcc->AdvanceBy(TimeDuration::FromMilliseconds(51));
     mSpan = newSpan;
   }
 
   bool isPinchLockActive() {
     FrameMetrics originalMetrics = apzc->GetFrameMetrics();
 
     // Send a small scale input to the APZC
     float pinchDistance =
         gfxPrefs::APZPinchLockSpanBreakoutThreshold() * 0.8 * tm->GetDPI();
     apzc->ReceiveInputEvent(
         CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE, mFocus,
-                                mSpan + pinchDistance, mSpan),
+                                mSpan + pinchDistance, mSpan, mcc->Time()),
         nullptr);
 
     FrameMetrics result = apzc->GetFrameMetrics();
     bool lockActive =
         originalMetrics.GetZoom() == result.GetZoom() &&
         originalMetrics.GetScrollOffset().x == result.GetScrollOffset().x &&
         originalMetrics.GetScrollOffset().y == result.GetScrollOffset().y;
 
@@ -489,39 +492,45 @@ TEST_F(APZCPinchGestureDetectorTester, P
                                  apzc->GetGuid(), LayoutDeviceCoord(0), _))
       .Times(1);
 
   int inputId = 0;
   ScreenIntPoint focus(250, 300);
 
   // Do a pinch holding a zero span and moving the focus by y=100
 
+  const TimeDuration TIME_BETWEEN_TOUCH_EVENT =
+      TimeDuration::FromMilliseconds(50);
+
   MultiTouchInput mtiStart =
-      MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, TimeStamp(), 0);
+      MultiTouchInput(MultiTouchInput::MULTITOUCH_START, 0, mcc->Time(), 0);
   mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
   mtiStart.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
   apzc->ReceiveInputEvent(mtiStart, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   focus.y -= 35 + 1;  // this is to get over the PINCH_START_THRESHOLD in
                       // GestureEventListener.cpp
   MultiTouchInput mtiMove1 =
-      MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+      MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
   mtiMove1.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
   apzc->ReceiveInputEvent(mtiMove1, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   focus.y -= 100;  // do a two-finger scroll of 100 screen pixels
   MultiTouchInput mtiMove2 =
-      MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, TimeStamp(), 0);
+      MultiTouchInput(MultiTouchInput::MULTITOUCH_MOVE, 0, mcc->Time(), 0);
   mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
   mtiMove2.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
   apzc->ReceiveInputEvent(mtiMove2, nullptr);
+  mcc->AdvanceBy(TIME_BETWEEN_TOUCH_EVENT);
 
   MultiTouchInput mtiEnd =
-      MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, TimeStamp(), 0);
+      MultiTouchInput(MultiTouchInput::MULTITOUCH_END, 0, mcc->Time(), 0);
   mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId, focus));
   mtiEnd.mTouches.AppendElement(CreateSingleTouchData(inputId + 1, focus));
   apzc->ReceiveInputEvent(mtiEnd, nullptr);
 
   // Done, check the metrics to make sure we scrolled by 100 screen pixels,
   // which is 50 CSS pixels for the pinchable frame metrics.
 
   FrameMetrics fm = apzc->GetFrameMetrics();
@@ -544,22 +553,22 @@ TEST_F(APZCPinchTester, Pinch_TwoFinger_
   // Send only the PINCHGESTURE_START and PINCHGESTURE_SCALE events,
   // in order to trigger a call to AsyncPanZoomController::OnScale
   // but not to AsyncPanZoomController::OnScaleEnd.
   ScreenIntPoint aFocus(250, 350);
   ScreenIntPoint aSecondFocus(200, 300);
   float aScale = 10;
   apzc->ReceiveInputEvent(
       CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_START, aFocus,
-                              10.0, 10.0),
+                              10.0, 10.0, mcc->Time()),
       nullptr);
 
   apzc->ReceiveInputEvent(
       CreatePinchGestureInput(PinchGestureInput::PINCHGESTURE_SCALE,
-                              aSecondFocus, 10.0 * aScale, 10.0),
+                              aSecondFocus, 10.0 * aScale, 10.0, mcc->Time()),
       nullptr);
 }
 
 TEST_F(APZCPinchLockingTester, Pinch_Locking_Free) {
   SCOPED_GFX_PREF(APZPinchLockMode, int32_t, 0);  // PINCH_FREE
 
   twoFingerPan();
   EXPECT_FALSE(isPinchLockActive());
--- a/gfx/thebes/gfxPrefs.h
+++ b/gfx/thebes/gfxPrefs.h
@@ -344,16 +344,17 @@ class gfxPrefs final {
   DECL_GFX_PREF(Live, "apz.overscroll.spring_stiffness",       APZOverscrollSpringStiffness, float, 0.001f);
   DECL_GFX_PREF(Live, "apz.overscroll.stop_distance_threshold", APZOverscrollStopDistanceThreshold, float, 5.0f);
   DECL_GFX_PREF(Live, "apz.paint_skipping.enabled",            APZPaintSkipping, bool, true);
   DECL_GFX_PREF(Live, "apz.peek_messages.enabled",             APZPeekMessages, bool, true);
   DECL_GFX_PREF(Live, "apz.pinch_lock.mode",                   APZPinchLockMode, int32_t, 1);
   DECL_GFX_PREF(Live, "apz.pinch_lock.scroll_lock_threshold",  APZPinchLockScrollLockThreshold, float, 1.0f / 32.0f);
   DECL_GFX_PREF(Live, "apz.pinch_lock.span_breakout_threshold", APZPinchLockSpanBreakoutThreshold, float, 1.0f / 32.0f);
   DECL_GFX_PREF(Live, "apz.pinch_lock.span_lock_threshold",    APZPinchLockSpanLockThreshold, float, 1.0f / 32.0f);
+  DECL_GFX_PREF(Once, "apz.pinch_lock.buffer_max_age",         APZPinchLockBufferMaxAge, int32_t, 50);
   DECL_GFX_PREF(Live, "apz.popups.enabled",                    APZPopupsEnabled, bool, false);
   DECL_GFX_PREF(Live, "apz.printtree",                         APZPrintTree, bool, false);
   DECL_GFX_PREF(Live, "apz.record_checkerboarding",            APZRecordCheckerboarding, bool, false);
   DECL_GFX_PREF(Live, "apz.second_tap_tolerance",              APZSecondTapTolerance, float, 0.5f);
   DECL_GFX_PREF(Live, "apz.test.fails_with_native_injection",  APZTestFailsWithNativeInjection, bool, false);
   DECL_GFX_PREF(Live, "apz.test.logging_enabled",              APZTestLoggingEnabled, bool, false);
   DECL_GFX_PREF(Live, "apz.touch_move_tolerance",              APZTouchMoveTolerance, float, 0.1f);
   DECL_GFX_PREF(Live, "apz.touch_start_tolerance",             APZTouchStartTolerance, float, 1.0f/4.5f);
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -753,16 +753,17 @@ pref("apz.overscroll.stop_velocity_thres
 pref("apz.overscroll.stretch_factor", "0.35");
 pref("apz.paint_skipping.enabled", true);
 // Fetch displayport updates early from the message queue
 pref("apz.peek_messages.enabled", true);
 pref("apz.pinch_lock.mode", 1);
 pref("apz.pinch_lock.scoll_lock_threshold", "0.03125");  // 1/32 inches
 pref("apz.pinch_lock.span_breakout_threshold", "0.03125");  // 1/32 inches
 pref("apz.pinch_lock.span_lock_threshold", "0.03125");  // 1/32 inches
+pref("apz.pinch_lock.buffer_max_age", "50"); // milliseconds
 pref("apz.popups.enabled", false);
 pref("apz.relative-update.enabled", true);
 
 // Whether to print the APZC tree for debugging
 pref("apz.printtree", false);
 
 #ifdef NIGHTLY_BUILD
 pref("apz.record_checkerboarding", true);