Bug 1083395 - Extract an InputQueue class from the APZC input code. r=botond
authorKartikaya Gupta <kgupta@mozilla.com>
Fri, 24 Oct 2014 13:29:03 -0400
changeset 212293 e194c411165c0835a0fbfb68767d9a2e3032b62f
parent 212292 00fdb9ef3be69c800433e2b28c8bb601642d6cf3
child 212294 6efc9dfeae46e54dc706f2f42d36153e8c67fc0f
push id27704
push userkwierso@gmail.com
push dateSat, 25 Oct 2014 01:25:30 +0000
treeherdermozilla-central@e37231060eb4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbotond
bugs1083395
milestone36.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 1083395 - Extract an InputQueue class from the APZC input code. r=botond
gfx/layers/apz/src/AsyncPanZoomController.cpp
gfx/layers/apz/src/AsyncPanZoomController.h
gfx/layers/apz/src/InputBlockState.cpp
gfx/layers/apz/src/InputBlockState.h
gfx/layers/apz/src/InputQueue.cpp
gfx/layers/apz/src/InputQueue.h
gfx/layers/moz.build
--- a/gfx/layers/apz/src/AsyncPanZoomController.cpp
+++ b/gfx/layers/apz/src/AsyncPanZoomController.cpp
@@ -11,16 +11,17 @@
 #include "AsyncPanZoomController.h"     // for AsyncPanZoomController, etc
 #include "Axis.h"                       // for AxisX, AxisY, Axis, etc
 #include "Compositor.h"                 // for Compositor
 #include "CompositorParent.h"           // for CompositorParent
 #include "FrameMetrics.h"               // for FrameMetrics, etc
 #include "GestureEventListener.h"       // for GestureEventListener
 #include "InputData.h"                  // for MultiTouchInput, etc
 #include "InputBlockState.h"            // for InputBlockState, TouchBlockState
+#include "InputQueue.h"                 // for InputQueue
 #include "OverscrollHandoffState.h"     // for OverscrollHandoffState
 #include "TaskThrottler.h"              // for TaskThrottler
 #include "Units.h"                      // for CSSRect, CSSPoint, etc
 #include "UnitTransforms.h"             // for TransformTo
 #include "base/message_loop.h"          // for MessageLoop
 #include "base/task.h"                  // for NewRunnableMethod, etc
 #include "base/tracked.h"               // for FROM_HERE
 #include "gfxPrefs.h"                   // for gfxPrefs
@@ -880,17 +881,17 @@ AsyncPanZoomController::AsyncPanZoomCont
      mZoomConstraints(false, false, MIN_ZOOM, MAX_ZOOM),
      mLastSampleTime(GetFrameTime()),
      mLastAsyncScrollTime(GetFrameTime()),
      mLastAsyncScrollOffset(0, 0),
      mCurrentAsyncScrollOffset(0, 0),
      mAsyncScrollTimeoutTask(nullptr),
      mState(NOTHING),
      mNotificationBlockers(0),
-     mTouchBlockBalance(0),
+     mInputQueue(new InputQueue()),
      mTreeManager(aTreeManager),
      mAPZCId(sAsyncPanZoomControllerCount++),
      mSharedLock(nullptr),
      mAsyncTransformAppliedToContent(false)
 {
   if (aGestures == USE_GESTURE_DETECTOR) {
     mGestureEventListener = new GestureEventListener(this);
   }
@@ -921,29 +922,35 @@ AsyncPanZoomController::GetGeckoContentC
 
 already_AddRefed<GestureEventListener>
 AsyncPanZoomController::GetGestureEventListener() const {
   MonitorAutoLock lock(mRefPtrMonitor);
   nsRefPtr<GestureEventListener> listener = mGestureEventListener;
   return listener.forget();
 }
 
+nsRefPtr<InputQueue>
+AsyncPanZoomController::GetInputQueue() const {
+  MonitorAutoLock lock(mRefPtrMonitor);
+  MOZ_ASSERT(mInputQueue);
+  return mInputQueue;
+}
+
 void
 AsyncPanZoomController::Destroy()
 {
   AssertOnCompositorThread();
 
   CancelAnimation();
 
-  mTouchBlockQueue.Clear();
-
   { // scope the lock
     MonitorAutoLock lock(mRefPtrMonitor);
     mGeckoContentController = nullptr;
     mGestureEventListener = nullptr;
+    mInputQueue = nullptr;  // XXX this is temporary and will be removed in a future patch
   }
   mPrevSibling = nullptr;
   mLastChild = nullptr;
   mParent = nullptr;
   mTreeManager = nullptr;
 
   PCompositorParent* compositor = GetSharedFrameMetricsCompositor();
   // Only send the release message if the SharedFrameMetrics has been created.
@@ -1002,86 +1009,17 @@ AsyncPanZoomController::ArePointerEvents
   if (!consumable) {
     return false;
   }
 
   return true;
 }
 
 nsEventStatus AsyncPanZoomController::ReceiveInputEvent(const InputData& aEvent) {
-  AssertOnControllerThread();
-
-  if (aEvent.mInputType != MULTITOUCH_INPUT) {
-    HandleInputEvent(aEvent);
-    // The return value for non-touch input isn't really used, so just return
-    // ConsumeDoDefault for now. This can be changed later if needed.
-    return nsEventStatus_eConsumeDoDefault;
-  }
-
-  TouchBlockState* block = nullptr;
-  if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
-    block = StartNewTouchBlock(false);
-    APZC_LOG("%p started new touch block %p\n", this, block);
-
-    // We want to cancel animations here as soon as possible (i.e. without waiting for
-    // content responses) because a finger has gone down and we don't want to keep moving
-    // the content under the finger. However, to prevent "future" touchstart events from
-    // interfering with "past" animations (i.e. from a previous touch block that is still
-    // being processed) we only do this animation-cancellation if there are no older
-    // touch blocks still in the queue.
-    if (block == CurrentTouchBlock()) {
-      if (block->GetOverscrollHandoffChain()->HasFastMovingApzc()) {
-        // If we're already in a fast fling, then we want the touch event to stop the fling
-        // and to disallow the touch event from being used as part of a fling.
-        block->DisallowSingleTap();
-      }
-      block->GetOverscrollHandoffChain()->CancelAnimations();
-    }
-
-    if (NeedToWaitForContent()) {
-      // Content may intercept the touch events and prevent-default them. So we schedule
-      // a timeout to give content time to do that.
-      ScheduleContentResponseTimeout();
-    } else {
-      // Content won't prevent-default this, so we can just pretend like we scheduled
-      // a timeout and it expired. Note that we will still receive a ContentReceivedTouch
-      // callback for this block, and so we need to make sure we adjust the touch balance.
-      APZC_LOG("%p not waiting for content response on block %p\n", this, block);
-      mTouchBlockBalance++;
-      block->TimeoutContentResponse();
-    }
-  } else if (mTouchBlockQueue.IsEmpty()) {
-    NS_WARNING("Received a non-start touch event while no touch blocks active!");
-  } else {
-    // this touch is part of the most-recently created block
-    block = mTouchBlockQueue.LastElement().get();
-    APZC_LOG("%p received new event in block %p\n", this, block);
-  }
-
-  if (!block) {
-    return nsEventStatus_eIgnore;
-  }
-
-  nsEventStatus result = ArePointerEventsConsumable(block, aEvent.AsMultiTouchInput().mTouches.Length())
-      ? nsEventStatus_eConsumeDoDefault
-      : nsEventStatus_eIgnore;
-
-  if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
-    APZC_LOG("%p's current touch block is ready with preventdefault %d\n",
-        this, block->IsDefaultPrevented());
-    if (block->IsDefaultPrevented()) {
-      return result;
-    }
-    HandleInputEvent(aEvent);
-    return result;
-  }
-
-  // Otherwise, add it to the queue for the touch block
-  block->AddEvent(aEvent.AsMultiTouchInput());
-  return result;
+  return GetInputQueue()->ReceiveInputEvent(this, aEvent);
 }
 
 nsEventStatus AsyncPanZoomController::HandleInputEvent(const InputData& aEvent) {
   AssertOnControllerThread();
 
   nsEventStatus rv = nsEventStatus_eIgnore;
 
   switch (aEvent.mInputType) {
@@ -1529,17 +1467,17 @@ nsEventStatus AsyncPanZoomController::On
 nsEventStatus AsyncPanZoomController::OnPanBegin(const PanGestureInput& aEvent) {
   APZC_LOG("%p got a pan-begin in state %d\n", this, mState);
 
   if (mState == SMOOTH_SCROLL) {
     // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
     CancelAnimation();
   }
 
-  mPanGestureState = MakeUnique<InputBlockState>(BuildOverscrollHandoffChain());
+  mPanGestureState = MakeUnique<InputBlockState>(this);
 
   mX.StartTouch(aEvent.mPanStartPoint.x, aEvent.mTime);
   mY.StartTouch(aEvent.mPanStartPoint.y, aEvent.mTime);
 
   if (GetAxisLockMode() == FREE) {
     SetState(PANNING);
     return nsEventStatus_eConsumeNoDefault;
   }
@@ -1608,17 +1546,17 @@ nsEventStatus AsyncPanZoomController::On
 nsEventStatus AsyncPanZoomController::OnPanMomentumStart(const PanGestureInput& aEvent) {
   APZC_LOG("%p got a pan-momentumstart in state %d\n", this, mState);
 
   if (mState == SMOOTH_SCROLL) {
     // SMOOTH_SCROLL scrolls are cancelled by pan gestures.
     CancelAnimation();
   }
 
-  mPanGestureState = MakeUnique<InputBlockState>(BuildOverscrollHandoffChain());
+  mPanGestureState = MakeUnique<InputBlockState>(this);
 
   return nsEventStatus_eConsumeNoDefault;
 }
 
 nsEventStatus AsyncPanZoomController::OnPanMomentumEnd(const PanGestureInput& aEvent) {
   APZC_LOG("%p got a pan-momentumend in state %d\n", this, mState);
 
   mPanGestureState = nullptr;
@@ -1636,18 +1574,17 @@ nsEventStatus AsyncPanZoomController::On
 
 nsEventStatus AsyncPanZoomController::OnLongPress(const TapGestureInput& aEvent) {
   APZC_LOG("%p got a long-press in state %d\n", this, mState);
   nsRefPtr<GeckoContentController> controller = GetGeckoContentController();
   if (controller) {
     int32_t modifiers = WidgetModifiersToDOMModifiers(aEvent.modifiers);
     CSSPoint geckoScreenPoint;
     if (ConvertToGecko(aEvent.mPoint, &geckoScreenPoint)) {
-      StartNewTouchBlock(true);
-      ScheduleContentResponseTimeout();
+      GetInputQueue()->InjectNewTouchBlock(this);
       controller->HandleLongTap(geckoScreenPoint, modifiers, GetGuid());
       return nsEventStatus_eConsumeNoDefault;
     }
   }
   return nsEventStatus_eIgnore;
 }
 
 nsEventStatus AsyncPanZoomController::OnLongPressUp(const TapGestureInput& aEvent) {
@@ -2913,181 +2850,53 @@ void AsyncPanZoomController::ZoomToRect(
 
     // Schedule a repaint now, so the new displayport will be painted before the
     // animation finishes.
     RequestContentRepaint(endZoomToMetrics);
   }
 }
 
 void
-AsyncPanZoomController::ScheduleContentResponseTimeout() {
-  APZC_LOG("%p scheduling content response timeout\n", this);
-  PostDelayedTask(
-    NewRunnableMethod(this, &AsyncPanZoomController::ContentResponseTimeout),
-    gfxPrefs::APZContentResponseTimeout());
-}
-
-void
-AsyncPanZoomController::ContentResponseTimeout() {
-  AssertOnControllerThread();
-
-  mTouchBlockBalance++;
-  APZC_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
-  if (mTouchBlockBalance > 0) {
-    // Find the first touch block in the queue that hasn't already received
-    // the content response timeout callback, and notify it.
-    bool found = false;
-    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
-      if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
-        found = true;
-        break;
-      }
-    }
-    if (found) {
-      ProcessPendingInputBlocks();
-    } else {
-      NS_WARNING("APZC received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
-    }
-  }
-}
-
-void
 AsyncPanZoomController::ContentReceivedTouch(bool aPreventDefault) {
-  AssertOnControllerThread();
-
-  mTouchBlockBalance--;
-  APZC_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
-  if (mTouchBlockBalance < 0) {
-    // Find the first touch block in the queue that hasn't already received
-    // its response from content, and notify it.
-    bool found = false;
-    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
-      if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
-        found = true;
-        break;
-      }
-    }
-    if (found) {
-      ProcessPendingInputBlocks();
-    } else {
-      NS_WARNING("APZC received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
-    }
-  }
+  GetInputQueue()->ContentReceivedTouch(aPreventDefault);
 }
 
 void
 AsyncPanZoomController::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
-  AssertOnControllerThread();
-
-  bool found = false;
-  for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
-    if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
-      found = true;
-      break;
-    }
-  }
-  if (found) {
-    ProcessPendingInputBlocks();
-  } else {
-    NS_WARNING("APZC received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
-  }
-}
-
-void
-AsyncPanZoomController::ProcessPendingInputBlocks() {
-  AssertOnControllerThread();
-
-  while (true) {
-    TouchBlockState* curBlock = CurrentTouchBlock();
-    if (!curBlock->IsReadyForHandling()) {
-      break;
-    }
-
-    APZC_LOG("%p processing input block %p; preventDefault %d\n",
-        this, curBlock, curBlock->IsDefaultPrevented());
-    if (curBlock->IsDefaultPrevented()) {
-      curBlock->DropEvents();
-      ResetInputState();
-    } else {
-      while (curBlock->HasEvents()) {
-        HandleInputEvent(curBlock->RemoveFirstEvent());
-      }
-    }
-    MOZ_ASSERT(!curBlock->HasEvents());
-
-    if (mTouchBlockQueue.Length() == 1) {
-      // If |curBlock| is the only touch block in the queue, then it is still
-      // active and we cannot remove it yet. We only know that a touch block is
-      // over when we start the next one. This block will be removed by the code
-      // in StartNewTouchBlock, where new touch blocks are added.
-      break;
-    }
-
-    // If we get here, we know there are more touch blocks in the queue after
-    // |curBlock|, so we can remove |curBlock| and try to process the next one.
-    APZC_LOG("%p discarding depleted touch block %p\n", this, curBlock);
-    mTouchBlockQueue.RemoveElementAt(0);
-  }
+  GetInputQueue()->SetAllowedTouchBehavior(aBehaviors);
 }
 
 bool
 AsyncPanZoomController::NeedToWaitForContent() const
 {
   return (mFrameMetrics.GetMayHaveTouchListeners() || mFrameMetrics.GetMayHaveTouchCaret());
 }
 
+TouchBlockState*
+AsyncPanZoomController::CurrentTouchBlock()
+{
+  return GetInputQueue()->CurrentTouchBlock();
+}
+
 void
 AsyncPanZoomController::ResetInputState()
 {
   SetState(NOTHING);
   // Also clear the state in the gesture event listener
   nsRefPtr<GestureEventListener> listener = GetGestureEventListener();
   if (listener) {
     MultiTouchInput cancel(MultiTouchInput::MULTITOUCH_CANCEL, 0, TimeStamp::Now(), 0);
     listener->HandleInputEvent(cancel);
   }
 }
 
-TouchBlockState*
-AsyncPanZoomController::StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent)
-{
-  TouchBlockState* newBlock = new TouchBlockState(BuildOverscrollHandoffChain());
-  if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
-    newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
-  }
-
-  // We're going to start a new block, so clear out any depleted blocks at the head of the queue.
-  // See corresponding comment in ProcessPendingInputBlocks.
-  while (!mTouchBlockQueue.IsEmpty()) {
-    if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
-      APZC_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0].get());
-      mTouchBlockQueue.RemoveElementAt(0);
-    } else {
-      break;
-    }
-  }
-
-  // Add the new block to the queue.
-  mTouchBlockQueue.AppendElement(newBlock);
-  return newBlock;
-}
-
-TouchBlockState*
-AsyncPanZoomController::CurrentTouchBlock()
-{
-  AssertOnControllerThread();
-
-  MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
-  return mTouchBlockQueue[0].get();
-}
-
 bool
 AsyncPanZoomController::HasReadyTouchBlock()
 {
-  return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
+  return GetInputQueue()->HasReadyTouchBlock();
 }
 
 AsyncPanZoomController::TouchBehaviorFlags
 AsyncPanZoomController::GetAllowedTouchBehavior(ScreenIntPoint& aPoint) {
   // Here we need to perform a hit testing over the touch-action regions attached to the
   // layer associated with current apzc.
   // Currently they are in progress, for more info see bug 928833.
   return AllowedTouchBehavior::UNKNOWN;
--- a/gfx/layers/apz/src/AsyncPanZoomController.h
+++ b/gfx/layers/apz/src/AsyncPanZoomController.h
@@ -14,16 +14,17 @@
 #include "mozilla/EventForwards.h"
 #include "mozilla/Monitor.h"
 #include "mozilla/ReentrantMonitor.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/Atomics.h"
 #include "InputData.h"
 #include "Axis.h"
+#include "InputQueue.h"
 #include "TaskThrottler.h"
 #include "mozilla/gfx/Matrix.h"
 #include "nsRegion.h"
 
 #include "base/message_loop.h"
 
 namespace mozilla {
 
@@ -578,25 +579,16 @@ protected:
   /**
    * Timeout function for mozbrowserasyncscroll event. Because we throttle
    * mozbrowserasyncscroll events in some conditions, this function ensures
    * that the last mozbrowserasyncscroll event will be fired after a period of
    * time.
    */
   void FireAsyncScrollOnTimeout();
 
-private:
-  /**
-   * Given the number of touch points in an input event and touch block they
-   * belong to, check if the event can result in a panning/zooming behavior.
-   * This is primarily used to figure out when to dispatch the pointercancel
-   * event for the pointer events spec.
-   */
-  bool ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints);
-
   /**
    * Convert ScreenPoint relative to this APZC to CSSPoint relative
    * to the parent document. This excludes the transient compositor transform.
    * NOTE: This must be converted to CSSPoint relative to the child
    * document before sending over IPC.
    */
   bool ConvertToGecko(const ScreenPoint& aPoint, CSSPoint* aOut);
 
@@ -633,16 +625,17 @@ private:
      compositor thread. */
   nsRefPtr<GeckoContentController> mGeckoContentController;
   nsRefPtr<GestureEventListener> mGestureEventListener;
   mutable Monitor mRefPtrMonitor;
 
   /* Utility functions that return a addrefed pointer to the corresponding fields. */
   already_AddRefed<GeckoContentController> GetGeckoContentController() const;
   already_AddRefed<GestureEventListener> GetGestureEventListener() const;
+  nsRefPtr<InputQueue> GetInputQueue() const;
 
   // If we are sharing our frame metrics with content across processes
   bool mSharingFrameMetricsAcrossProcesses;
   /* Utility function to get the Compositor with which we share the FrameMetrics.
      This function is only callable from the compositor thread. */
   PCompositorParent* GetSharedFrameMetricsCompositor();
 
 protected:
@@ -776,107 +769,54 @@ private:
 
   /* ===================================================================
    * The functions and members in this section are used to manage
    * blocks of touch events and the state needed to deal with content
    * listeners.
    */
 public:
   /**
-   * This function is invoked by the APZCTreeManager which in turn is invoked
-   * by the widget when web content decides whether or not it wants to
-   * cancel a block of events. This automatically gets applied to the next
-   * block of events that has not yet been responded to. This function MUST
-   * be invoked exactly once for each touch block.
+   * See InputQueue::ContentReceivedTouch
    */
   void ContentReceivedTouch(bool aPreventDefault);
 
   /**
-   * Sets allowed touch behavior for current touch session.
-   * This method is invoked by the APZCTreeManager which in its turn invoked by
-   * the widget after performing touch-action values retrieving.
-   * Must be called after receiving the TOUCH_START even that started the
-   * touch session.
+   * See InputQueue::SetAllowedTouchBehavior
    */
   void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
 
   /**
    * Flush a repaint request if one is needed, without throttling it with the
    * paint throttler.
    */
   void FlushRepaintForNewInputBlock();
 
-private:
-  void ScheduleContentResponseTimeout();
-  void ContentResponseTimeout();
+  /**
+   * Given the number of touch points in an input event and touch block they
+   * belong to, check if the event can result in a panning/zooming behavior.
+   * This is primarily used to figure out when to dispatch the pointercancel
+   * event for the pointer events spec.
+   */
+  bool ArePointerEventsConsumable(TouchBlockState* aBlock, uint32_t aTouchPoints);
+
   /**
-   * Processes any pending input blocks that are ready for processing. There
-   * must be at least one input block in the queue when this function is called.
+   * Return true if there are are touch listeners registered on content
+   * scrolled by this APZC.
    */
-  void ProcessPendingInputBlocks();
-  TouchBlockState* StartNewTouchBlock(bool aCopyAllowedTouchBehaviorFromCurrent);
-  TouchBlockState* CurrentTouchBlock();
-  bool HasReadyTouchBlock();
   bool NeedToWaitForContent() const;
+
+  /**
+   * Clear internal state relating to input handling.
+   */
   void ResetInputState();
 
 private:
-  // The queue of touch blocks that have not yet been processed by this APZC.
-  // This member must only be accessed on the controller/UI thread.
-  nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
-
-  // This variable requires some explanation. Strap yourself in.
-  //
-  // For each block of events, we do two things: (1) send the events to gecko and expect
-  // exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
-  // that triggers in case we don't hear from web content in a timely fashion.
-  // Since events are constantly coming in, we need to be able to handle more than one
-  // block of input events sitting in the queue.
-  //
-  // There are ordering restrictions on events that we can take advantage of, and that
-  // we need to abide by. Blocks of events in the queue will always be in the order that
-  // the user generated them. Responses we get from content will be in the same order as
-  // as the blocks of events in the queue. The timeout callbacks that have been posted
-  // will also fire in the same order as the blocks of events in the queue.
-  // HOWEVER, we may get multiple responses from content interleaved with multiple
-  // timeout expirations, and that interleaving is not predictable.
-  //
-  // Therefore, we need to make sure that for each block of events, we process the queued
-  // events exactly once, either when we get the response from content, or when the
-  // timeout expires (whichever happens first). There is no way to associate the timeout
-  // or response from content with a particular block of events other than via ordering.
-  //
-  // So, what we do to accomplish this is to track a "touch block balance", which is the
-  // number of timeout expirations that have fired, minus the number of content responses
-  // that have been received. (Think "balance" as in teeter-totter balance). This
-  // value is:
-  // - zero when we are in a state where the next content response we expect to receive
-  //   and the next timeout expiration we expect to fire both correspond to the next
-  //   unprocessed block of events in the queue.
-  // - negative when we are in a state where we have received more content responses than
-  //   timeout expirations. This means that the next content repsonse we receive will
-  //   correspond to the first unprocessed block, but the next n timeout expirations need
-  //   to be ignored as they are for blocks we have already processed. (n is the absolute
-  //   value of the balance.)
-  // - positive when we are in a state where we have received more timeout expirations
-  //   than content responses. This means that the next timeout expiration that we will
-  //   receive will correspond to the first unprocessed block, but the next n content
-  //   responses need to be ignored as they are for blocks we have already processed.
-  //   (n is the absolute value of the balance.)
-  //
-  // Note that each touch block internally carries flags that indicate whether or not it
-  // has received a content response and/or timeout expiration. However, we cannot rely
-  // on that alone to deliver these notifications to the right input block, because
-  // once an input block has been processed, it can potentially be removed from the queue.
-  // Therefore the information in that block is lost. An alternative approach would
-  // be to keep around those blocks until they have received both the content response
-  // and timeout expiration, but that involves a higher level of memory usage.
-  //
-  // This member must only be accessed on the controller/UI thread.
-  int32_t mTouchBlockBalance;
+  nsRefPtr<InputQueue> mInputQueue;
+  TouchBlockState* CurrentTouchBlock();
+  bool HasReadyTouchBlock();
 
 
   /* ===================================================================
    * The functions and members in this section are used to manage
    * pan gestures.
    */
 
 private:
@@ -1025,16 +965,37 @@ public:
   void FlushRepaintForOverscrollHandoff();
 
   /**
    * If overscrolled, start a snap-back animation and return true.
    * Otherwise return false.
    */
   bool SnapBackIfOverscrolled();
 
+  /**
+   * Build the chain of APZCs along which scroll will be handed off when
+   * this APZC receives input events.
+   *
+   * Notes on lifetime and const-correctness:
+   *   - The returned handoff chain is |const|, to indicate that it cannot be
+   *     changed after being built.
+   *   - When passing the chain to a function that uses it without storing it,
+   *     pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
+   *   - When storing the chain, store it by RefPtr-to-const (as in
+   *     |nsRefPtr<const OverscrollHandoffChain>|). This ensures the chain is
+   *     kept alive. Note that queueing a task that uses the chain as an
+   *     argument constitutes storing, as the task may outlive its queuer.
+   *   - When passing the chain to a function that will store it, pass it as
+   *     |const nsRefPtr<const OverscrollHandoffChain>&|. This allows the
+   *     function to copy it into the |nsRefPtr<const OverscrollHandoffChain>|
+   *     that will store it, while avoiding an unnecessary copy (and thus
+   *     AddRef() and Release()) when passing it.
+   */
+  nsRefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
+
 private:
   /**
    * A helper function for calling APZCTreeManager::DispatchScroll().
    * Guards against the case where the APZC is being concurrently destroyed
    * (and thus mTreeManager is being nulled out).
    */
   bool CallDispatchScroll(const ScreenPoint& aStartPoint,
                           const ScreenPoint& aEndPoint,
@@ -1051,36 +1012,16 @@ private:
   /**
    * Try to overscroll by 'aOverscroll'.
    * If we are pannable, 'aOverscroll' is added to any existing overscroll,
    * and the function returns true.
    * Otherwise, nothing happens and the function return false.
    */
   bool OverscrollBy(const ScreenPoint& aOverscroll);
 
-  /**
-   * Build the chain of APZCs along which scroll will be handed off when
-   * this APZC receives input events.
-   *
-   * Notes on lifetime and const-correctness:
-   *   - The returned handoff chain is |const|, to indicate that it cannot be
-   *     changed after being built.
-   *   - When passing the chain to a function that uses it without storing it,
-   *     pass it by reference-to-const (as in |const OverscrollHandoffChain&|).
-   *   - When storing the chain, store it by RefPtr-to-const (as in
-   *     |nsRefPtr<const OverscrollHandoffChain>|). This ensures the chain is
-   *     kept alive. Note that queueing a task that uses the chain as an
-   *     argument constitutes storing, as the task may outlive its queuer.
-   *   - When passing the chain to a function that will store it, pass it as
-   *     |const nsRefPtr<const OverscrollHandoffChain>&|. This allows the
-   *     function to copy it into the |nsRefPtr<const OverscrollHandoffChain>|
-   *     that will store it, while avoiding an unnecessary copy (and thus
-   *     AddRef() and Release()) when passing it.
-   */
-  nsRefPtr<const OverscrollHandoffChain> BuildOverscrollHandoffChain();
 
   /* ===================================================================
    * The functions and members in this section are used to maintain the
    * area that this APZC instance is responsible for. This is used when
    * hit-testing to see which APZC instance should handle touch events.
    */
 public:
   void SetLayerHitTestData(const nsIntRegion& aRegion, const Matrix4x4& aTransformToLayer) {
--- a/gfx/layers/apz/src/InputBlockState.cpp
+++ b/gfx/layers/apz/src/InputBlockState.cpp
@@ -10,31 +10,38 @@
 #include "OverscrollHandoffState.h"
 
 #define TBS_LOG(...)
 // #define TBS_LOG(...) printf_stderr("TBS: " __VA_ARGS__)
 
 namespace mozilla {
 namespace layers {
 
-InputBlockState::InputBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain)
-  : mOverscrollHandoffChain(aOverscrollHandoffChain)
+InputBlockState::InputBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc)
+  : mTargetApzc(aTargetApzc)
 {
-  // We should never be constructed with a nullptr handoff chain.
-  MOZ_ASSERT(mOverscrollHandoffChain);
+  // We should never be constructed with a nullptr target.
+  MOZ_ASSERT(mTargetApzc);
+  mOverscrollHandoffChain = mTargetApzc->BuildOverscrollHandoffChain();
+}
+
+const nsRefPtr<AsyncPanZoomController>&
+InputBlockState::GetTargetApzc() const
+{
+  return mTargetApzc;
 }
 
 const nsRefPtr<const OverscrollHandoffChain>&
 InputBlockState::GetOverscrollHandoffChain() const
 {
   return mOverscrollHandoffChain;
 }
 
-TouchBlockState::TouchBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain)
-  : InputBlockState(aOverscrollHandoffChain)
+TouchBlockState::TouchBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc)
+  : InputBlockState(aTargetApzc)
   , mAllowedTouchBehaviorSet(false)
   , mPreventDefault(false)
   , mContentResponded(false)
   , mContentResponseTimerExpired(false)
   , mSingleTapDisallowed(false)
   , mSingleTapOccurred(false)
 {
   TBS_LOG("Creating %p\n", this);
--- a/gfx/layers/apz/src/InputBlockState.h
+++ b/gfx/layers/apz/src/InputBlockState.h
@@ -18,20 +18,23 @@ class OverscrollHandoffChain;
 
 /**
  * A base class that stores state common to various input blocks.
  * Currently, it just stores the overscroll handoff chain.
  */
 class InputBlockState
 {
 public:
-  explicit InputBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain);
+  explicit InputBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc);
 
+  const nsRefPtr<AsyncPanZoomController>& GetTargetApzc() const;
   const nsRefPtr<const OverscrollHandoffChain>& GetOverscrollHandoffChain() const;
+
 private:
+  nsRefPtr<AsyncPanZoomController> mTargetApzc;
   nsRefPtr<const OverscrollHandoffChain> mOverscrollHandoffChain;
 };
 
 /**
  * This class represents a single touch block. A touch block is
  * a set of touch events that can be cancelled by web content via
  * touch event listeners.
  *
@@ -61,17 +64,17 @@ private:
  * be populated with some latency. The mAllowedTouchBehaviorSet and
  * mAllowedTouchBehaviors variables track this information.
  */
 class TouchBlockState : public InputBlockState
 {
 public:
   typedef uint32_t TouchBehaviorFlags;
 
-  explicit TouchBlockState(const nsRefPtr<const OverscrollHandoffChain>& aOverscrollHandoffChain);
+  explicit TouchBlockState(const nsRefPtr<AsyncPanZoomController>& aTargetApzc);
 
   /**
    * Record whether or not content cancelled this block of events.
    * @param aPreventDefault true iff the block is cancelled.
    * @return false if this block has already received a response from
    *         web content, true if not.
    */
   bool SetContentResponse(bool aPreventDefault);
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.cpp
@@ -0,0 +1,263 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et 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 "InputQueue.h"
+
+#include "AsyncPanZoomController.h"
+#include "gfxPrefs.h"
+#include "InputBlockState.h"
+#include "OverscrollHandoffState.h"
+
+#define INPQ_LOG(...)
+// #define INPQ_LOG(...) printf_stderr("INPQ: " __VA_ARGS__)
+
+namespace mozilla {
+namespace layers {
+
+InputQueue::InputQueue()
+  : mTouchBlockBalance(0)
+{
+}
+
+InputQueue::~InputQueue() {
+  mTouchBlockQueue.Clear();
+}
+
+nsEventStatus
+InputQueue::ReceiveInputEvent(const nsRefPtr<AsyncPanZoomController>& aTarget, const InputData& aEvent) {
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  if (aEvent.mInputType != MULTITOUCH_INPUT) {
+    aTarget->HandleInputEvent(aEvent);
+    // The return value for non-touch input isn't really used, so just return
+    // ConsumeDoDefault for now. This can be changed later if needed.
+    return nsEventStatus_eConsumeDoDefault;
+  }
+
+  TouchBlockState* block = nullptr;
+  if (aEvent.AsMultiTouchInput().mType == MultiTouchInput::MULTITOUCH_START) {
+    block = StartNewTouchBlock(aTarget, false);
+    INPQ_LOG("%p started new touch block %p for target %p\n", this, block, aTarget.get());
+
+    // We want to cancel animations here as soon as possible (i.e. without waiting for
+    // content responses) because a finger has gone down and we don't want to keep moving
+    // the content under the finger. However, to prevent "future" touchstart events from
+    // interfering with "past" animations (i.e. from a previous touch block that is still
+    // being processed) we only do this animation-cancellation if there are no older
+    // touch blocks still in the queue.
+    if (block == CurrentTouchBlock()) {
+      if (block->GetOverscrollHandoffChain()->HasFastMovingApzc()) {
+        // If we're already in a fast fling, then we want the touch event to stop the fling
+        // and to disallow the touch event from being used as part of a fling.
+        block->DisallowSingleTap();
+      }
+      block->GetOverscrollHandoffChain()->CancelAnimations();
+    }
+
+    if (aTarget->NeedToWaitForContent()) {
+      // Content may intercept the touch events and prevent-default them. So we schedule
+      // a timeout to give content time to do that.
+      ScheduleContentResponseTimeout(aTarget);
+    } else {
+      // Content won't prevent-default this, so we can just pretend like we scheduled
+      // a timeout and it expired. Note that we will still receive a ContentReceivedTouch
+      // callback for this block, and so we need to make sure we adjust the touch balance.
+      INPQ_LOG("%p not waiting for content response on block %p\n", this, block);
+      mTouchBlockBalance++;
+      block->TimeoutContentResponse();
+    }
+  } else if (mTouchBlockQueue.IsEmpty()) {
+    NS_WARNING("Received a non-start touch event while no touch blocks active!");
+  } else {
+    // this touch is part of the most-recently created block
+    block = mTouchBlockQueue.LastElement().get();
+    INPQ_LOG("%p received new event in block %p\n", this, block);
+  }
+
+  if (!block) {
+    return nsEventStatus_eIgnore;
+  }
+
+  nsEventStatus result = aTarget->ArePointerEventsConsumable(block, aEvent.AsMultiTouchInput().mTouches.Length())
+      ? nsEventStatus_eConsumeDoDefault
+      : nsEventStatus_eIgnore;
+
+  if (block == CurrentTouchBlock() && block->IsReadyForHandling()) {
+    INPQ_LOG("%p's current touch block is ready with preventdefault %d\n",
+        this, block->IsDefaultPrevented());
+    if (block->IsDefaultPrevented()) {
+      return result;
+    }
+    aTarget->HandleInputEvent(aEvent);
+    return result;
+  }
+
+  // Otherwise, add it to the queue for the touch block
+  block->AddEvent(aEvent.AsMultiTouchInput());
+  return result;
+}
+
+void
+InputQueue::InjectNewTouchBlock(AsyncPanZoomController* aTarget)
+{
+  StartNewTouchBlock(aTarget, true);
+  ScheduleContentResponseTimeout(aTarget);
+}
+
+TouchBlockState*
+InputQueue::StartNewTouchBlock(const nsRefPtr<AsyncPanZoomController>& aTarget, bool aCopyAllowedTouchBehaviorFromCurrent)
+{
+  TouchBlockState* newBlock = new TouchBlockState(aTarget);
+  if (gfxPrefs::TouchActionEnabled() && aCopyAllowedTouchBehaviorFromCurrent) {
+    newBlock->CopyAllowedTouchBehaviorsFrom(*CurrentTouchBlock());
+  }
+
+  // We're going to start a new block, so clear out any depleted blocks at the head of the queue.
+  // See corresponding comment in ProcessPendingInputBlocks.
+  while (!mTouchBlockQueue.IsEmpty()) {
+    if (mTouchBlockQueue[0]->IsReadyForHandling() && !mTouchBlockQueue[0]->HasEvents()) {
+      INPQ_LOG("%p discarding depleted touch block %p\n", this, mTouchBlockQueue[0].get());
+      mTouchBlockQueue.RemoveElementAt(0);
+    } else {
+      break;
+    }
+  }
+
+  // Add the new block to the queue.
+  mTouchBlockQueue.AppendElement(newBlock);
+  return newBlock;
+}
+
+TouchBlockState*
+InputQueue::CurrentTouchBlock() const
+{
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  MOZ_ASSERT(!mTouchBlockQueue.IsEmpty());
+  return mTouchBlockQueue[0].get();
+}
+
+bool
+InputQueue::HasReadyTouchBlock() const
+{
+  return !mTouchBlockQueue.IsEmpty() && mTouchBlockQueue[0]->IsReadyForHandling();
+}
+
+void
+InputQueue::ScheduleContentResponseTimeout(const nsRefPtr<AsyncPanZoomController>& aTarget) {
+  INPQ_LOG("%p scheduling content response timeout for target %p\n", this, aTarget.get());
+  aTarget->PostDelayedTask(
+    NewRunnableMethod(this, &InputQueue::ContentResponseTimeout),
+    gfxPrefs::APZContentResponseTimeout());
+}
+
+void
+InputQueue::ContentResponseTimeout() {
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  mTouchBlockBalance++;
+  INPQ_LOG("%p got a content response timeout; balance %d\n", this, mTouchBlockBalance);
+  if (mTouchBlockBalance > 0) {
+    // Find the first touch block in the queue that hasn't already received
+    // the content response timeout callback, and notify it.
+    bool found = false;
+    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+      if (mTouchBlockQueue[i]->TimeoutContentResponse()) {
+        found = true;
+        break;
+      }
+    }
+    if (found) {
+      ProcessPendingInputBlocks();
+    } else {
+      NS_WARNING("INPQ received more ContentResponseTimeout calls than it has unprocessed touch blocks\n");
+    }
+  }
+}
+
+void
+InputQueue::ContentReceivedTouch(bool aPreventDefault) {
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  mTouchBlockBalance--;
+  INPQ_LOG("%p got a content response; balance %d\n", this, mTouchBlockBalance);
+  if (mTouchBlockBalance < 0) {
+    // Find the first touch block in the queue that hasn't already received
+    // its response from content, and notify it.
+    bool found = false;
+    for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+      if (mTouchBlockQueue[i]->SetContentResponse(aPreventDefault)) {
+        found = true;
+        break;
+      }
+    }
+    if (found) {
+      ProcessPendingInputBlocks();
+    } else {
+      NS_WARNING("INPQ received more ContentReceivedTouch calls than it has unprocessed touch blocks\n");
+    }
+  }
+}
+
+void
+InputQueue::SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors) {
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  bool found = false;
+  for (size_t i = 0; i < mTouchBlockQueue.Length(); i++) {
+    if (mTouchBlockQueue[i]->SetAllowedTouchBehaviors(aBehaviors)) {
+      found = true;
+      break;
+    }
+  }
+  if (found) {
+    ProcessPendingInputBlocks();
+  } else {
+    NS_WARNING("INPQ received more SetAllowedTouchBehavior calls than it has unprocessed touch blocks\n");
+  }
+}
+
+void
+InputQueue::ProcessPendingInputBlocks() {
+  AsyncPanZoomController::AssertOnControllerThread();
+
+  while (true) {
+    TouchBlockState* curBlock = CurrentTouchBlock();
+    if (!curBlock->IsReadyForHandling()) {
+      break;
+    }
+
+    INPQ_LOG("%p processing input block %p; preventDefault %d target %p\n",
+        this, curBlock, curBlock->IsDefaultPrevented(),
+        curBlock->GetTargetApzc().get());
+    nsRefPtr<AsyncPanZoomController> target = curBlock->GetTargetApzc();
+    if (curBlock->IsDefaultPrevented()) {
+      curBlock->DropEvents();
+      target->ResetInputState();
+    } else {
+      while (curBlock->HasEvents()) {
+        target->HandleInputEvent(curBlock->RemoveFirstEvent());
+      }
+    }
+    MOZ_ASSERT(!curBlock->HasEvents());
+
+    if (mTouchBlockQueue.Length() == 1) {
+      // If |curBlock| is the only touch block in the queue, then it is still
+      // active and we cannot remove it yet. We only know that a touch block is
+      // over when we start the next one. This block will be removed by the code
+      // in StartNewTouchBlock, where new touch blocks are added.
+      break;
+    }
+
+    // If we get here, we know there are more touch blocks in the queue after
+    // |curBlock|, so we can remove |curBlock| and try to process the next one.
+    INPQ_LOG("%p discarding depleted touch block %p\n", this, curBlock);
+    mTouchBlockQueue.RemoveElementAt(0);
+  }
+}
+
+} // namespace layers
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/src/InputQueue.h
@@ -0,0 +1,147 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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_InputQueue_h
+#define mozilla_layers_InputQueue_h
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/UniquePtr.h"
+#include "nsAutoPtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class InputData;
+
+namespace layers {
+
+class AsyncPanZoomController;
+class OverscrollHandoffChain;
+class TouchBlockState;
+
+/**
+ * This class stores incoming input events, separated into "input blocks", until
+ * they are ready for handling. Currently input blocks are only created from
+ * touch input.
+ */
+class InputQueue {
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(InputQueue)
+
+public:
+  typedef uint32_t TouchBehaviorFlags;
+
+public:
+  InputQueue();
+
+  /**
+   * Notifies the InputQueue of a new incoming input event. The APZC that the
+   * input event was targeted to should be provided in the |aTarget| parameter.
+   * See the documentation on APZCTreeManager::ReceiveInputEvent for info on
+   * return values from this function.
+   */
+  nsEventStatus ReceiveInputEvent(const nsRefPtr<AsyncPanZoomController>& aTarget, const InputData& aEvent);
+  /**
+   * This function should be invoked to notify the InputQueue when web content
+   * decides whether or not it wants to cancel a block of events. This
+   * automatically gets applied to the next block of events that has not yet
+   * been responded to. This function MUST be invoked exactly once for each
+   * touch block, after the touch-start event that creates the block is sent to
+   * ReceiveInputEvent.
+   */
+  void ContentReceivedTouch(bool aPreventDefault);
+  /**
+   * This function should be invoked to notify the InputQueue of the touch-
+   * action properties for the different touch points in an input block. This
+   * automatically gets applied to the next block of events that has not yet
+   * received a touch behaviour notification. This function MUST be invoked
+   * exactly once for each touch block, after the touch-start event that creates
+   * the block is sent to ReceiveInputEvent. If touch-action is not enabled on
+   * the platform, this function does nothing and need not be called.
+   */
+  void SetAllowedTouchBehavior(const nsTArray<TouchBehaviorFlags>& aBehaviors);
+  /**
+   * Adds a new touch block at the end of the input queue that has the same
+   * allowed touch behaviour flags as the the touch block currently being
+   * processed. This should only be called when processing of a touch block
+   * triggers the creation of a new touch block.
+   */
+  void InjectNewTouchBlock(AsyncPanZoomController* aTarget);
+  /**
+   * Returns the touch block at the head of the queue.
+   */
+  TouchBlockState* CurrentTouchBlock() const;
+  /**
+   * Returns true iff the touch block at the head of the queue is ready for
+   * handling.
+   */
+  bool HasReadyTouchBlock() const;
+
+private:
+  ~InputQueue();
+  TouchBlockState* StartNewTouchBlock(const nsRefPtr<AsyncPanZoomController>& aTarget, bool aCopyAllowedTouchBehaviorFromCurrent);
+  void ScheduleContentResponseTimeout(const nsRefPtr<AsyncPanZoomController>& aTarget);
+  void ContentResponseTimeout();
+  void ProcessPendingInputBlocks();
+
+private:
+  // The queue of touch blocks that have not yet been processed.
+  // This member must only be accessed on the controller/UI thread.
+  nsTArray<UniquePtr<TouchBlockState>> mTouchBlockQueue;
+
+  // This variable requires some explanation. Strap yourself in.
+  //
+  // For each block of events, we do two things: (1) send the events to gecko and expect
+  // exactly one call to ContentReceivedTouch in return, and (2) kick off a timeout
+  // that triggers in case we don't hear from web content in a timely fashion.
+  // Since events are constantly coming in, we need to be able to handle more than one
+  // block of input events sitting in the queue.
+  //
+  // There are ordering restrictions on events that we can take advantage of, and that
+  // we need to abide by. Blocks of events in the queue will always be in the order that
+  // the user generated them. Responses we get from content will be in the same order as
+  // as the blocks of events in the queue. The timeout callbacks that have been posted
+  // will also fire in the same order as the blocks of events in the queue.
+  // HOWEVER, we may get multiple responses from content interleaved with multiple
+  // timeout expirations, and that interleaving is not predictable.
+  //
+  // Therefore, we need to make sure that for each block of events, we process the queued
+  // events exactly once, either when we get the response from content, or when the
+  // timeout expires (whichever happens first). There is no way to associate the timeout
+  // or response from content with a particular block of events other than via ordering.
+  //
+  // So, what we do to accomplish this is to track a "touch block balance", which is the
+  // number of timeout expirations that have fired, minus the number of content responses
+  // that have been received. (Think "balance" as in teeter-totter balance). This
+  // value is:
+  // - zero when we are in a state where the next content response we expect to receive
+  //   and the next timeout expiration we expect to fire both correspond to the next
+  //   unprocessed block of events in the queue.
+  // - negative when we are in a state where we have received more content responses than
+  //   timeout expirations. This means that the next content repsonse we receive will
+  //   correspond to the first unprocessed block, but the next n timeout expirations need
+  //   to be ignored as they are for blocks we have already processed. (n is the absolute
+  //   value of the balance.)
+  // - positive when we are in a state where we have received more timeout expirations
+  //   than content responses. This means that the next timeout expiration that we will
+  //   receive will correspond to the first unprocessed block, but the next n content
+  //   responses need to be ignored as they are for blocks we have already processed.
+  //   (n is the absolute value of the balance.)
+  //
+  // Note that each touch block internally carries flags that indicate whether or not it
+  // has received a content response and/or timeout expiration. However, we cannot rely
+  // on that alone to deliver these notifications to the right input block, because
+  // once an input block has been processed, it can potentially be removed from the queue.
+  // Therefore the information in that block is lost. An alternative approach would
+  // be to keep around those blocks until they have received both the content response
+  // and timeout expiration, but that involves a higher level of memory usage.
+  //
+  // This member must only be accessed on the controller/UI thread.
+  int32_t mTouchBlockBalance;
+};
+
+}
+}
+
+#endif // mozilla_layers_InputQueue_h
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -235,16 +235,17 @@ if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk
         ]
 
 UNIFIED_SOURCES += [
     'apz/src/APZCTreeManager.cpp',
     'apz/src/AsyncPanZoomController.cpp',
     'apz/src/Axis.cpp',
     'apz/src/GestureEventListener.cpp',
     'apz/src/InputBlockState.cpp',
+    'apz/src/InputQueue.cpp',
     'apz/src/OverscrollHandoffState.cpp',
     'apz/src/TaskThrottler.cpp',
     'apz/testutil/APZTestData.cpp',
     'apz/util/ActiveElementManager.cpp',
     'apz/util/APZCCallbackHelper.cpp',
     'AxisPhysicsModel.cpp',
     'AxisPhysicsMSDModel.cpp',
     'basic/BasicCanvasLayer.cpp',