Bug 1404181 - Part 9: Add code for detecting if display list building happened for a given frame, and use it to add some tests for retained display lists. r?mstange draft
authorMatt Woodrow <mwoodrow@mozilla.com>, Miko Mynttinen <mikokm@gmail.com>, Timothy Nikkel <tnikkel@gmail.com>
Wed, 27 Sep 2017 17:17:11 +1300
changeset 684520 1dcb9d77444cfde6ee47613bdd3eed627b1a2808
parent 684519 d0752e7c27fed49affd863f47293b0d26de7cb77
child 684521 f23aa7bd00ca306560e55e5cc2e773d04cc6a9d8
push id85633
push usermwoodrow@mozilla.com
push dateSun, 22 Oct 2017 23:03:02 +0000
reviewersmstange
bugs1404181
milestone58.0a1
Bug 1404181 - Part 9: Add code for detecting if display list building happened for a given frame, and use it to add some tests for retained display lists. r?mstange MozReview-Commit-ID: AIb0AWU7iiS
dom/base/nsDOMWindowUtils.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
layout/generic/nsFrame.cpp
layout/generic/nsIFrame.h
layout/reftests/display-list/reftest.list
layout/reftests/display-list/retained-dl-async-scrolled-1-ref.html
layout/reftests/display-list/retained-dl-async-scrolled-1.html
layout/reftests/display-list/retained-dl-displayport-1-ref.html
layout/reftests/display-list/retained-dl-displayport-1.html
layout/reftests/display-list/retained-dl-frame-created-1.html
layout/reftests/display-list/retained-dl-frame-deleted-1.html
layout/reftests/display-list/retained-dl-prerender-transform-1-ref.html
layout/reftests/display-list/retained-dl-prerender-transform-1.html
layout/reftests/display-list/retained-dl-remove-for-ancestor-change-1-ref.html
layout/reftests/display-list/retained-dl-remove-for-ancestor-change-1.html
layout/reftests/display-list/retained-dl-scroll-out-of-view-1-ref.html
layout/reftests/display-list/retained-dl-scroll-out-of-view-1.html
layout/reftests/display-list/retained-dl-style-change-1-ref.html
layout/reftests/display-list/retained-dl-style-change-1.html
layout/reftests/display-list/retained-dl-style-change-stacking-context-1-ref.html
layout/reftests/display-list/retained-dl-style-change-stacking-context-1.html
layout/reftests/reftest.list
layout/tools/reftest/globals.jsm
layout/tools/reftest/manifest.jsm
layout/tools/reftest/reftest-content.js
layout/tools/reftest/reftest.jsm
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -3157,16 +3157,50 @@ nsDOMWindowUtils::CheckAndClearPaintedSt
     }
   }
 
   *aResult = frame->CheckAndClearPaintedState();
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDOMWindowUtils::CheckAndClearDisplayListState(nsIDOMElement* aElement, bool* aResult)
+{
+  if (!aElement) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsresult rv;
+  nsCOMPtr<nsIContent> content = do_QueryInterface(aElement, &rv);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsIFrame* frame = content->GetPrimaryFrame();
+
+  if (!frame) {
+    *aResult = false;
+    return NS_OK;
+  }
+
+  // Get the outermost frame for the content node, so that we can test
+  // canvasframe invalidations by observing the documentElement.
+  for (;;) {
+    nsIFrame* parentFrame = frame->GetParent();
+    if (parentFrame && parentFrame->GetContent() == content) {
+      frame = parentFrame;
+    } else {
+      break;
+    }
+  }
+
+  *aResult = frame->CheckAndClearDisplayListState();
+  return NS_OK;
+
+}
+
+NS_IMETHODIMP
 nsDOMWindowUtils::IsPartOfOpaqueLayer(nsIDOMElement* aElement, bool* aResult)
 {
   if (!aElement) {
     return NS_ERROR_INVALID_ARG;
   }
 
   nsresult rv;
   nsCOMPtr<nsIContent> content = do_QueryInterface(aElement, &rv);
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -1602,16 +1602,22 @@ interface nsIDOMWindowUtils : nsISupport
 
   /**
    * Check if any PaintedLayer painting has been done for this element,
    * clears the painted flags if they have.
    */
   boolean checkAndClearPaintedState(in nsIDOMElement aElement);
 
   /**
+   * Check if any display list building has been done for this element,
+   * clears the display list flags if they have.
+   */
+  boolean checkAndClearDisplayListState(in nsIDOMElement aElement);
+
+  /**
    * Check whether all display items of the primary frame of aElement have been
    * assigned to the same single PaintedLayer in the last paint. If that is the
    * case, returns whether that PaintedLayer is opaque; if it's not the case, an
    * exception is thrown.
    */
   boolean isPartOfOpaqueLayer(in nsIDOMElement aElement);
 
   /**
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -384,16 +384,35 @@ nsIFrame::CheckAndClearPaintedState()
         result = true;
       }
     }
   }
   return result;
 }
 
 bool
+nsIFrame::CheckAndClearDisplayListState()
+{
+  bool result = BuiltDisplayList();
+  SetBuiltDisplayList(false);
+
+  nsIFrame::ChildListIterator lists(this);
+  for (; !lists.IsDone(); lists.Next()) {
+    nsFrameList::Enumerator childFrames(lists.CurrentList());
+    for (; !childFrames.AtEnd(); childFrames.Next()) {
+      nsIFrame* child = childFrames.get();
+      if (child->CheckAndClearDisplayListState()) {
+        result = true;
+      }
+    }
+  }
+  return result;
+}
+
+bool
 nsIFrame::IsVisibleConsideringAncestors(uint32_t aFlags) const
 {
   if (!StyleVisibility()->IsVisible()) {
     return false;
   }
 
   const nsIFrame* frame = this;
   while (frame) {
@@ -3182,16 +3201,18 @@ nsIFrame::BuildDisplayListForChild(nsDis
   // Since we're now sure that we're adding this frame to the display list
   // (which means we're painting it, modulo occlusion), mark it as visible
   // within the displayport.
   if (aBuilder->IsPaintingToWindow() && child->TrackingVisibility()) {
     child->PresContext()->PresShell()->EnsureFrameInApproximatelyVisibleList(child);
     awayFromCommonPath = true;
   }
 
+  child->SetBuiltDisplayList(true);
+
   // Child is composited if it's transformed, partially transparent, or has
   // SVG effects or a blend mode..
   EffectSet* effectSet = EffectSet::GetEffectSet(child);
   const nsStyleDisplay* disp = child->StyleDisplay();
   const nsStyleEffects* effects = child->StyleEffects();
   const nsStylePosition* pos = child->StylePosition();
   bool isVisuallyAtomic = child->IsVisuallyAtomic(effectSet, disp, effects);
   bool isPositioned = disp->IsAbsPosContainingBlock(child);
--- a/layout/generic/nsIFrame.h
+++ b/layout/generic/nsIFrame.h
@@ -620,16 +620,17 @@ public:
     , mClass(aID)
     , mMayHaveRoundedCorners(false)
     , mHasImageRequest(false)
     , mHasFirstLetterChild(false)
     , mParentIsWrapperAnonBox(false)
     , mIsWrapperBoxNeedingRestyle(false)
     , mReflowRequestedForCharDataChange(false)
     , mForceDescendIntoIfVisible(false)
+    , mBuiltDisplayList(false)
     , mIsPrimaryFrame(false)
   {
     mozilla::PodZero(&mOverflow);
   }
 
   nsPresContext* PresContext() const {
     return StyleContext()->PresContext();
   }
@@ -3833,16 +3834,20 @@ public:
   void MarkAsNotAbsoluteContainingBlock();
   // Child frame types override this function to select their own child list name
   virtual mozilla::layout::FrameChildListID GetAbsoluteListID() const { return kAbsoluteList; }
 
   // Checks if we (or any of our descendents) have NS_FRAME_PAINTED_THEBES set, and
   // clears this bit if so.
   bool CheckAndClearPaintedState();
 
+  // Checks if we (or any of our descendents) have mBuiltDisplayList set, and
+  // clears this bit if so.
+  bool CheckAndClearDisplayListState();
+
   // CSS visibility just doesn't cut it because it doesn't inherit through
   // documents. Also if this frame is in a hidden card of a deck then it isn't
   // visible either and that isn't expressed using CSS visibility. Also if it
   // is in a hidden view (there are a few cases left and they are hopefully
   // going away soon).
   // If the VISIBILITY_CROSS_CHROME_CONTENT_BOUNDARY flag is passed then we
   // ignore the chrome/content boundary, otherwise we stop looking when we
   // reach it.
@@ -4086,16 +4091,19 @@ public:
   bool HasDisplayItems();
   bool HasDisplayItem(nsDisplayItem* aItem);
 
   void DestroyAnonymousContent(already_AddRefed<nsIContent> aContent);
 
   bool ForceDescendIntoIfVisible() { return mForceDescendIntoIfVisible; }
   void SetForceDescendIntoIfVisible(bool aForce) { mForceDescendIntoIfVisible = aForce; }
 
+  bool BuiltDisplayList() { return mBuiltDisplayList; }
+  void SetBuiltDisplayList(bool aBuilt) { mBuiltDisplayList = aBuilt; }
+
 protected:
 
   /**
    * Reparent this frame's view if it has one.
    */
   void ReparentFrameViewTo(nsViewManager* aViewManager,
                            nsView*        aNewParentView,
                            nsView*        aOldParentView);
@@ -4254,25 +4262,33 @@ protected:
 
   /**
    * This bit is used during BuildDisplayList to mark frames that need to
    * have display items rebuilt. We will descend into them if they are
    * currently visible, even if they don't intersect the dirty area.
    */
   bool mForceDescendIntoIfVisible : 1;
 
+  /**
+   * True if we have built display items for this frame since
+   * the last call to CheckAndClearDisplayListState, false
+   * otherwise. Used for the reftest harness to verify minimal
+   * display list building.
+   */
+  bool mBuiltDisplayList : 1;
+
 private:
   /**
    * True if this is the primary frame for mContent.
    */
   bool mIsPrimaryFrame : 1;
 
 protected:
 
-  // There is a 8-bit gap left here.
+  // There is a 7-bit gap left here.
 
   // Helpers
   /**
    * Can we stop inside this frame when we're skipping non-rendered whitespace?
    * @param  aForward [in] Are we moving forward (or backward) in content order.
    * @param  aOffset [in/out] At what offset into the frame to start looking.
    *         on output - what offset was reached (whether or not we found a place to stop).
    * @return STOP: An appropriate offset was found within this frame,
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/reftest.list
@@ -0,0 +1,9 @@
+skip-if(!retainedDisplayList) == retained-dl-style-change-1.html retained-dl-style-change-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-frame-deleted-1.html retained-dl-style-change-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-frame-created-1.html retained-dl-style-change-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-style-change-stacking-context-1.html retained-dl-style-change-stacking-context-1-ref.html
+skip-if(!retainedDisplayList||!asyncPan) == retained-dl-async-scrolled-1.html retained-dl-async-scrolled-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-remove-for-ancestor-change-1.html retained-dl-remove-for-ancestor-change-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-scroll-out-of-view-1.html retained-dl-scroll-out-of-view-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-displayport-1.html retained-dl-displayport-1-ref.html
+skip-if(!retainedDisplayList) == retained-dl-prerender-transform-1.html retained-dl-prerender-transform-1-ref.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-async-scrolled-1-ref.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+body {
+  margin: 0;
+}
+
+div {
+  left: 200px;
+  top: 200px;
+  position:absolute;
+}
+</style>
+</head>
+<body>
+<div style="width: 200px; height: 200px; background-color: blue;"></div>
+<div style="width: 100px; height: 100px; background-color: red;"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-async-scrolled-1.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html reftest-async-scroll
+      reftest-displayport-x="0" reftest-displayport-y="0"
+      reftest-displayport-w="800" reftest-displayport-h="2000"
+      reftest-async-scroll-x="0" reftest-async-scroll-y="400"
+      class="reftest-wait">
+<head>
+
+<style>
+
+body {
+  margin: 0;
+  height: 4000px;
+  overflow:hidden;
+}
+
+div {
+  left: 200px;
+  top: 200px;
+  width: 200px;
+  height: 200px;
+}
+
+.scrolled {
+  position: absolute;
+  z-index: 1;
+}
+
+.fixed {
+  position: fixed;
+  background-color: red;
+}
+
+</style>
+</head>
+<body>
+
+<div class="scrolled reftest-no-display-list" style="top: 200px; background-color: green"></div>
+<div class="scrolled" style="top: 600px;" id="scrolled"></div>
+<div class="fixed" style="top: 200px"></div>
+<div class="fixed" style="top: 200px; width: 100px; height: 100px; z-index: 2"></div>
+
+</body>
+
+<script>
+function doTest() {
+  document.getElementById("scrolled").style.backgroundColor = "blue";
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-displayport-1-ref.html
@@ -0,0 +1,11 @@
+<html>
+<head>
+</head>
+<body>
+    <div id="container" style="width: 100px; height: 100px; overflow: auto;">
+    	<div id="first" style="background-color:blue; width: 10px; height: 10px;"></div>
+    	<div id="second" style="background-color:green; width: 10px; height: 10px;"></div>
+    	<div id="spacer" style="height: 200px;"></div>
+    </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-displayport-1.html
@@ -0,0 +1,21 @@
+<html class="reftest-wait reftest-async-scroll">
+<head>
+</head>
+<body>
+    <div id="container" style="width: 100px; height: 100px; overflow: auto;"
+         reftest-displayport-x="0" reftest-displayport-y="0"
+         reftest-displayport-width="100" reftest-displayport-height="100">
+    	<div id="first" style="background-color:blue; width: 10px; height: 10px;" class="reftest-no-display-list"></div>
+    	<div id="second" style="background-color:red; width: 10px; height: 10px;"></div>
+    	<div id="spacer" style="height: 200px;"></div>
+    </div>
+</body>
+<script>
+function doTest() {
+  document.getElementById("second").style.backgroundColor = "green";
+  document.documentElement.classList.remove("reftest-wait");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-frame-created-1.html
@@ -0,0 +1,25 @@
+<html class="reftest-wait">
+<head>
+<style>
+  div {
+    width:10px;
+    height:10px;
+    background-color:green;
+    display: inline-block;
+  }
+</style>
+</head>
+<body id="body">
+  <div id="first" class="reftest-no-display-list"></div>
+</body>
+<script>
+function doTest() {
+  var div = document.createElement("div");
+  var prev = document.getElementById("first");
+  prev.parentNode.insertBefore(div, prev.nextSibling);
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-frame-deleted-1.html
@@ -0,0 +1,24 @@
+<html class="reftest-wait">
+<head>
+<style>
+  div {
+    width:10px;
+    height:10px;
+    background-color:green;
+    display: inline-block;
+  }
+</style>
+</head>
+<body id="body">
+  <div id="first" class="reftest-no-display-list"></div><div id="second" class="reftest-no-display-list"></div><div id="third"></div>
+</body>
+<script>
+function doTest() {
+  var elem = document.getElementById("third");
+  elem.parentNode.removeChild(elem);
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-prerender-transform-1-ref.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+<style>
+  * {
+    margin: 0px;
+    padding: 0px;
+  }
+  .inner {
+    width: 100px;
+    height: 100px;
+    background-color: green;
+    display: inline-block;
+  }
+  body {
+    overflow: hidden;
+  }
+</style>
+</head>
+<body id="body">
+  <div id="transformed" style="transform:translateX(700px);">
+      <div id="first" class="inner"></div>
+  </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-prerender-transform-1.html
@@ -0,0 +1,36 @@
+<html class="reftest-wait">
+<head>
+<style>
+  * {
+    margin: 0px;
+    padding: 0px;
+  }
+  .inner {
+    width: 100px;
+    height: 100px;
+    background-color: green;
+    display: inline-block;
+  }
+  #third {
+    background-color: red;
+  }
+  body {
+    overflow: hidden;
+  }
+</style>
+</head>
+<body id="body">
+  <div id="transformed" style="transform:translateX(700px); will-change:transform;">
+      <div id="first" class="reftest-no-display-list inner"></div><div id="second" class="reftest-no-display-list inner"></div><div id="third" class="reftest-display-list inner"></div>
+  </div>
+</body>
+<script>
+function doTest() {
+  var third = document.getElementById("third")
+  third.style.backgroundColor = "green";
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-remove-for-ancestor-change-1-ref.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+</head>
+<body>
+    <div id="container" style="height: 40px; overflow: hidden;">
+    	<div id="spacer" style="height: 50px;"></div>
+    	<div id="second" style="background-color:red; width: 10px; height: 10px;"></div>
+    </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-remove-for-ancestor-change-1.html
@@ -0,0 +1,18 @@
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+    <div id="container" style="height: 100px; overflow: hidden;">
+    	<div id="spacer" style="height: 50px;"></div>
+    	<div id="second" style="background-color:red; width: 10px; height: 10px;"></div>
+    </div>
+</body>
+<script>
+function doTest() {
+  document.getElementById("container").style.height = "40px";
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-scroll-out-of-view-1-ref.html
@@ -0,0 +1,8 @@
+<html>
+<head>
+</head>
+<body>
+    <div id="container" style="height: 100px; overflow: hidden;">
+    </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-scroll-out-of-view-1.html
@@ -0,0 +1,18 @@
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+    <div id="container" style="height: 100px; overflow: hidden;">
+    	<div id="second" style="background-color:red; width: 10px; height: 10px;"></div>
+    	<div id="spacer" style="height: 200px;"></div>
+    </div>
+</body>
+<script>
+function doTest() {
+  document.getElementById("container").scrollTop = 100;
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-style-change-1-ref.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<style>
+  div {
+    width:10px;
+    height:10px;
+    background-color:green;
+    display: inline-block;
+  }
+</style>
+</head>
+<body id="body">
+    <div id="first"></div><div id="second"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-style-change-1.html
@@ -0,0 +1,23 @@
+<html class="reftest-wait">
+<head>
+<style>
+  div {
+    width:10px;
+    height:10px;
+    background-color:green;
+    display: inline-block;
+  }
+</style>
+</head>
+<body id="body">
+    <div id="first" class="reftest-no-display-list"></div><div id="second" style="background-color:red"></div>
+</body>
+<script>
+function doTest() {
+  document.getElementById("second").style.backgroundColor = "green";
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-style-change-stacking-context-1-ref.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<style>
+  body {
+    margin: 0px;
+  }
+  div {
+    width:200px;
+    height:200px;
+    display: inline-block;
+    position: absolute;
+  }
+</style>
+</head>
+<body>
+  <div style="background-color:green"></div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/display-list/retained-dl-style-change-stacking-context-1.html
@@ -0,0 +1,33 @@
+<html class="reftest-wait">
+<head>
+<style>
+  body {
+    margin: 0px;
+  }
+  div {
+    width:100px;
+    height:100px;
+    display: inline-block;
+    position:absolute;
+  }
+</style>
+</head>
+<body>
+  <div id="first" style="background-color:green; width:200px; height:200px" class="reftest-no-display-list"></div>
+  <div style="transform:translateZ(1px)">
+    <div id="second" style="background-color:red"></div>
+  </div>
+  <div style="position:fixed; left:100px">
+    <div id="third" style="background-color:red"></div>
+  </div>
+</body>
+<script>
+function doTest() {
+  document.getElementById("second").style.backgroundColor = "green";
+  document.getElementById("third").style.backgroundColor = "green";
+  document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</html>
--- a/layout/reftests/reftest.list
+++ b/layout/reftests/reftest.list
@@ -438,10 +438,13 @@ include box-sizing/reftest.list
 include invalidation/reftest.list
 
 # encodings
 include ../../dom/encoding/test/reftest/reftest.list
 
 # APZ/async positioning tests
 include ../../gfx/layers/apz/test/reftest/reftest.list
 
+# Display list building
+include display-list/reftest.list
+
 # Media
 include ../../dom/media/test/reftest/reftest.list
--- a/layout/tools/reftest/globals.jsm
+++ b/layout/tools/reftest/globals.jsm
@@ -123,16 +123,18 @@ for (let [key, val] of Object.entries({
     drawWindowFlags: undefined,
 
     expectingProcessCrash: false,
     expectedCrashDumpFiles: [],
     unexpectedCrashDumpFiles: {},
     crashDumpDir: undefined,
     pendingCrashDumpDir: undefined,
     failedNoPaint: false,
+    failedNoDisplayList: false,
+    failedDisplayList: false,
     failedOpaqueLayer: false,
     failedOpaqueLayerMessages: [],
     failedAssignedLayer: false,
     failedAssignedLayerMessages: [],
 
     startAfter: undefined,
     suiteStarted: false,
 
--- a/layout/tools/reftest/manifest.jsm
+++ b/layout/tools/reftest/manifest.jsm
@@ -471,16 +471,19 @@ function BuildConditionSandbox(aURL) {
     sandbox.webrender =
       g.windowUtils.layerManagerType == "WebRender";
     sandbox.layersOMTC =
       g.windowUtils.layerManagerRemote == true;
     sandbox.advancedLayers =
       g.windowUtils.usingAdvancedLayers == true;
     sandbox.layerChecksEnabled = !sandbox.webrender;
 
+    sandbox.retainedDisplayList =
+      prefs.getBoolPref("layout.display-list.retain");
+
     // Shortcuts for widget toolkits.
     sandbox.Android = xr.OS == "Android";
     sandbox.cocoaWidget = xr.widgetToolkit == "cocoa";
     sandbox.gtkWidget = xr.widgetToolkit == "gtk2"
                         || xr.widgetToolkit == "gtk3";
     sandbox.qtWidget = xr.widgetToolkit == "qt";
     sandbox.winWidget = xr.widgetToolkit == "windows";
 
--- a/layout/tools/reftest/reftest-content.js
+++ b/layout/tools/reftest/reftest-content.js
@@ -435,16 +435,22 @@ function shouldSnapshotWholePage(content
            contentRootElement.hasAttribute('class') &&
            contentRootElement.getAttribute('class').split(/\s+/)
                              .indexOf("reftest-snapshot-all") != -1;
 }
 
 function getNoPaintElements(contentRootElement) {
     return contentRootElement.getElementsByClassName('reftest-no-paint');
 }
+function getNoDisplayListElements(contentRootElement) {
+    return contentRootElement.getElementsByClassName('reftest-no-display-list');
+}
+function getDisplayListElements(contentRootElement) {
+    return contentRootElement.getElementsByClassName('reftest-display-list');
+}
 
 function getOpaqueLayerElements(contentRootElement) {
     return contentRootElement.getElementsByClassName('reftest-opaque-layer');
 }
 
 function getAssignedLayerMap(contentRootElement) {
     var layerNameToElementsMap = {};
     var elements = contentRootElement.querySelectorAll('[reftest-assigned-layer]');
@@ -593,16 +599,24 @@ function WaitForTestEnd(contentRootEleme
             var hasReftestWait = shouldWaitForReftestWaitRemoval(contentRootElement);
             // Notify the test document that now is a good time to test some invalidation
             LogInfo("MakeProgress: dispatching MozReftestInvalidate");
             if (contentRootElement) {
                 var elements = getNoPaintElements(contentRootElement);
                 for (var i = 0; i < elements.length; ++i) {
                   windowUtils().checkAndClearPaintedState(elements[i]);
                 }
+                elements = getNoDisplayListElements(contentRootElement);
+                for (var i = 0; i < elements.length; ++i) {
+                  windowUtils().checkAndClearDisplayListState(elements[i]);
+                }
+                elements = getDisplayListElements(contentRootElement);
+                for (var i = 0; i < elements.length; ++i) {
+                  windowUtils().checkAndClearDisplayListState(elements[i]);
+                }
                 var notification = content.document.createEvent("Events");
                 notification.initEvent("MozReftestInvalidate", true, false);
                 contentRootElement.dispatchEvent(notification);
             }
 
             if (!inPrintMode && doPrintMode(contentRootElement)) {
                 LogInfo("MakeProgress: setting up print mode");
                 setupPrintMode();
@@ -692,16 +706,33 @@ function WaitForTestEnd(contentRootEleme
             }
             if (contentRootElement) {
               var elements = getNoPaintElements(contentRootElement);
               for (var i = 0; i < elements.length; ++i) {
                   if (windowUtils().checkAndClearPaintedState(elements[i])) {
                       SendFailedNoPaint();
                   }
               }
+              // We only support retained display lists in the content process
+              // right now, so don't fail reftest-no-display-list tests when
+              // we don't have e10s.
+              if (gBrowserIsRemote) {
+                elements = getNoDisplayListElements(contentRootElement);
+                for (var i = 0; i < elements.length; ++i) {
+                    if (windowUtils().checkAndClearDisplayListState(elements[i])) {
+                        SendFailedNoDisplayList();
+                    }
+                }
+                elements = getDisplayListElements(contentRootElement);
+                for (var i = 0; i < elements.length; ++i) {
+                    if (!windowUtils().checkAndClearDisplayListState(elements[i])) {
+                        SendFailedDisplayList();
+                    }
+                }
+              }
               CheckLayerAssertions(contentRootElement);
             }
             LogInfo("MakeProgress: Completed");
             state = STATE_COMPLETED;
             gFailureReason = "timed out while taking snapshot (bug in harness?)";
             RemoveListeners();
             CheckForProcessCrashExpectation();
             setTimeout(RecordResult, 0);
@@ -1154,16 +1185,26 @@ function SendFailedLoad(why)
     sendAsyncMessage("reftest:FailedLoad", { why: why });
 }
 
 function SendFailedNoPaint()
 {
     sendAsyncMessage("reftest:FailedNoPaint");
 }
 
+function SendFailedNoDisplayList()
+{
+    sendAsyncMessage("reftest:FailedNoDisplayList");
+}
+
+function SendFailedDisplayList()
+{
+    sendAsyncMessage("reftest:FailedDisplayList");
+}
+
 function SendFailedOpaqueLayer(why)
 {
     sendAsyncMessage("reftest:FailedOpaqueLayer", { why: why });
 }
 
 function SendFailedAssignedLayer(why)
 {
     sendAsyncMessage("reftest:FailedAssignedLayer", { why: why });
--- a/layout/tools/reftest/reftest.jsm
+++ b/layout/tools/reftest/reftest.jsm
@@ -1045,17 +1045,17 @@ function RecordResult(testRunTime, error
                             `(${g.urls[0].fuzzyMaxDelta}, ${g.urls[0].fuzzyMaxPixels})`);
                 fuzz_exceeded = maxDifference.value > g.urls[0].fuzzyMaxDelta ||
                                 differences > g.urls[0].fuzzyMaxPixels;
                 equal = !fuzz_exceeded &&
                         maxDifference.value >= g.urls[0].fuzzyMinDelta &&
                         differences >= g.urls[0].fuzzyMinPixels;
             }
 
-            var failedExtraCheck = g.failedNoPaint || g.failedOpaqueLayer || g.failedAssignedLayer;
+            var failedExtraCheck = g.failedNoPaint || g.failedNoDisplayList || g.failedDisplayList || g.failedOpaqueLayer || g.failedAssignedLayer;
 
             // whether the comparison result matches what is in the manifest
             var test_passed = (equal == (g.urls[0].type == TYPE_REFTEST_EQUAL)) && !failedExtraCheck;
 
             if (expected != EXPECTED_FUZZY) {
                 output = outputs[expected][test_passed];
             } else if (test_passed) {
                 output = {s: ["PASS", "PASS"], n: "Pass"};
@@ -1082,16 +1082,22 @@ function RecordResult(testRunTime, error
 
             // It's possible that we failed both an "extra check" and the normal comparison, but we don't
             // have a way to annotate these separately, so just print an error for the extra check failures.
             if (failedExtraCheck) {
                 var failures = [];
                 if (g.failedNoPaint) {
                     failures.push("failed reftest-no-paint");
                 }
+                if (g.failedNoDisplayList) {
+                    failures.push("failed reftest-no-display-list");
+                }
+                if (g.failedDisplayList) {
+                    failures.push("failed reftest-display-list");
+                }
                 // The g.failed*Messages arrays will contain messages from both the test and the reference.
                 if (g.failedOpaqueLayer) {
                     failures.push("failed reftest-opaque-layer: " + g.failedOpaqueLayerMessages.join(", "));
                 }
                 if (g.failedAssignedLayer) {
                     failures.push("failed reftest-assigned-layer: " + g.failedAssignedLayerMessages.join(", "));
                 }
                 var failureString = failures.join(", ");
@@ -1242,16 +1248,18 @@ function FinishTestItem()
 
     // Replace document with BLANK_URL_FOR_CLEARING in case there are
     // assertions when unloading.
     logger.debug("Loading a blank page");
     // After clearing, content will notify us of the assertion count
     // and tests will continue.
     SendClear();
     g.failedNoPaint = false;
+    g.failedNoDisplayList = false;
+    g.failedDisplayList = false;
     g.failedOpaqueLayer = false;
     g.failedOpaqueLayerMessages = [];
     g.failedAssignedLayer = false;
     g.failedAssignedLayerMessages = [];
 }
 
 function DoAssertionCheck(numAsserts)
 {
@@ -1327,16 +1335,24 @@ function RegisterMessageListenersAndLoad
         "reftest:FailedLoad",
         function (m) { RecvFailedLoad(m.json.why); }
     );
     g.browserMessageManager.addMessageListener(
         "reftest:FailedNoPaint",
         function (m) { RecvFailedNoPaint(); }
     );
     g.browserMessageManager.addMessageListener(
+        "reftest:FailedNoDisplayList",
+        function (m) { RecvFailedNoDisplayList(); }
+    );
+    g.browserMessageManager.addMessageListener(
+        "reftest:FailedDisplayList",
+        function (m) { RecvFailedDisplayList(); }
+    );
+    g.browserMessageManager.addMessageListener(
         "reftest:FailedOpaqueLayer",
         function (m) { RecvFailedOpaqueLayer(m.json.why); }
     );
     g.browserMessageManager.addMessageListener(
         "reftest:FailedAssignedLayer",
         function (m) { RecvFailedAssignedLayer(m.json.why); }
     );
     g.browserMessageManager.addMessageListener(
@@ -1398,16 +1414,26 @@ function RecvFailedLoad(why)
     LoadFailed(why);
 }
 
 function RecvFailedNoPaint()
 {
     g.failedNoPaint = true;
 }
 
+function RecvFailedNoDisplayList()
+{
+    g.failedNoDisplayList = true;
+}
+
+function RecvFailedDisplayList()
+{
+    g.failedDisplayList = true;
+}
+
 function RecvFailedOpaqueLayer(why) {
     g.failedOpaqueLayer = true;
     g.failedOpaqueLayerMessages.push(why);
 }
 
 function RecvFailedAssignedLayer(why) {
     g.failedAssignedLayer = true;
     g.failedAssignedLayerMessages.push(why);