Bug 1437036 - Stop the reftest harness from waiting for MozAfterPaint during infinite/superlong animations. r?hiro draft
authorKartikaya Gupta <kgupta@mozilla.com>
Mon, 14 May 2018 08:53:52 -0400
changeset 794711 8d1e8ef50c92d3470274b86b7656c6f60d1ab45d
parent 794690 45ec8fd380dd2c308e79dbb396ca87f2ce9b3f9c
child 794712 04eae7e78a34ceed5ff4b7716dd1eb65e732a205
push id109768
push userkgupta@mozilla.com
push dateMon, 14 May 2018 12:54:14 +0000
reviewershiro
bugs1437036
milestone62.0a1
Bug 1437036 - Stop the reftest harness from waiting for MozAfterPaint during infinite/superlong animations. r?hiro These tests rely on an optimization within Gecko where it stops firing MozAfterPaint events if there was no visible change to the generated layers. This allows the reftest harness to exit the waiting-for-MozAfterPaint loop and proceed with the test. However, with webrender, this optimization does not exist and so the loop never exits. In order to solve this problem, this patch adds an explicit mechanism to exit the loop by means of a class attribute on the root element of the test page. MozReview-Commit-ID: 17ta5kLPDr9
layout/reftests/transform-3d/animate-backface-hidden.html
layout/reftests/transform-3d/animate-preserve3d-child.html
layout/reftests/transform-3d/animate-preserve3d-parent.html
layout/reftests/transform-3d/reftest.list
layout/tools/reftest/README.txt
layout/tools/reftest/reftest-content.js
layout/tools/reftest/reftest.jsm
--- a/layout/reftests/transform-3d/animate-backface-hidden.html
+++ b/layout/reftests/transform-3d/animate-backface-hidden.html
@@ -31,16 +31,19 @@ document.getElementById("test").addEvent
 
 function StartListener(event) {
   var test = document.getElementById("test");
   test.style.animationPlayState = 'running';
   test.addEventListener("animationiteration", IterationListener);
 }
 
 function IterationListener(event) {
-  setTimeout(RemoveReftestWait, 0);
+  window.addEventListener("MozAfterPaint", () => {
+    requestAnimationFrame(RemoveReftestWait);
+  }, { once: true });
 }
 
 function RemoveReftestWait() {
   document.documentElement.classList.remove("reftest-wait");
+  document.documentElement.classList.add("reftest-ignore-pending-paints");
 }
 
 </script>
--- a/layout/reftests/transform-3d/animate-preserve3d-child.html
+++ b/layout/reftests/transform-3d/animate-preserve3d-child.html
@@ -1,10 +1,10 @@
 <!DOCTYPE HTML>
-<html class="reftest-wait">
+<html class="reftest-wait reftest-no-flush">
 <title>Testcase, bug 1176969</title>
 <style>
 
 body { padding: 50px }
 
 #grandparent { perspective: 400px }
 
 @keyframes spin {
@@ -41,16 +41,19 @@ body { padding: 50px }
   </div>
 </div>
 
 <script>
 
 document.getElementById("parent").addEventListener("animationiteration", IterationListener);
 
 function IterationListener(event) {
-  setTimeout(RemoveReftestWait, 0);
+  window.addEventListener("MozAfterPaint", () => {
+    requestAnimationFrame(RemoveReftestWait);
+  }, { once: true });
 }
 
 function RemoveReftestWait() {
   document.documentElement.classList.remove("reftest-wait");
+  document.documentElement.classList.add("reftest-ignore-pending-paints");
 }
 
 </script>
--- a/layout/reftests/transform-3d/animate-preserve3d-parent.html
+++ b/layout/reftests/transform-3d/animate-preserve3d-parent.html
@@ -1,10 +1,10 @@
 <!DOCTYPE HTML>
-<html class="reftest-wait">
+<html class="reftest-wait reftest-no-flush">
 <title>Testcase, bug 1176969</title>
 <style>
 
 body { padding: 50px }
 
 #grandparent { perspective: 400px }
 
 @keyframes spin {
@@ -43,16 +43,19 @@ document.getElementById("parent").addEve
 
 function StartListener(event) {
   var test = document.getElementById("parent");
   test.style.animationPlayState = 'running';
   test.addEventListener("animationiteration", IterationListener);
 }
 
 function IterationListener(event) {
-  setTimeout(RemoveReftestWait, 0);
+  window.addEventListener("MozAfterPaint", () => {
+    requestAnimationFrame(RemoveReftestWait);
+  }, { once: true });
 }
 
 function RemoveReftestWait() {
   document.documentElement.classList.remove("reftest-wait");
+  document.documentElement.classList.add("reftest-ignore-pending-paints");
 }
 
 </script>
--- a/layout/reftests/transform-3d/reftest.list
+++ b/layout/reftests/transform-3d/reftest.list
@@ -66,17 +66,17 @@ fails-if(webrender) != 1157984-1.html ab
 fuzzy(3,99) == animate-cube-radians.html animate-cube-radians-ref.html # subpixel AA
 fuzzy(3,99) fuzzy-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)&&!layersGPUAccelerated,16,6) == animate-cube-radians-zoom.html animate-cube-radians-zoom-ref.html
 != animate-cube-radians-ref.html animate-cube-radians-zoom-ref.html
 fuzzy(3,99) == animate-cube-degrees.html animate-cube-degrees-ref.html # subpixel AA
 == animate-cube-degrees-zoom.html animate-cube-degrees-zoom-ref.html
 != animate-cube-degrees-ref.html animate-cube-degrees-zoom-ref.html
 fuzzy-if(gtkWidget,128,100) fuzzy-if(Android||OSX==1010||(gtkWidget&&layersGPUAccelerated),143,100) fuzzy-if(winWidget||OSX<1010,141,100) == preserves3d-nested.html preserves3d-nested-ref.html
 fuzzy-if(cocoaWidget,128,9) == animate-preserve3d-parent.html animate-preserve3d-ref.html # intermittently fuzzy on Mac
-fuzzy-if(cocoaWidget,128,9) == animate-preserve3d-child.html animate-preserve3d-ref.html # intermittently fuzzy on Mac
+fuzzy-if(cocoaWidget,128,9) skip-if(Android) == animate-preserve3d-child.html animate-preserve3d-ref.html # intermittently fuzzy on Mac, bug 1461311 for Android
 == animate-backface-hidden.html about:blank
 == 1245450-1.html green-rect.html
 fuzzy(1,2000) == opacity-preserve3d-1.html opacity-preserve3d-1-ref.html
 fuzzy(1,15000) == opacity-preserve3d-2.html opacity-preserve3d-2-ref.html
 fuzzy(1,10000) == opacity-preserve3d-3.html opacity-preserve3d-3-ref.html
 fuzzy(1,10000) == opacity-preserve3d-4.html opacity-preserve3d-4-ref.html
 == opacity-preserve3d-5.html opacity-preserve3d-5-ref.html
 == snap-perspective-1.html snap-perspective-1-ref.html
--- a/layout/tools/reftest/README.txt
+++ b/layout/tools/reftest/README.txt
@@ -660,8 +660,25 @@ and they won't cause any error messages 
 Skip Forcing A Content Process Layer-Tree Update: reftest-no-sync-layers attribute
 ==================================================================================
 
 Normally when an multi-process reftest test ends, we force the content process
 to push a layer-tree update to the compositor before taking the snapshot.
 Setting the "reftest-no-sync-layers" attribute on the root element skips this
 step, enabling testing that layer-tree updates are being correctly generated.
 However the test must manually wait for a MozAfterPaint event before ending.
+
+Avoid hanging on long/infinite animation tests: reftest-ignore-pending-paints
+=============================================================================
+
+If a test contains a long animation, and the desired behaviour is to take a
+snapshot partway through the animation, the usual procedure is to have a part
+of the animation that is visually unchanging, and when the test page reaches that
+part, it removes the reftest-wait to allow the harness to finish. However, this
+relies on an optimization inside Gecko that stops repaints if it detects that
+nothing will visually change (by detecting an empty invalidation area, for
+example). In some cases, this optimization may not trigger (e.g. with WebRender
+enabled). For such cases, the reftest-wait class attribute can be replaced by
+reftest-ignore-pending-paints on the root html element, and this will make the
+harness ignore any pending repaints (i.e. stop listening for MozAfterPaint) and
+just go ahead and finish the test.
+Note that any reftest that attempts to use this feature without animations will
+fail with an error.
--- a/layout/tools/reftest/reftest-content.js
+++ b/layout/tools/reftest/reftest-content.js
@@ -417,20 +417,36 @@ function resetDisplayportAndViewport() {
     // XXX currently the displayport configuration lives on the
     // presshell and so is "reset" on nav when we get a new presshell.
 }
 
 function shouldWaitForExplicitPaintWaiters() {
     return gExplicitPendingPaintCount > 0;
 }
 
-function shouldWaitForPendingPaints() {
+function shouldWaitForPendingPaints(contentRootElement) {
     // if gHaveCanvasSnapshot is false, we're not taking snapshots so
     // there is no need to wait for pending paints to be flushed.
-    return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending;
+    return gHaveCanvasSnapshot &&
+           !shouldIgnorePendingMozAfterPaints(contentRootElement) &&
+           windowUtils().isMozAfterPaintPending;
+}
+
+function shouldIgnorePendingMozAfterPaints(contentRootElement) {
+    // use getAttribute because className works differently in HTML and SVG
+    var ignore = contentRootElement &&
+           contentRootElement.hasAttribute('class') &&
+           contentRootElement.getAttribute('class').split(/\s+/)
+                             .includes("reftest-ignore-pending-paints");
+    // getAnimations is nightly-only, so check it exists before calling it
+    if (ignore && contentRootElement.ownerDocument.getAnimations
+               && contentRootElement.ownerDocument.getAnimations().length == 0) {
+      LogError("reftest-ignore-pending-paints should only be used on documents with animations!");
+    }
+    return ignore;
 }
 
 function shouldWaitForReftestWaitRemoval(contentRootElement) {
     // use getAttribute because className works differently in HTML and SVG
     return contentRootElement &&
            contentRootElement.hasAttribute('class') &&
            contentRootElement.getAttribute('class').split(/\s+/)
                              .includes("reftest-wait");
@@ -606,23 +622,23 @@ function WaitForTestEnd(contentRootEleme
                     ? FlushMode.IGNORE_THROTTLED_ANIMATIONS
                     : FlushMode.ALL;
           FlushRendering(flushMode);
         }
 
         switch (state) {
         case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
             LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
-            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints(contentRootElement)) {
                 gFailureReason = "timed out waiting for pending paint count to reach zero";
                 if (shouldWaitForExplicitPaintWaiters()) {
                     gFailureReason += " (waiting for MozPaintWaitFinished)";
                     LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
                 }
-                if (shouldWaitForPendingPaints()) {
+                if (shouldWaitForPendingPaints(contentRootElement)) {
                     gFailureReason += " (waiting for MozAfterPaint)";
                     LogInfo("MakeProgress: waiting for MozAfterPaint");
                 }
                 return;
             }
 
             state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
             var hasReftestWait = shouldWaitForReftestWaitRemoval(contentRootElement);
@@ -650,17 +666,17 @@ function WaitForTestEnd(contentRootEleme
                 LogInfo("MakeProgress: setting up print mode");
                 setupPrintMode();
             }
 
             if (hasReftestWait && !shouldWaitForReftestWaitRemoval(contentRootElement)) {
                 // MozReftestInvalidate handler removed reftest-wait.
                 // We expect something to have been invalidated...
                 FlushRendering(FlushMode.ALL);
-                if (!shouldWaitForPendingPaints() && !shouldWaitForExplicitPaintWaiters()) {
+                if (!shouldWaitForPendingPaints(contentRootElement) && !shouldWaitForExplicitPaintWaiters()) {
                     LogWarning("MozInvalidateEvent didn't invalidate");
                 }
             }
             // Try next state
             MakeProgress();
             return;
         }
 
@@ -715,24 +731,24 @@ function WaitForTestEnd(contentRootEleme
         case STATE_WAITING_FOR_APZ_FLUSH:
             LogInfo("MakeProgress: STATE_WAITING_FOR_APZ_FLUSH");
             // Nothing to do here; once we get the apz-repaints-flushed event
             // we will go to STATE_WAITING_TO_FINISH
             return;
 
         case STATE_WAITING_TO_FINISH:
             LogInfo("MakeProgress: STATE_WAITING_TO_FINISH");
-            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
+            if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints(contentRootElement)) {
                 gFailureReason = "timed out waiting for pending paint count to " +
                     "reach zero (after reftest-wait removed and switch to print mode)";
                 if (shouldWaitForExplicitPaintWaiters()) {
                     gFailureReason += " (waiting for MozPaintWaitFinished)";
                     LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
                 }
-                if (shouldWaitForPendingPaints()) {
+                if (shouldWaitForPendingPaints(contentRootElement)) {
                     gFailureReason += " (waiting for MozAfterPaint)";
                     LogInfo("MakeProgress: waiting for MozAfterPaint");
                 }
                 return;
             }
             if (contentRootElement) {
               var elements = getNoPaintElements(contentRootElement);
               for (var i = 0; i < elements.length; ++i) {
@@ -1075,16 +1091,25 @@ function DoAssertionCheck()
 }
 
 function LoadURI(uri)
 {
     var flags = webNavigation().LOAD_FLAGS_NONE;
     webNavigation().loadURI(uri, flags, null, null, null);
 }
 
+function LogError(str)
+{
+    if (gVerbose) {
+        sendSyncMessage("reftest:Log", { type: "error", msg: str });
+    } else {
+        sendAsyncMessage("reftest:Log", { type: "error", msg: str });
+    }
+}
+
 function LogWarning(str)
 {
     if (gVerbose) {
         sendSyncMessage("reftest:Log", { type: "warning", msg: str });
     } else {
         sendAsyncMessage("reftest:Log", { type: "warning", msg: str });
     }
 }
--- a/layout/tools/reftest/reftest.jsm
+++ b/layout/tools/reftest/reftest.jsm
@@ -1507,16 +1507,19 @@ function RecvInitCanvasWithSnapshot()
 
 function RecvLog(type, msg)
 {
     msg = "[CONTENT] " + msg;
     if (type == "info") {
         TestBuffer(msg);
     } else if (type == "warning") {
         logger.warning(msg);
+    } else if (type == "error") {
+        logger.error("REFTEST TEST-UNEXPECTED-FAIL | " + g.currentURL + " | " + msg + "\n");
+        ++g.testResults.Exception;
     } else {
         logger.error("REFTEST TEST-UNEXPECTED-FAIL | " + g.currentURL + " | unknown log type " + type + "\n");
         ++g.testResults.Exception;
     }
 }
 
 function RecvScriptResults(runtimeMs, error, results)
 {