Bug 1292781 - Add a test. r=tnikkel, a=lizzard
authorKartikaya Gupta <kgupta@mozilla.com>
Wed, 24 Aug 2016 09:15:42 -0400
changeset 349942 c0cb39e5174697d693a1e3e006ff429ef7b6bca8
parent 349941 23cb9f2e57ff1f6f0f947b8657f874ade8baed84
child 349943 95e4986d9803655d61e2975b9f06fdaf1937c79f
push id1230
push userjlund@mozilla.com
push dateMon, 31 Oct 2016 18:13:35 +0000
treeherdermozilla-release@5e06e3766db2 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel, lizzard
bugs1292781
milestone50.0a2
Bug 1292781 - Add a test. r=tnikkel, a=lizzard MozReview-Commit-ID: f0UvsKMwAL
dom/base/nsDOMWindowUtils.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
gfx/layers/apz/src/APZCTreeManager.cpp
gfx/layers/apz/test/mochitest/mochitest.ini
gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
layout/base/nsPresContext.cpp
layout/base/nsPresContext.h
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -4018,16 +4018,27 @@ nsDOMWindowUtils::SetNextPaintSyncId(int
 NS_IMETHODIMP
 nsDOMWindowUtils::RespectDisplayPortSuppression(bool aEnabled)
 {
   nsCOMPtr<nsIPresShell> shell(GetPresShell());
   APZCCallbackHelper::RespectDisplayPortSuppression(aEnabled, shell);
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::ForceReflowInterrupt()
+{
+  nsPresContext* pc = GetPresContext();
+  if (!pc) {
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+  pc->SetPendingInterruptFromTest();
+  return NS_OK;
+}
+
 NS_INTERFACE_MAP_BEGIN(nsTranslationNodeList)
   NS_INTERFACE_MAP_ENTRY(nsISupports)
   NS_INTERFACE_MAP_ENTRY(nsITranslationNodeList)
 NS_INTERFACE_MAP_END
 
 NS_IMPL_ADDREF(nsTranslationNodeList)
 NS_IMPL_RELEASE(nsTranslationNodeList)
 
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -1926,16 +1926,22 @@ interface nsIDOMWindowUtils : nsISupport
 
   /**
    * Enable or disable displayport suppression. This is intended to be used by
    * testing code, to provide more deterministic behaviour over the displayport
    * suppression during tests. Note that this updates a flag, so whatever value
    * was last provided is what will be used.
    */
   void respectDisplayPortSuppression(in boolean aEnabled);
+
+  /**
+   * Set a flag that forces the next reflow interrupt check to return true. This
+   * can be used by tests to force execution of the interrupted reflow codepaths.
+   */
+  void forceReflowInterrupt();
 };
 
 [scriptable, uuid(c694e359-7227-4392-a138-33c0cc1f15a6)]
 interface nsITranslationNodeList : nsISupports {
   readonly attribute unsigned long length;
   nsIDOMNode item(in unsigned long index);
 
   // A translation root is a block element, or an inline element
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -494,16 +494,19 @@ APZCTreeManager::PrepareNodeForLayer(con
         MOZ_ASSERT(apzc->GetParent());
         aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
             "parentScrollId", apzc->GetParent()->GetGuid().mScrollId);
       }
       if (aMetrics.IsRootContent()) {
         aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(),
             "isRootContent", true);
       }
+      // Note that the async scroll offset is in ParentLayer pixels
+      aState.mPaintLogger.LogTestData(aMetrics.GetScrollId(), "asyncScrollOffset",
+          apzc->GetCurrentAsyncScrollOffset(AsyncPanZoomController::NORMAL));
     }
 
     if (newApzc) {
       auto it = mZoomConstraints.find(guid);
       if (it != mZoomConstraints.end()) {
         // We have a zoomconstraints for this guid, apply it.
         apzc->UpdateZoomConstraints(it->second);
       } else if (!apzc->HasNoParentWithSameLayersId()) {
--- a/gfx/layers/apz/test/mochitest/mochitest.ini
+++ b/gfx/layers/apz/test/mochitest/mochitest.ini
@@ -42,16 +42,17 @@ skip-if = (os == 'android') || (os == 'b
 skip-if = (os == 'android') || (os == 'b2g') # uses wheel events which are not supported on mobile
 [test_scroll_inactive_flattened_frame.html]
 skip-if = (os == 'android') || (os == 'b2g') || (buildapp == 'mulet') # wheel events not supported on mobile; see bug 1164274 for mulet
 [test_scroll_inactive_bug1190112.html]
 skip-if = (os == 'android') || (os == 'b2g') || (buildapp == 'mulet') # wheel events not supported on mobile; see bug 1164274 for mulet
 [test_scroll_subframe_scrollbar.html]
 skip-if = (os == 'android') || (os == 'b2g') || (buildapp == 'mulet') # wheel events not supported on mobile; see bug 1164274 for mulet
 [test_frame_reconstruction.html]
+[test_interrupted_reflow.html]
 [test_group_touchevents.html]
 # Windows touch injection doesn't work in automation, but this test can be run locally on a windows touch device.
 skip-if = (toolkit == 'windows')
 [test_group_wheelevents.html]
 skip-if = (toolkit == 'android') # wheel events not supported on mobile
 [test_group_mouseevents.html]
 [test_touch_listeners_impacting_wheel.html]
 skip-if = (toolkit == 'android') || (toolkit == 'cocoa') # wheel events not supported on mobile, and synthesized wheel smooth-scrolling not supported on OS X
new file mode 100644
--- /dev/null
+++ b/gfx/layers/apz/test/mochitest/test_interrupted_reflow.html
@@ -0,0 +1,704 @@
+<!DOCTYPE html>
+<html>
+ <!--
+ https://bugzilla.mozilla.org/show_bug.cgi?id=1292781
+ -->
+ <head>
+  <title>Test for bug 1292781</title>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/paint_listener.js"></script>
+  <script type="application/javascript" src="apz_test_utils.js"></script>
+  <script type="application/javascript" src="apz_test_native_event_utils.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <style>
+    .outer {
+        height: 400px;
+        width: 415px;
+        overflow: hidden;
+        position: relative;
+    }
+    .inner {
+        height: 100%;
+        outline: none;
+        overflow-x: hidden;
+        overflow-y: scroll;
+        position: relative;
+    }
+    .inner div:nth-child(even) {
+        background-color: lightblue;
+    }
+    .inner div:nth-child(odd) {
+        background-color: lightgreen;
+    }
+    .outer.contentBefore::before {
+        top: 0;
+        content: '';
+        display: block;
+        height: 2px;
+        position: absolute;
+        width: 100%;
+        z-index: 99;
+    }
+  </style>
+ </head>
+ <body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1292781">Mozilla Bug 1292781</a>
+<p id="display"></p>
+<div id="content">
+ <p>The frame reconstruction should not leave this scrollframe in a bad state</p>
+ <div class="outer">
+  <div class="inner">
+    this is the top of the scrollframe.
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    this is near the top of the scrollframe.
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    this is near the bottom of the scrollframe.
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    <div>this is a box</div>
+    this is the bottom of the scrollframe.
+  </div>
+ </div>
+</div>
+
+<pre id="test">
+<script type="text/javascript">
+
+// Returns a list of async scroll offsets that the |inner| element had, one for
+// each paint.
+function getAsyncScrollOffsets(aPaintsToIgnore) {
+  var offsets = [];
+  var compositorTestData = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData();
+  var buckets = compositorTestData.paints.slice(aPaintsToIgnore);
+  ok(buckets.length >= 3, "Expected at least three paints in the compositor test data");
+  var childIsLayerized = false;
+  for (var i = 0; i < buckets.length; ++i) {
+    var apzcTree = buildApzcTree(convertScrollFrameData(buckets[i].scrollFrames));
+    var rcd = findRcdNode(apzcTree);
+    if (rcd == null) {
+      continue;
+    }
+    if (rcd.children.length > 0) {
+      // The child may not be layerized in the first few paints, but once it is
+      // layerized, it should stay layerized.
+      childIsLayerized = true;
+    }
+    if (!childIsLayerized) {
+      continue;
+    }
+
+    ok(rcd.children.length == 1, "Root content APZC has exactly one child");
+    var scroll = rcd.children[0].asyncScrollOffset;
+    var pieces = scroll.replace(/[()\s]+/g, '').split(',');
+    is(pieces.length, 2, "expected string of form (x,y)");
+    offsets.push({ x: parseInt(pieces[0]),
+                   y: parseInt(pieces[1]) });
+  }
+  return offsets;
+}
+
+function* test(testDriver) {
+  var utils = SpecialPowers.DOMWindowUtils;
+
+  // The APZ test data accumulates whenever a test turns it on. We just want
+  // the data for this test, so we check how many frames are already recorded
+  // and discard those later.
+  var framesToSkip = SpecialPowers.getDOMWindowUtils(window).getCompositorAPZTestData().paints.length;
+
+  var elm = document.getElementsByClassName('inner')[0];
+  // Set a zero-margin displayport to ensure that the element is async-scrollable
+  // otherwise on Fennec it is not
+  utils.setDisplayPortMarginsForElement(0, 0, 0, 0, elm, 0);
+
+  var maxScroll = elm.scrollTopMax;
+  elm.scrollTop = maxScroll;
+  yield waitForAllPaints(function() {
+    flushApzRepaints(testDriver);
+  });
+
+  utils.forceReflowInterrupt();
+  // Trigger the frame reconstruction. The reflow that results will be
+  // interrupted, and we should end up with a transient 0,0 scroll offset.
+  elm.parentNode.classList.add('contentBefore');
+  yield waitForAllPaints(function() {
+    flushApzRepaints(testDriver);
+  });
+
+  is(elm.scrollTop, maxScroll, "Main-thread scroll position was restored");
+
+  // Ensure the interrupted reflow finished and got pushed to the APZ
+  yield waitForApzFlushedRepaints(testDriver);
+
+  // Now we pull the compositor data and check it. What we expect to see is that
+  // the scroll position goes to maxScroll, then drops to 0 and then goes back
+  // to maxScroll. This test is specifically testing that last bit - that it
+  // properly gets restored from 0 to maxScroll.
+  // The one hitch is that on Android this page is loaded with some amount of
+  // zoom, and the async scroll is in ParentLayerPixel coordinates, so it will
+  // not match maxScroll exactly. Since we can't reliably compute what that
+  // ParentLayer scroll will be, we just make sure the async scroll is nonzero
+  // and use the first value we encounter to verify that it got restored properly.
+  // The other alternative is to spawn this test into a new window with 1.0 zoom
+  // but I'm tired of doing that for pretty much every test.
+  var state = 0;
+  var asyncScrollOffsets = getAsyncScrollOffsets(framesToSkip);
+  dump("Got scroll offsets: " + JSON.stringify(asyncScrollOffsets) + "\n");
+  var maxScrollParentLayerPixels = maxScroll;
+  while (asyncScrollOffsets.length > 0) {
+    let offset = asyncScrollOffsets.shift();
+    switch (state) {
+      // 0 is the initial state, the scroll offset might be zero but should
+      // become non-zero from when we set scrollTop to scrollTopMax
+      case 0:
+        if (offset.y == 0) {
+          break;
+        }
+        if (getPlatform() == "android") {
+          ok(offset.y > 0, "Async scroll y of scrollframe is " + offset.y);
+          maxScrollParentLayerPixels = offset.y;
+        } else {
+          is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe is " + offset.y);
+        }
+        state = 1;
+        break;
+
+      // state 1 starts out at maxScrollParentLayerPixels, should drop to 0
+      // because of the interrupted reflow putting the scroll into a transient
+      // zero state
+      case 1:
+        if (offset.y == maxScrollParentLayerPixels) {
+          break;
+        }
+        is(offset.y, 0, "Async scroll position was temporarily 0");
+        state = 2;
+        break;
+
+      // state 2 starts out the transient 0 scroll offset, and we expect the
+      // scroll position to get restored back to maxScrollParentLayerPixels
+      case 2:
+        if (offset.y == 0) {
+          break;
+        }
+        is(offset.y, maxScrollParentLayerPixels, "Async scroll y of scrollframe restored to " + offset.y);
+        state = 3;
+        break;
+
+      // Terminal state. The scroll position should stay at maxScrollParentLayerPixels
+      case 3:
+        is(offset.y, maxScrollParentLayerPixels, "Scroll position maintained");
+        break;
+    }
+  }
+  is(state, 3, "The scroll position did drop to 0 and then get restored properly");
+}
+
+if (isApzEnabled()) {
+  SimpleTest.waitForExplicitFinish();
+
+  pushPrefs([["apz.test.logging_enabled", true],
+             ["apz.displayport_expiry_ms", 0]])
+  .then(waitUntilApzStable)
+  .then(runContinuation(test))
+  .then(SimpleTest.finish);
+}
+
+</script>
+</body>
+</html>
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -2682,16 +2682,22 @@ nsPresContext::CheckForInterrupt(nsIFram
   mInterruptChecksToSkip = sInterruptChecksToSkip;
 
   // Don't interrupt if it's been less than sInterruptTimeout since we started
   // the reflow.
   mHasPendingInterrupt =
     TimeStamp::Now() - mReflowStartTime > sInterruptTimeout &&
     HavePendingInputEvent() &&
     !IsChrome();
+
+  if (mPendingInterruptFromTest) {
+    mPendingInterruptFromTest = false;
+    mHasPendingInterrupt = true;
+  }
+
   if (mHasPendingInterrupt) {
 #ifdef NOISY_INTERRUPTIBLE_REFLOW
     printf("*** DETECTED pending interrupt (time=%lld)\n", PR_Now());
 #endif /* NOISY_INTERRUPTIBLE_REFLOW */
     mShell->FrameNeedsToContinueReflow(aFrame);
   }
   return mHasPendingInterrupt;
 }
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -1019,16 +1019,22 @@ public:
    * nsIPresShell::FrameNeedsToContinueReflow.
    */
   bool CheckForInterrupt(nsIFrame* aFrame);
   /**
    * Returns true if CheckForInterrupt has returned true since the last
    * ReflowStarted call. Cannot itself trigger an interrupt check.
    */
   bool HasPendingInterrupt() { return mHasPendingInterrupt; }
+  /**
+   * Sets a flag that will trip a reflow interrupt. This only bypasses the
+   * interrupt timeout and the pending event check; other checks such as whether
+   * interrupts are enabled and the interrupt check skipping still take effect.
+   */
+  void SetPendingInterruptFromTest() { mPendingInterruptFromTest = true; }
 
   /**
    * If we have a presshell, and if the given content's current
    * document is the same as our presshell's document, return the
    * content's primary frame.  Otherwise, return null.  Only use this
    * if you care about which presshell the primary frame is in.
    */
   nsIFrame* GetPrimaryFrameFor(nsIContent* aContent);
@@ -1326,16 +1332,17 @@ protected:
   uint64_t              mFramesReflowed;
 
   mozilla::TimeStamp    mReflowStartTime;
 
   // last time we did a full style flush
   mozilla::TimeStamp    mLastStyleUpdateForAllAnimations;
 
   unsigned              mHasPendingInterrupt : 1;
+  unsigned              mPendingInterruptFromTest : 1;
   unsigned              mInterruptsEnabled : 1;
   unsigned              mUseDocumentFonts : 1;
   unsigned              mUseDocumentColors : 1;
   unsigned              mUnderlineLinks : 1;
   unsigned              mSendAfterPaintToContent : 1;
   unsigned              mUseFocusColors : 1;
   unsigned              mFocusRingOnAnything : 1;
   unsigned              mFocusRingStyle : 1;