Bug 1443027 - Fix the merging algorithm to pass the new tests correctly. r?mstange draft
authorMatt Woodrow <mwoodrow@mozilla.com>
Fri, 23 Mar 2018 16:47:37 +1300
changeset 776975 8c48d804e1178f936d9016cc86144c88f59978a8
parent 776974 f3f5d7ffb5f3536da592dcdb95f5e052bbbefd04
push id105044
push usermwoodrow@mozilla.com
push dateTue, 03 Apr 2018 23:00:19 +0000
reviewersmstange
bugs1443027
milestone61.0a1
Bug 1443027 - Fix the merging algorithm to pass the new tests correctly. r?mstange MozReview-Commit-ID: JnglCbdhZzE * * * [mq]: update-test
layout/generic/nsFrame.cpp
layout/painting/RetainedDisplayListBuilder.cpp
layout/painting/RetainedDisplayListBuilder.h
layout/painting/RetainedDisplayListHelpers.h
layout/painting/moz.build
layout/painting/nsDisplayList.h
layout/reftests/position-dynamic-changes/relative/reftest.list
layout/reftests/w3c-css/submitted/ruby/reftest.list
--- a/layout/generic/nsFrame.cpp
+++ b/layout/generic/nsFrame.cpp
@@ -3086,22 +3086,21 @@ nsIFrame::BuildDisplayListForStackingCon
     // repeating display list building if it changed.
 
     // If we changed whether we're going to build a blend mode item,
     // then we need to make sure we're marked as invalid and we've built
     // the full display list.
     if (aBuilder->ContainsBlendMode() != BuiltBlendContainer() &&
         aBuilder->IsRetainingDisplayList()) {
       SetBuiltBlendContainer(aBuilder->ContainsBlendMode());
-      aBuilder->MarkCurrentFrameModifiedDuringBuilding();
 
       // If we did a partial build then delete all the items we just built
       // and repeat building with the full area.
       if (!aBuilder->GetDirtyRect().Contains(aBuilder->GetVisibleRect())) {
-        aBuilder->SetDirtyRect(aBuilder->GetVisibleRect());
+        aBuilder->MarkCurrentFrameModifiedDuringBuilding();
         set.DeleteAll(aBuilder);
 
         if (eventRegions) {
           eventRegions->Destroy(aBuilder);
           eventRegions = MakeDisplayItem<nsDisplayLayerEventRegions>(aBuilder, this);
           eventRegions->AddFrame(aBuilder, this);
           aBuilder->SetLayerEventRegions(eventRegions);
         }
--- a/layout/painting/RetainedDisplayListBuilder.cpp
+++ b/layout/painting/RetainedDisplayListBuilder.cpp
@@ -104,193 +104,85 @@ SelectAGRForFrame(nsIFrame* aFrame, Anim
 
 // Removes any display items that belonged to a frame that was deleted,
 // and mark frames that belong to a different AGR so that get their
 // items built again.
 // TODO: We currently descend into all children even if we don't have an AGR
 // to mark, as child stacking contexts might. It would be nice if we could
 // jump into those immediately rather than walking the entire thing.
 bool
-RetainedDisplayListBuilder::PreProcessDisplayList(nsDisplayList* aList,
+RetainedDisplayListBuilder::PreProcessDisplayList(RetainedDisplayList* aList,
                                                   AnimatedGeometryRoot* aAGR)
 {
-  bool modified = false;
+  // The DAG merging algorithm does not have strong mechanisms in place to keep the
+  // complexity of the resulting DAG under control. In some cases we can build up
+  // edges very quickly. Detect those cases and force a full display list build if
+  // we hit them.
+  static const uint32_t kMaxEdgeRatio = 5;
+  bool initializeDAG = !aList->mDAG.Length();
+  if (!initializeDAG &&
+      aList->mDAG.mDirectPredecessorList.Length() >
+      (aList->mDAG.mNodesInfo.Length() * kMaxEdgeRatio)) {
+    return false;
+
+  }
+
   nsDisplayList saved;
-  while (nsDisplayItem* i = aList->RemoveBottom()) {
-    if (i->HasDeletedFrame() || !i->CanBeReused()) {
-      i->Destroy(&mBuilder);
-      modified = true;
+  aList->mOldItems.SetCapacity(aList->Count());
+  size_t i = 0;
+  while (nsDisplayItem* item = aList->RemoveBottom()) {
+    if (item->HasDeletedFrame() || !item->CanBeReused()) {
+      // If we haven't yet initialized the DAG, then we can
+      // just drop this item. Otherwise we need to keep it
+      // around to preserve indices, and merging will
+      // get rid of it.
+      if (initializeDAG) {
+        item->Destroy(&mBuilder);
+      } else {
+        aList->mOldItems.AppendElement(OldItemInfo(item));
+      }
       continue;
     }
 
-    nsIFrame* f = i->Frame();
+    aList->mOldItems.AppendElement(OldItemInfo(item));
+    if (initializeDAG) {
+      if (i == 0) {
+        aList->mDAG.AddNode(Span<const MergedListIndex>());
+      } else {
+        MergedListIndex previous(i - 1);
+        aList->mDAG.AddNode(Span<const MergedListIndex>(&previous, 1));
+      }
 
-    if (i->GetChildren()) {
-      if (PreProcessDisplayList(i->GetChildren(), SelectAGRForFrame(f, aAGR))) {
-        modified = true;
+      aList->mKeyLookup.Put({ item->Frame(), item->GetPerFrameKey() }, i);
+      i++;
+    }
+
+    nsIFrame* f = item->Frame();
+
+    if (item->GetChildren()) {
+      if (!PreProcessDisplayList(item->GetChildren(), SelectAGRForFrame(f, aAGR))) {
+        mBuilder.MarkFrameForDisplayIfVisible(f, mBuilder.RootReferenceFrame());
+        mBuilder.MarkFrameModifiedDuringBuilding(f);
       }
     }
 
     // TODO: We should be able to check the clipped bounds relative
     // to the common AGR (of both the existing item and the invalidated
     // frame) and determine if they can ever intersect.
-    if (aAGR && i->GetAnimatedGeometryRoot()->GetAsyncAGR() != aAGR) {
+    if (aAGR && item->GetAnimatedGeometryRoot()->GetAsyncAGR() != aAGR) {
       mBuilder.MarkFrameForDisplayIfVisible(f, mBuilder.RootReferenceFrame());
-      modified = true;
     }
 
     // TODO: This is here because we sometimes reuse the previous display list
     // completely. For optimization, we could only restore the state for reused
     // display items.
-    i->RestoreState();
-
-    saved.AppendToTop(i);
-  }
-  aList->AppendToTop(&saved);
-  aList->RestoreState();
-  return modified;
-}
-
-struct DisplayItemKey
-{
-  bool operator ==(const DisplayItemKey& aOther) const {
-    return mFrame == aOther.mFrame &&
-           mPerFrameKey == aOther.mPerFrameKey;
-  }
-
-  nsIFrame* mFrame;
-  uint32_t mPerFrameKey;
-};
-
-class DisplayItemHashEntry : public PLDHashEntryHdr
-{
-public:
-  typedef DisplayItemKey KeyType;
-  typedef const DisplayItemKey* KeyTypePointer;
-
-  explicit DisplayItemHashEntry(KeyTypePointer aKey)
-    : mKey(*aKey) {}
-  explicit DisplayItemHashEntry(const DisplayItemHashEntry& aCopy)=default;
-
-  ~DisplayItemHashEntry() = default;
-
-  KeyType GetKey() const { return mKey; }
-  bool KeyEquals(KeyTypePointer aKey) const
-  {
-    return mKey == *aKey;
-  }
-
-  static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
-  static PLDHashNumber HashKey(KeyTypePointer aKey)
-  {
-    if (!aKey)
-      return 0;
-
-    return mozilla::HashGeneric(aKey->mFrame, aKey->mPerFrameKey);
-  }
-  enum { ALLOW_MEMMOVE = true };
-
-  DisplayItemKey mKey;
-};
-
-template<typename T>
-void SwapAndRemove(nsTArray<T>& aArray, uint32_t aIndex)
-{
-  if (aIndex != (aArray.Length() - 1)) {
-    T last = aArray.LastElement();
-    aArray.LastElement() = aArray[aIndex];
-    aArray[aIndex] = last;
+    item->RestoreState();
   }
-
-  aArray.RemoveLastElement();
-}
-
-static bool
-MergeFrameRects(nsDisplayLayerEventRegions* aOldItem,
-                nsDisplayLayerEventRegions* aNewItem,
-                nsDisplayLayerEventRegions::FrameRects nsDisplayLayerEventRegions::*aRectList,
-                nsTArray<nsIFrame*>& aAddedFrames)
-{
-  bool modified = false;
-  // Go through the old item's rect list and remove any rectangles
-  // belonging to invalidated frames (deleted frames should
-  // already be gone at this point)
-  nsDisplayLayerEventRegions::FrameRects& oldRects = aOldItem->*aRectList;
-  uint32_t i = 0;
-  while (i < oldRects.mFrames.Length()) {
-    // TODO: As mentioned in nsDisplayLayerEventRegions, this
-    // operation might perform really poorly on a vector.
-    nsIFrame* f = oldRects.mFrames[i];
-    if (IsAnyAncestorModified(f)) {
-      MOZ_ASSERT(f != aOldItem->Frame());
-      f->RemoveDisplayItem(aOldItem);
-      SwapAndRemove(oldRects.mFrames, i);
-      SwapAndRemove(oldRects.mBoxes, i);
-      modified = true;
-    } else {
-      i++;
-    }
-  }
-  if (!aNewItem) {
-    return modified;
-  }
-
-  // Copy items from the source list to the dest list, but
-  // only if the dest doesn't already include them.
-  nsDisplayItem* destItem = aOldItem;
-  nsDisplayLayerEventRegions::FrameRects* destRects = &(aOldItem->*aRectList);
-  nsDisplayLayerEventRegions::FrameRects* srcRects = &(aNewItem->*aRectList);
-
-  for (uint32_t i = 0; i < srcRects->mFrames.Length(); i++) {
-    nsIFrame* f = srcRects->mFrames[i];
-    if (!f->HasDisplayItem(destItem)) {
-      // If this frame isn't already in the destination item,
-      // then add it!
-      destRects->Add(f, srcRects->mBoxes[i]);
-
-      // We also need to update RealDisplayItemData for 'f',
-      // but that'll mess up this check for the following
-      // FrameRects lists, so defer that until the end.
-      aAddedFrames.AppendElement(f);
-      MOZ_ASSERT(f != aOldItem->Frame());
-
-      modified = true;
-    }
-
-  }
-  return modified;
-}
-
-bool MergeLayerEventRegions(nsDisplayItem* aOldItem,
-                            nsDisplayItem* aNewItem)
-{
-  nsDisplayLayerEventRegions* oldItem =
-    static_cast<nsDisplayLayerEventRegions*>(aOldItem);
-  nsDisplayLayerEventRegions* newItem =
-    static_cast<nsDisplayLayerEventRegions*>(aNewItem);
-
-  nsTArray<nsIFrame*> addedFrames;
-
-  bool modified = false;
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mHitRegion, addedFrames);
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mMaybeHitRegion, addedFrames);
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mDispatchToContentHitRegion, addedFrames);
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mNoActionRegion, addedFrames);
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mHorizontalPanRegion, addedFrames);
-  modified |= MergeFrameRects(oldItem, newItem, &nsDisplayLayerEventRegions::mVerticalPanRegion, addedFrames);
-
-  // MergeFrameRects deferred updating the display item data list during
-  // processing so that earlier calls didn't change the result of later
-  // ones. Fix that up now.
-  for (nsIFrame* f : addedFrames) {
-    if (!f->HasDisplayItem(aOldItem)) {
-      f->AddDisplayItem(aOldItem);
-    }
-  }
-  return modified;
+  aList->RestoreState();
+  return true;
 }
 
 void
 RetainedDisplayListBuilder::IncrementSubDocPresShellPaintCount(nsDisplayItem* aItem)
 {
   MOZ_ASSERT(aItem->GetType() == DisplayItemType::TYPE_SUBDOCUMENT);
 
   nsSubDocumentFrame* subDocFrame =
@@ -317,276 +209,268 @@ UpdateASR(nsDisplayItem* aItem,
     return;
   }
 
   wrapList->SetActiveScrolledRoot(
     ActiveScrolledRoot::PickAncestor(wrapList->GetFrameActiveScrolledRoot(),
                                      aContainerASR.value()));
 }
 
+void
+OldItemInfo::AddedMatchToMergedList(RetainedDisplayListBuilder* aBuilder,
+                                    MergedListIndex aIndex)
+{
+  mItem->Destroy(aBuilder->Builder());
+  AddedToMergedList(aIndex);
+}
+
+void
+OldItemInfo::Discard(RetainedDisplayListBuilder* aBuilder,
+                     nsTArray<MergedListIndex>&& aDirectPredecessors)
+{
+  MOZ_ASSERT(!IsUsed());
+  mUsed = mDiscarded = true;
+  mDirectPredecessors = Move(aDirectPredecessors);
+  mItem->Destroy(aBuilder->Builder());
+  mItem = nullptr;
+}
+
 /**
- * Takes two display lists and merges them into an output list.
- *
- * The basic algorithm is:
- *
- * For-each item i in the new list:
- *     If the item has a matching item in the old list:
- *         Remove items from the start of the old list up until we reach an item that also exists in the new list (leaving the matched item in place):
- *             Add valid items to the merged list, destroy invalid items.
- *     Add i into the merged list.
- *     If the start of the old list matches i, remove and destroy it, otherwise mark the old version of i as used.
- * Add all remaining valid items from the old list into the merged list, skipping over (and destroying) any that are marked as used.
- *
- * If any item has a child display list, then we recurse into the merge
- * algorithm once we match up the new/old versions (if present).
- *
- * Example 1:
- *
- * Old List: A,B,C,D
- * Modified List: A,D
- * Invalidations: C,D
- *
- * We first match the A items, and add the new one to the merged list.
- * We then match the D items, copy B into the merged list, but not C
- * (since it's invalid). We then add the new D to the list and we're
- * finished.
- *
- * Merged List: A,B,D
- *
- * Example 2 (layout/reftests/retained-dl-zindex-1.html):
- *
- * Old List: A, B
- * Modified List: B, A
- * Invalidations: A
- *
- * In this example A has been explicitly moved to the back.
- *
- * We match the B items, but don't copy A since it's invalid, and then add the
- * new B into the merged list. We then add A, and we're done.
- *
- * Merged List: B, A
- *
- * Example 3:
+ * A C++ implementation of Markus Stange's merge-dags algorthim.
+ * https://github.com/mstange/merge-dags
  *
- * Old List: A, B
- * Modified List: B, A
- * Invalidations: -
- *
- * This can happen because a prior merge might have changed the ordering
- * for non-intersecting items.
- *
- * We match the B items, but don't copy A since it's also present in the new list
- * and then add the new B into the merged list. We then add A, and we're done.
- *
- * Merged List: B, A
- *
- * Example 4 (layout/reftests/retained-dl-zindex-2.html):
- *
- * Element A has two elements covering it (B and C), that don't intersect each
- * other. We then move C to the back.
- *
- * The correct initial ordering has B and C after A, in any order.
- *
- * Old List: A, B, C
- * Modified List: C, A
- * Invalidations: C
- *
- * We match the C items, but don't add anything from the old list because A is present
- * in both lists. We add C to the merged list, and mark the old version of C as reused.
- *
- * We then match A, add the new version the merged list and delete the old version.
- *
- * We then process the remainder of the old list, B is added (since it is valid,
- * and hasn't been mark as reused), C is destroyed since it's marked as reused and
- * is already present in the merged list.
- *
- * Merged List: C, A, B
+ * MergeState handles combining a new list of display items into an existing
+ * DAG and computes the new DAG in a single pass.
+ * Each time we add a new item, we resolve all dependencies for it, so that the resulting
+ * list and DAG are built in topological ordering.
  */
-bool
-RetainedDisplayListBuilder::MergeDisplayLists(nsDisplayList* aNewList,
-                                              nsDisplayList* aOldList,
-                                              nsDisplayList* aOutList,
-                                              Maybe<const ActiveScrolledRoot*>& aOutContainerASR)
-{
-  bool modified = false;
+class MergeState {
+public:
+  MergeState(RetainedDisplayListBuilder* aBuilder, RetainedDisplayList&& aOldList)
+    : mBuilder(aBuilder)
+    , mOldItems(Move(aOldList.mOldItems))
+    , mOldDAG(Move(*reinterpret_cast<DirectedAcyclicGraph<OldListUnits>*>(&aOldList.mDAG)))
+    , mResultIsModified(false)
+  {
+    mOldKeyLookup.SwapElements(aOldList.mKeyLookup);
+    mMergedDAG.EnsureCapacityFor(mOldDAG);
+  }
+
+  MergedListIndex ProcessItemFromNewList(nsDisplayItem* aNewItem, const Maybe<MergedListIndex>& aPreviousItem) {
+    OldListIndex oldIndex;
+    if (mOldKeyLookup.Get({ aNewItem->Frame(), aNewItem->GetPerFrameKey() }, &oldIndex.val)) {
+      if (!IsChanged(aNewItem)) {
+        MOZ_ASSERT(!mOldItems[oldIndex.val].IsUsed());
+        if (aNewItem->GetChildren()) {
+          Maybe<const ActiveScrolledRoot*> containerASRForChildren;
+          if (mBuilder->MergeDisplayLists(aNewItem->GetChildren(),
+                                          mOldItems[oldIndex.val].mItem->GetChildren(),
+                                          aNewItem->GetChildren(),
+                                          containerASRForChildren)) {
+            mResultIsModified = true;
+
+          }
+          UpdateASR(aNewItem, containerASRForChildren);
+          aNewItem->UpdateBounds(mBuilder->Builder());
+        }
 
-  nsDisplayList merged;
-  const auto UseItem = [&](nsDisplayItem* aItem) {
+        AutoTArray<MergedListIndex, 2> directPredecessors = ProcessPredecessorsOfOldNode(oldIndex);
+        MergedListIndex newIndex = AddNewNode(aNewItem, Some(oldIndex), directPredecessors, aPreviousItem);
+        mOldItems[oldIndex.val].AddedMatchToMergedList(mBuilder, newIndex);
+        return newIndex;
+      }
+    }
+    mResultIsModified = true;
+    return AddNewNode(aNewItem, Nothing(), Span<MergedListIndex>(), aPreviousItem);
+  }
+
+  RetainedDisplayList Finalize() {
+    for (size_t i = 0; i < mOldDAG.Length(); i++) {
+      if (mOldItems[i].IsUsed()) {
+        continue;
+      }
+
+      AutoTArray<MergedListIndex, 2> directPredecessors =
+        ResolveNodeIndexesOldToMerged(mOldDAG.GetDirectPredecessors(OldListIndex(i)));
+      ProcessOldNode(OldListIndex(i), Move(directPredecessors));
+    }
+
+    RetainedDisplayList result;
+    result.AppendToTop(&mMergedItems);
+    result.mDAG = Move(mMergedDAG);
+    result.mKeyLookup.SwapElements(mMergedKeyLookup);
+    return result;
+  }
+
+  bool IsChanged(nsDisplayItem* aItem) {
+    return aItem->HasDeletedFrame() || !aItem->CanBeReused() ||
+           IsAnyAncestorModified(aItem->FrameForInvalidation());
+  }
+
+  void UpdateContainerASR(nsDisplayItem* aItem)
+  {
     const ActiveScrolledRoot* itemClipASR =
       aItem->GetClipChain() ? aItem->GetClipChain()->mASR : nullptr;
 
     const ActiveScrolledRoot* finiteBoundsASR = ActiveScrolledRoot::PickDescendant(
       itemClipASR, aItem->GetActiveScrolledRoot());
-    if (!aOutContainerASR) {
-      aOutContainerASR = Some(finiteBoundsASR);
+    if (!mContainerASR) {
+      mContainerASR = Some(finiteBoundsASR);
     } else {
-      aOutContainerASR =
-        Some(ActiveScrolledRoot::PickAncestor(aOutContainerASR.value(), finiteBoundsASR));
-    }
-
-    merged.AppendToTop(aItem);
-  };
-
-  const auto ReuseItem = [&](nsDisplayItem* aItem) {
-    UseItem(aItem);
-    aItem->SetReused(true);
-
-    if (aItem->GetType() == DisplayItemType::TYPE_SUBDOCUMENT) {
-      IncrementSubDocPresShellPaintCount(aItem);
-    }
-  };
-
-  const bool newListIsEmpty = aNewList->IsEmpty();
-  if (!newListIsEmpty) {
-    // Build a hashtable of items in the old list so we can look for them quickly.
-    // We have similar data in the nsIFrame DisplayItems() property, but it doesn't
-    // know which display list items are in, and we only want to match items in
-    // this list.
-    nsDataHashtable<DisplayItemHashEntry, nsDisplayItem*> oldListLookup(aOldList->Count());
-
-    for (nsDisplayItem* i = aOldList->GetBottom(); i != nullptr; i = i->GetAbove()) {
-      i->SetReused(false);
-      oldListLookup.Put({ i->Frame(), i->GetPerFrameKey() }, i);
-    }
-
-    nsDataHashtable<DisplayItemHashEntry, nsDisplayItem*> newListLookup(aNewList->Count());
-    for (nsDisplayItem* i = aNewList->GetBottom(); i != nullptr; i = i->GetAbove()) {
-#ifdef DEBUG
-      if (newListLookup.Get({ i->Frame(), i->GetPerFrameKey() }, nullptr)) {
-        MOZ_CRASH_UNSAFE_PRINTF("Duplicate display items detected!: %s(0x%p) type=%d key=%d",
-                                  i->Name(), i->Frame(),
-                                  static_cast<int>(i->GetType()), i->GetPerFrameKey());
-      }
-#endif
-      newListLookup.Put({ i->Frame(), i->GetPerFrameKey() }, i);
+      mContainerASR = Some(ActiveScrolledRoot::PickAncestor(mContainerASR.value(), finiteBoundsASR));
     }
 
-    while (nsDisplayItem* newItem = aNewList->RemoveBottom()) {
-      if (nsDisplayItem* oldItem = oldListLookup.Get({ newItem->Frame(), newItem->GetPerFrameKey() })) {
-        // The new item has a matching counterpart in the old list that we haven't yet reached,
-        // so copy all valid items from the old list into the merged list until we get to the
-        // matched item.
-        nsDisplayItem* old = nullptr;
-        while ((old = aOldList->GetBottom()) && old != oldItem) {
-          if (IsAnyAncestorModified(old->FrameForInvalidation())) {
-            // The old item is invalid, discard it.
-            oldListLookup.Remove({ old->Frame(), old->GetPerFrameKey() });
-            aOldList->RemoveBottom();
-            old->Destroy(&mBuilder);
-            modified = true;
-          } else if (newListLookup.Get({ old->Frame(), old->GetPerFrameKey() })) {
-            // This old item is also in the new list, but we haven't got to it yet.
-            // Stop now, and we'll deal with it when we get to the new entry.
-            modified = true;
-            break;
-          } else {
-            // Recurse into the child list (without a matching new list) to
-            // ensure that we find and remove any invalidated items.
-            if (old->GetChildren()) {
-              nsDisplayList empty;
-              Maybe<const ActiveScrolledRoot*> containerASRForChildren;
-              if (MergeDisplayLists(&empty, old->GetChildren(),
-                                    old->GetChildren(), containerASRForChildren)) {
-                modified = true;
-              }
-              UpdateASR(old, containerASRForChildren);
-              old->UpdateBounds(&mBuilder);
-            }
-            aOldList->RemoveBottom();
-            ReuseItem(old);
-          }
+  }
+
+  MergedListIndex AddNewNode(nsDisplayItem* aItem,
+                             const Maybe<OldListIndex>& aOldIndex,
+                             Span<const MergedListIndex> aDirectPredecessors,
+                             const Maybe<MergedListIndex>& aExtraDirectPredecessor) {
+    UpdateContainerASR(aItem);
+    mMergedItems.AppendToTop(aItem);
+    MergedListIndex newIndex = mMergedDAG.AddNode(aDirectPredecessors, aExtraDirectPredecessor);
+    mMergedKeyLookup.Put({ aItem->Frame(), aItem->GetPerFrameKey() }, newIndex.val);
+    return newIndex;
+  }
+
+  void ProcessOldNode(OldListIndex aNode, nsTArray<MergedListIndex>&& aDirectPredecessors) {
+    nsDisplayItem* item = mOldItems[aNode.val].mItem;
+    if (IsChanged(item)) {
+      mOldItems[aNode.val].Discard(mBuilder, Move(aDirectPredecessors));
+      mResultIsModified = true;
+    } else {
+      if (item->GetChildren()) {
+        Maybe<const ActiveScrolledRoot*> containerASRForChildren;
+        nsDisplayList empty;
+        if (mBuilder->MergeDisplayLists(&empty, item->GetChildren(), item->GetChildren(),
+                                        containerASRForChildren)) {
+          mResultIsModified = true;
         }
-        bool destroy = false;
-        if (old == oldItem) {
-          // If we advanced the old list until the matching item then we can pop
-          // the matching item off the old list and make sure we clean it up.
-          aOldList->RemoveBottom();
-          destroy = true;
-        } else {
-          // If we didn't get to the matching item, then mark the old item
-          // as being reused (since we're adding the new version to the new
-          // list now) so that we don't add it twice at the end.
-          oldItem->SetReused(true);
-        }
+        UpdateASR(item, containerASRForChildren);
+        item->UpdateBounds(mBuilder->Builder());
+      }
+      if (item->GetType() == DisplayItemType::TYPE_SUBDOCUMENT) {
+        mBuilder->IncrementSubDocPresShellPaintCount(item);
+      }
+      item->SetReused(true);
+      mOldItems[aNode.val].AddedToMergedList(
+        AddNewNode(item, Some(aNode), aDirectPredecessors, Nothing()));
+    }
+  }
+
+  struct PredecessorStackItem {
+    PredecessorStackItem(OldListIndex aNode, Span<OldListIndex> aPredecessors)
+     : mNode(aNode)
+     , mDirectPredecessors(aPredecessors)
+     , mCurrentPredecessorIndex(0)
+    {}
+
+    bool IsFinished() {
+      return mCurrentPredecessorIndex == mDirectPredecessors.Length();
+    }
 
-        // Recursively merge any child lists, destroy the old item and add
-        // the new one to the list.
-        if (destroy &&
-            oldItem->GetType() == DisplayItemType::TYPE_LAYER_EVENT_REGIONS &&
-            !IsAnyAncestorModified(oldItem->FrameForInvalidation())) {
-          // Event regions items don't have anything interesting other than
-          // the lists of regions and frames, so we have no need to use the
-          // newer item. Always use the old item instead since we assume it's
-          // likely to have the bigger lists and merging will be quicker.
-          if (MergeLayerEventRegions(oldItem, newItem)) {
-            modified = true;
-          }
-          ReuseItem(oldItem);
-          newItem->Destroy(&mBuilder);
+    OldListIndex GetAndIncrementCurrentPredecessor() { return mDirectPredecessors[mCurrentPredecessorIndex++]; }
+
+    OldListIndex mNode;
+    Span<OldListIndex> mDirectPredecessors;
+    size_t mCurrentPredecessorIndex;
+  };
+
+  AutoTArray<MergedListIndex, 2> ProcessPredecessorsOfOldNode(OldListIndex aNode) {
+    AutoTArray<PredecessorStackItem,256> mStack;
+    mStack.AppendElement(PredecessorStackItem(aNode, mOldDAG.GetDirectPredecessors(aNode)));
+
+    while (true) {
+      if (mStack.LastElement().IsFinished()) {
+        // If we've finished processing all the entries in the current set, then pop
+        // it off the processing stack and process it.
+        PredecessorStackItem item = mStack.PopLastElement();
+        AutoTArray<MergedListIndex,2> result =
+          ResolveNodeIndexesOldToMerged(item.mDirectPredecessors);
+        if (mStack.IsEmpty()) {
+          return result;
         } else {
-          if (IsAnyAncestorModified(oldItem->FrameForInvalidation())) {
-            modified = true;
-          } else if (oldItem->GetChildren()) {
-            MOZ_ASSERT(newItem->GetChildren());
-            Maybe<const ActiveScrolledRoot*> containerASRForChildren;
-            if (MergeDisplayLists(newItem->GetChildren(), oldItem->GetChildren(),
-                                  newItem->GetChildren(), containerASRForChildren)) {
-              modified = true;
-            }
-            UpdateASR(newItem, containerASRForChildren);
-            newItem->UpdateBounds(&mBuilder);
-          }
-
-          if (destroy) {
-            oldItem->Destroy(&mBuilder);
-          }
-          UseItem(newItem);
+          ProcessOldNode(item.mNode, Move(result));
         }
       } else {
-        // If there was no matching item in the old list, then we only need to
-        // add the new item to the merged list.
-        modified = true;
-        UseItem(newItem);
+        // Grab the current predecessor, push predecessors of that onto the processing
+        // stack (if it hasn't already been processed), and then advance to the next entry.
+        OldListIndex currentIndex = mStack.LastElement().GetAndIncrementCurrentPredecessor();
+        if (!mOldItems[currentIndex.val].IsUsed()) {
+          mStack.AppendElement(
+            PredecessorStackItem(currentIndex, mOldDAG.GetDirectPredecessors(currentIndex)));
+        }
       }
     }
   }
 
-  // Reuse the remaining valid items from the old display list.
-  while (nsDisplayItem* old = aOldList->RemoveBottom()) {
-    if (!IsAnyAncestorModified(old->FrameForInvalidation()) &&
-        (!old->IsReused() || newListIsEmpty)) {
-      if (old->GetChildren()) {
-        // We are calling MergeDisplayLists() to ensure that the display items
-        // with modified or deleted children will be correctly handled.
-        // Passing an empty new display list as an argument skips the merging
-        // loop above and jumps back here.
-        nsDisplayList empty;
-        Maybe<const ActiveScrolledRoot*> containerASRForChildren;
-
-        if (MergeDisplayLists(&empty, old->GetChildren(),
-                              old->GetChildren(), containerASRForChildren)) {
-          modified = true;
+  AutoTArray<MergedListIndex, 2> ResolveNodeIndexesOldToMerged(Span<OldListIndex> aDirectPredecessors) {
+    AutoTArray<MergedListIndex, 2> result;
+    result.SetCapacity(aDirectPredecessors.Length());
+    for (OldListIndex index : aDirectPredecessors) {
+      OldItemInfo& oldItem = mOldItems[index.val];
+      if (oldItem.IsDiscarded()) {
+        for (MergedListIndex inner : oldItem.mDirectPredecessors) {
+          if (!result.Contains(inner)) {
+            result.AppendElement(inner);
+          }
         }
-        UpdateASR(old, containerASRForChildren);
-        old->UpdateBounds(&mBuilder);
+      } else {
+        result.AppendElement(oldItem.mIndex);
       }
-      if (old->GetType() == DisplayItemType::TYPE_LAYER_EVENT_REGIONS) {
-        if (MergeLayerEventRegions(old, nullptr)) {
-          modified = true;
-        }
-      }
-      ReuseItem(old);
-    } else {
-      old->Destroy(&mBuilder);
-      modified = true;
     }
+    return result;
   }
 
-  aOutList->AppendToTop(&merged);
-  return modified;
+  RetainedDisplayListBuilder* mBuilder;
+  Maybe<const ActiveScrolledRoot*> mContainerASR;
+  nsTArray<OldItemInfo> mOldItems;
+  DirectedAcyclicGraph<OldListUnits> mOldDAG;
+  // Unfortunately we can't use strong typing for the hashtables
+  // since they internally encode the type with the mOps pointer,
+  // and assert when we try swap the contents
+  nsDataHashtable<DisplayItemHashEntry, size_t> mOldKeyLookup;
+  nsDisplayList mMergedItems;
+  DirectedAcyclicGraph<MergedListUnits> mMergedDAG;
+  nsDataHashtable<DisplayItemHashEntry, size_t> mMergedKeyLookup;
+  bool mResultIsModified;
+};
+
+void RetainedDisplayList::ClearDAG()
+{
+  mDAG.Clear();
+  mKeyLookup.Clear();
+}
+
+/**
+ * Takes two display lists and merges them into an output list.
+ *
+ * Display lists wthout an explicit DAG are interpreted as linear DAGs (with a maximum
+ * of one direct predecessor and one direct successor per node). We add the two DAGs
+ * together, and then output the topological sorted ordering as the final display list.
+ *
+ * Once we've merged a list, we then retain the DAG (as part of the RetainedDisplayList
+ * object) to use for future merges.
+ */
+bool
+RetainedDisplayListBuilder::MergeDisplayLists(nsDisplayList* aNewList,
+                                              RetainedDisplayList* aOldList,
+                                              RetainedDisplayList* aOutList,
+                                              mozilla::Maybe<const mozilla::ActiveScrolledRoot*>& aOutContainerASR)
+{
+  MergeState merge(this, Move(*aOldList));
+
+  Maybe<MergedListIndex> previousItemIndex;
+  while (nsDisplayItem* item = aNewList->RemoveBottom()) {
+    previousItemIndex = Some(merge.ProcessItemFromNewList(item, previousItemIndex));
+  }
+
+  *aOutList = Move(merge.Finalize());
+  aOutContainerASR = merge.mContainerASR;
+  return merge.mResultIsModified;
 }
 
 static void
 TakeAndAddModifiedAndFramesWithPropsFromRootFrame(
   nsTArray<nsIFrame*>* aModifiedFrames,
   nsTArray<nsIFrame*>* aFramesWithProps,
   nsIFrame* aRootFrame)
 {
@@ -1134,26 +1018,27 @@ RetainedDisplayListBuilder::AttemptParti
 
     mPreviousCaret = mBuilder.GetCaretFrame();
   }
 
   nsRect modifiedDirty;
   AnimatedGeometryRoot* modifiedAGR = nullptr;
   if (!shouldBuildPartial ||
       !ComputeRebuildRegion(modifiedFrames.Frames(), &modifiedDirty,
-                           &modifiedAGR, framesWithProps.Frames())) {
-    mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), &mList);
+                           &modifiedAGR, framesWithProps.Frames()) ||
+      !PreProcessDisplayList(&mList, modifiedAGR)) {
+    mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), List());
+    mList.ClearDAG();
     return PartialUpdateResult::Failed;
   }
 
   modifiedDirty.IntersectRect(modifiedDirty, mBuilder.RootReferenceFrame()->GetVisualOverflowRectRelativeToSelf());
 
   PartialUpdateResult result = PartialUpdateResult::NoChange;
-  if (PreProcessDisplayList(&mList, modifiedAGR) ||
-      !modifiedDirty.IsEmpty() ||
+  if (!modifiedDirty.IsEmpty() ||
       !framesWithProps.IsEmpty()) {
     result = PartialUpdateResult::Updated;
   }
 
   mBuilder.SetDirtyRect(modifiedDirty);
   mBuilder.SetPartialUpdate(true);
 
   nsDisplayList modifiedDL;
@@ -1186,11 +1071,11 @@ RetainedDisplayListBuilder::AttemptParti
   Maybe<const ActiveScrolledRoot*> dummy;
   if (MergeDisplayLists(&modifiedDL, &mList, &mList, dummy)) {
     result = PartialUpdateResult::Updated;
   }
 
   //printf_stderr("Painting --- Merged list:\n");
   //nsFrame::PrintDisplayList(&mBuilder, mList);
 
-  mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), &mList);
+  mBuilder.LeavePresShell(mBuilder.RootReferenceFrame(), List());
   return result;
 }
--- a/layout/painting/RetainedDisplayListBuilder.h
+++ b/layout/painting/RetainedDisplayListBuilder.h
@@ -44,29 +44,30 @@ struct RetainedDisplayListBuilder {
    * Also clears the frame properties set by RetainedDisplayListBuilder for all
    * the frames in the modified frame lists.
    */
   void ClearFramesWithProps();
 
   NS_DECLARE_FRAME_PROPERTY_DELETABLE(Cached, RetainedDisplayListBuilder)
 
 private:
-  bool PreProcessDisplayList(nsDisplayList* aList, AnimatedGeometryRoot* aAGR);
-
+  bool PreProcessDisplayList(RetainedDisplayList* aList, AnimatedGeometryRoot* aAGR);
   bool MergeDisplayLists(nsDisplayList* aNewList,
-                         nsDisplayList* aOldList,
-                         nsDisplayList* aOutList,
+                         RetainedDisplayList* aOldList,
+                         RetainedDisplayList* aOutList,
                          mozilla::Maybe<const mozilla::ActiveScrolledRoot*>& aOutContainerASR);
 
   bool ComputeRebuildRegion(nsTArray<nsIFrame*>& aModifiedFrames,
                             nsRect* aOutDirty,
                             AnimatedGeometryRoot** aOutModifiedAGR,
                             nsTArray<nsIFrame*>& aOutFramesWithProps);
 
   void IncrementSubDocPresShellPaintCount(nsDisplayItem* aItem);
 
+  friend class MergeState;
+
   nsDisplayListBuilder mBuilder;
-  nsDisplayList mList;
+  RetainedDisplayList mList;
   WeakFrame mPreviousCaret;
 
 };
 
 #endif // RETAINEDDISPLAYLISTBUILDER_H_
new file mode 100644
--- /dev/null
+++ b/layout/painting/RetainedDisplayListHelpers.h
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef RETAINEDDISPLAYLISTHELPERS_H_
+#define RETAINEDDISPLAYLISTHELPERS_H_
+
+#include "PLDHashTable.h"
+
+struct DisplayItemKey
+{
+  bool operator ==(const DisplayItemKey& aOther) const {
+    return mFrame == aOther.mFrame &&
+           mPerFrameKey == aOther.mPerFrameKey;
+  }
+
+  nsIFrame* mFrame;
+  uint32_t mPerFrameKey;
+};
+
+class DisplayItemHashEntry : public PLDHashEntryHdr
+{
+public:
+  typedef DisplayItemKey KeyType;
+  typedef const DisplayItemKey* KeyTypePointer;
+
+  explicit DisplayItemHashEntry(KeyTypePointer aKey)
+    : mKey(*aKey) {}
+  explicit DisplayItemHashEntry(const DisplayItemHashEntry& aCopy)=default;
+
+  ~DisplayItemHashEntry() = default;
+
+  KeyType GetKey() const { return mKey; }
+  bool KeyEquals(KeyTypePointer aKey) const
+  {
+    return mKey == *aKey;
+  }
+
+  static KeyTypePointer KeyToPointer(KeyType& aKey) { return &aKey; }
+  static PLDHashNumber HashKey(KeyTypePointer aKey)
+  {
+    if (!aKey)
+      return 0;
+
+    return mozilla::HashGeneric(aKey->mFrame, aKey->mPerFrameKey);
+  }
+  enum { ALLOW_MEMMOVE = true };
+
+  DisplayItemKey mKey;
+};
+
+template <typename T>
+bool SpanContains(mozilla::Span<const T>& aSpan, T aItem)
+{
+  for (const T& i : aSpan) {
+    if (i == aItem) {
+      return true;
+    }
+  }
+  return false;
+}
+
+class OldListUnits {};
+class MergedListUnits {};
+
+template <typename Units>
+struct Index {
+  Index()
+    : val(0)
+  {}
+  explicit Index(size_t aVal)
+    : val(aVal)
+  {}
+
+  bool operator==(const Index<Units>& aOther) const
+  {
+    return val == aOther.val;
+  }
+
+  size_t val;
+};
+typedef Index<OldListUnits> OldListIndex;
+typedef Index<MergedListUnits> MergedListIndex;
+
+
+template <typename T>
+class DirectedAcyclicGraph {
+public:
+  DirectedAcyclicGraph() {}
+  DirectedAcyclicGraph(DirectedAcyclicGraph&& aOther)
+    : mNodesInfo(mozilla::Move(aOther.mNodesInfo))
+    , mDirectPredecessorList(mozilla::Move(aOther.mDirectPredecessorList))
+  {}
+
+  DirectedAcyclicGraph& operator=(DirectedAcyclicGraph&& aOther)
+  {
+    mNodesInfo = mozilla::Move(aOther.mNodesInfo);
+    mDirectPredecessorList = mozilla::Move(aOther.mDirectPredecessorList);
+    return *this;
+  }
+
+  Index<T> AddNode(mozilla::Span<const Index<T>> aDirectPredecessors,
+                   const mozilla::Maybe<Index<T>>& aExtraPredecessor = mozilla::Nothing())
+  {
+    size_t index = mNodesInfo.Length();
+    mNodesInfo.AppendElement(NodeInfo(mDirectPredecessorList.Length(), aDirectPredecessors.Length()));
+    if (aExtraPredecessor && !SpanContains(aDirectPredecessors, aExtraPredecessor.value())) {
+      mNodesInfo.LastElement().mDirectPredecessorCount++;
+      mDirectPredecessorList.SetCapacity(mDirectPredecessorList.Length() + aDirectPredecessors.Length() + 1);
+      mDirectPredecessorList.AppendElements(aDirectPredecessors);
+      mDirectPredecessorList.AppendElement(aExtraPredecessor.value());
+    } else {
+      mDirectPredecessorList.AppendElements(aDirectPredecessors);
+    }
+    return Index<T>(index);
+  }
+
+  size_t Length()
+  {
+    return mNodesInfo.Length();
+  }
+
+  mozilla::Span<Index<T>> GetDirectPredecessors(Index<T> aNodeIndex)
+  {
+    NodeInfo& node = mNodesInfo[aNodeIndex.val];
+    return mozilla::MakeSpan(mDirectPredecessorList).Subspan(node.mIndexInDirectPredecessorList,
+                                                             node.mDirectPredecessorCount);
+  }
+
+  template<typename OtherUnits>
+  void EnsureCapacityFor(const DirectedAcyclicGraph<OtherUnits>& aOther)
+  {
+    mNodesInfo.SetCapacity(aOther.mNodesInfo.Length());
+    mDirectPredecessorList.SetCapacity(aOther.mDirectPredecessorList.Length());
+  }
+
+  void Clear()
+  {
+    mNodesInfo.Clear();
+    mDirectPredecessorList.Clear();
+  }
+
+  struct NodeInfo {
+    NodeInfo(size_t aIndexInDirectPredecessorList,
+             size_t aDirectPredecessorCount)
+      : mIndexInDirectPredecessorList(aIndexInDirectPredecessorList)
+      , mDirectPredecessorCount(aDirectPredecessorCount)
+    {}
+    size_t mIndexInDirectPredecessorList;
+    size_t mDirectPredecessorCount;
+  };
+
+  nsTArray<NodeInfo> mNodesInfo;
+  nsTArray<Index<T>> mDirectPredecessorList;
+};
+
+struct RetainedDisplayListBuilder;
+class nsDisplayItem;
+
+struct OldItemInfo {
+  explicit OldItemInfo(nsDisplayItem* aItem)
+    : mItem(aItem)
+    , mUsed(false)
+    , mDiscarded(false)
+  {}
+
+  void AddedToMergedList(MergedListIndex aIndex)
+  {
+    MOZ_ASSERT(!IsUsed());
+    mUsed = true;
+    mIndex = aIndex;
+    mItem = nullptr;
+  }
+
+  void AddedMatchToMergedList(RetainedDisplayListBuilder* aBuilder,
+                              MergedListIndex aIndex);
+  void Discard(RetainedDisplayListBuilder* aBuilder,
+               nsTArray<MergedListIndex>&& aDirectPredecessors);
+  bool IsUsed()
+  {
+    return mUsed;
+  }
+
+  bool IsDiscarded()
+  {
+    MOZ_ASSERT(IsUsed());
+    return mDiscarded;
+  }
+
+  nsDisplayItem* mItem;
+  bool mUsed;
+  bool mDiscarded;
+  MergedListIndex mIndex;
+  nsTArray<MergedListIndex> mDirectPredecessors;
+};
+
+#endif // RETAINEDDISPLAYLISTHELPERS_H_
--- a/layout/painting/moz.build
+++ b/layout/painting/moz.build
@@ -17,16 +17,17 @@ EXPORTS += [
     'nsCSSRenderingBorders.h',
     'nsCSSRenderingGradients.h',
     'nsDisplayItemTypes.h',
     'nsDisplayItemTypesList.h',
     'nsDisplayList.h',
     'nsDisplayListInvalidation.h',
     'nsImageRenderer.h',
     'RetainedDisplayListBuilder.h',
+    'RetainedDisplayListHelpers.h',
 ]
 
 EXPORTS.mozilla += [
     'PaintTracker.h',
 ]
 
 UNIFIED_SOURCES += [
     'ActiveLayerTracker.cpp',
--- a/layout/painting/nsDisplayList.h
+++ b/layout/painting/nsDisplayList.h
@@ -39,16 +39,17 @@
 #include "mozilla/UniquePtr.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/gfx/UserData.h"
 #include "mozilla/layers/LayerAttributes.h"
 #include "nsCSSRenderingBorders.h"
 #include "nsPresArena.h"
 #include "nsAutoLayoutPhase.h"
 #include "nsDisplayItemTypes.h"
+#include "RetainedDisplayListHelpers.h"
 
 #include <stdint.h>
 #include "nsTHashtable.h"
 
 #include <stdlib.h>
 #include <algorithm>
 #include <unordered_set>
 
@@ -1091,21 +1092,21 @@ public:
       if (aBuilder->mCurrentFrame == aForChild->GetParent()) {
         if (mCurrentAGRState == AGR_YES) {
           aBuilder->mCurrentAGR = aBuilder->WrapAGRForFrame(aForChild, isAsync, aBuilder->mCurrentAGR);
         }
       } else if (aForChild != aBuilder->mCurrentFrame) {
         aBuilder->mCurrentAGR = aBuilder->FindAnimatedGeometryRootFor(aForChild);
       }
       MOZ_ASSERT(nsLayoutUtils::IsAncestorFrameCrossDoc(aBuilder->RootReferenceFrame(), *aBuilder->mCurrentAGR));
+      aBuilder->mInInvalidSubtree = aBuilder->mInInvalidSubtree || aForChild->IsFrameModified();
       aBuilder->mCurrentFrame = aForChild;
       aBuilder->mVisibleRect = aVisibleRect;
-      aBuilder->mDirtyRect = aDirtyRect;
+      aBuilder->mDirtyRect = aBuilder->mInInvalidSubtree ? aVisibleRect : aDirtyRect;
       aBuilder->mIsAtRootOfPseudoStackingContext = aIsRoot;
-      aBuilder->mInInvalidSubtree = aBuilder->mInInvalidSubtree || aForChild->IsFrameModified();
     }
     void SetReferenceFrameAndCurrentOffset(const nsIFrame* aFrame, const nsPoint& aOffset) {
       mBuilder->mCurrentReferenceFrame = aFrame;
       mBuilder->mCurrentOffsetToReferenceFrame = aOffset;
     }
     bool IsAnimatedGeometryRoot() const {
       return mCurrentAGRState == AGR_YES;
     }
@@ -1735,16 +1736,17 @@ public:
     }
     return false;
   }
 
   bool MarkCurrentFrameModifiedDuringBuilding()
   {
     if (MarkFrameModifiedDuringBuilding(const_cast<nsIFrame*>(mCurrentFrame))) {
       mInInvalidSubtree = true;
+      mDirtyRect = mVisibleRect;
       return true;
     }
     return false;
   }
 
   /**
    * This is a convenience function to ease the transition until AGRs and ASRs
    * are unified.
@@ -2011,16 +2013,17 @@ private:
   bool                           mInInvalidSubtree;
   bool                           mBuildCompositorHitTestInfo;
   bool                           mLessEventRegionItems;
   bool                           mBuiltOverlayScrollbars;
 };
 
 class nsDisplayItem;
 class nsDisplayList;
+class RetainedDisplayList;
 /**
  * nsDisplayItems are put in singly-linked lists rooted in an nsDisplayList.
  * nsDisplayItemLink holds the link. The lists are linked from lowest to
  * highest in z-order.
  */
 class nsDisplayItemLink {
   // This is never instantiated directly, so no need to count constructors and
   // destructors.
@@ -2650,17 +2653,17 @@ public:
    * the same 3d rendering context.
    */
   virtual void DoUpdateBoundsPreserves3D(nsDisplayListBuilder* aBuilder) {}
 
   /**
    * If this has a child list, return it, even if the children are in
    * a different coordinate system to this item.
    */
-  virtual nsDisplayList* GetChildren() const { return nullptr; }
+  virtual RetainedDisplayList* GetChildren() const { return nullptr; }
 
   /**
    * Returns the visible rect.
    */
   const nsRect& GetVisibleRect() const { return mVisibleRect; }
 
   void SetVisibleRect(const nsRect& aVisibleRect, bool aStore)
   {
@@ -3321,16 +3324,69 @@ struct nsDisplayListCollection : public 
 private:
   // This class is only used on stack, so we don't have to worry about leaking
   // it.  Don't let us be heap-allocated!
   void* operator new(size_t sz) CPP_THROW_NEW;
 
   nsDisplayList mLists[6];
 };
 
+/**
+ * A display list that also retains the partial build
+ * information (in the form of a DAG) used to create it.
+ *
+ * Display lists built from a partial list aren't necessarily
+ * in the same order as a full build, and the DAG retains
+ * the information needing to interpret the current
+ * order correctly.
+ */
+class RetainedDisplayList : public nsDisplayList {
+public:
+  RetainedDisplayList() {}
+  RetainedDisplayList(RetainedDisplayList&& aOther)
+  {
+    AppendToTop(&aOther);
+    mDAG = mozilla::Move(aOther.mDAG);
+    mKeyLookup.SwapElements(aOther.mKeyLookup);
+  }
+  ~RetainedDisplayList()
+  {
+    MOZ_ASSERT(mOldItems.IsEmpty(), "Must empty list before destroying");
+  }
+
+  RetainedDisplayList& operator=(RetainedDisplayList&& aOther)
+  {
+    MOZ_ASSERT(!Count(), "Can only move into an empty list!");
+    MOZ_ASSERT(mOldItems.IsEmpty(), "Can only move into an empty list!");
+    AppendToTop(&aOther);
+    mDAG = mozilla::Move(aOther.mDAG);
+    mKeyLookup.SwapElements(aOther.mKeyLookup);
+    return *this;
+  }
+
+  void DeleteAll(nsDisplayListBuilder* aBuilder)
+  {
+    for (OldItemInfo& i : mOldItems) {
+      if (i.mItem) {
+        i.mItem->Destroy(aBuilder);
+      }
+    }
+    mOldItems.Clear();
+    nsDisplayList::DeleteAll(aBuilder);
+  }
+
+  void ClearDAG();
+
+  DirectedAcyclicGraph<MergedListUnits> mDAG;
+  nsDataHashtable<DisplayItemHashEntry, size_t> mKeyLookup;
+
+  // Temporary state initialized during the preprocess pass
+  // of RetainedDisplayListBuilder and then used during merging.
+  nsTArray<OldItemInfo> mOldItems;
+};
 
 class nsDisplayImageContainer : public nsDisplayItem {
 public:
   typedef mozilla::LayerIntPoint LayerIntPoint;
   typedef mozilla::LayoutDeviceRect LayoutDeviceRect;
   typedef mozilla::layers::ImageContainer ImageContainer;
   typedef mozilla::layers::ImageLayer ImageLayer;
 
@@ -5000,17 +5056,17 @@ public:
   {
     NS_ASSERTION(mListPtr->IsEmpty() || !ReferenceFrame() ||
                  !mListPtr->GetBottom()->ReferenceFrame() ||
                  mListPtr->GetBottom()->ReferenceFrame() == ReferenceFrame(),
                  "Children must have same reference frame");
     return mListPtr;
   }
 
-  virtual nsDisplayList* GetChildren() const override { return mListPtr; }
+  virtual RetainedDisplayList* GetChildren() const override { return mListPtr; }
 
   virtual int32_t ZIndex() const override
   {
     return (mHasZIndexOverride) ? mOverrideZIndex : nsDisplayItem::ZIndex();
   }
 
   void SetOverrideZIndex(int32_t aZIndex)
   {
@@ -5046,18 +5102,18 @@ protected:
   void MergeFromTrackingMergedFrames(const nsDisplayWrapList* aOther)
   {
     mBounds.UnionRect(mBounds, aOther->mBounds);
     mVisibleRect.UnionRect(mVisibleRect, aOther->mVisibleRect);
     mMergedFrames.AppendElement(aOther->mFrame);
     mMergedFrames.AppendElements(aOther->mMergedFrames);
   }
 
-  nsDisplayList mList;
-  nsDisplayList* mListPtr;
+  RetainedDisplayList mList;
+  RetainedDisplayList* mListPtr;
   // The active scrolled root for the frame that created this
   // wrap list.
   RefPtr<const ActiveScrolledRoot> mFrameActiveScrolledRoot;
   // The frames from items that have been merged into this item, excluding
   // this item's own frame.
   nsTArray<nsIFrame*> mMergedFrames;
   nsRect mBounds;
   // Visible rect contributed by this display item itself.
@@ -6181,17 +6237,17 @@ public:
   virtual nsRect GetComponentAlphaBounds(nsDisplayListBuilder* aBuilder) const override
   {
     if (mStoredList.GetComponentAlphaBounds(aBuilder).IsEmpty())
       return nsRect();
     bool snap;
     return GetBounds(aBuilder, &snap);
   }
 
-  virtual nsDisplayList* GetChildren() const override
+  virtual RetainedDisplayList* GetChildren() const override
   {
     return mStoredList.GetChildren();
   }
 
   virtual void SetActiveScrolledRoot(const ActiveScrolledRoot* aActiveScrolledRoot) override
   {
     nsDisplayItem::SetActiveScrolledRoot(aActiveScrolledRoot);
     mStoredList.SetActiveScrolledRoot(aActiveScrolledRoot);
@@ -6581,17 +6637,17 @@ public:
     return true;
   }
 
   virtual nsDisplayList* GetSameCoordinateSystemChildren() const override
   {
     return mList.GetChildren();
   }
 
-  virtual nsDisplayList* GetChildren() const override
+  virtual RetainedDisplayList* GetChildren() const override
   {
     return mList.GetChildren();
   }
 
   virtual void SetActiveScrolledRoot(const ActiveScrolledRoot* aActiveScrolledRoot) override
   {
     nsDisplayItem::SetActiveScrolledRoot(aActiveScrolledRoot);
     mList.SetActiveScrolledRoot(aActiveScrolledRoot);
--- a/layout/reftests/position-dynamic-changes/relative/reftest.list
+++ b/layout/reftests/position-dynamic-changes/relative/reftest.list
@@ -1,5 +1,5 @@
-fuzzy-if(cocoaWidget,1,2) fuzzy-if(d2d,47,26) fuzzy-if(asyncPan&&!layersGPUAccelerated,149,716) == move-right-bottom.html move-right-bottom-ref.html
-fuzzy-if(cocoaWidget,1,2) fuzzy-if(asyncPan&&!layersGPUAccelerated,149,716) == move-top-left.html move-top-left-ref.html # Bug 688545
+fuzzy-if(cocoaWidget,1,2) fuzzy-if(d2d,47,26) fuzzy-if(asyncPan&&!layersGPUAccelerated,169,970) == move-right-bottom.html move-right-bottom-ref.html
+fuzzy-if(cocoaWidget,1,2) fuzzy-if(asyncPan&&!layersGPUAccelerated,169,970) == move-top-left.html move-top-left-ref.html # Bug 688545
 fuzzy-if(cocoaWidget,1,3) fuzzy-if(asyncPan&&!layersGPUAccelerated,144,580) == move-right-bottom-table.html move-right-bottom-table-ref.html
 fuzzy-if(cocoaWidget,1,3) fuzzy-if(asyncPan&&!layersGPUAccelerated,144,580) == move-top-left-table.html move-top-left-table-ref.html # Bug 688545
 == percent.html percent-ref.html
--- a/layout/reftests/w3c-css/submitted/ruby/reftest.list
+++ b/layout/reftests/w3c-css/submitted/ruby/reftest.list
@@ -1,11 +1,11 @@
 # Tests for inlinizing block-level boxes
 == ruby-inlinize-blocks-001.html ruby-inlinize-blocks-001-ref.html
-== ruby-inlinize-blocks-002.html ruby-inlinize-blocks-002-ref.html
+fuzzy-if(winWidget,144,1) == ruby-inlinize-blocks-002.html ruby-inlinize-blocks-002-ref.html
 == ruby-inlinize-blocks-003.html ruby-inlinize-blocks-003-ref.html
 == ruby-inlinize-blocks-004.html ruby-inlinize-blocks-004-ref.html
 == ruby-inlinize-blocks-005.html ruby-inlinize-blocks-005-ref.html
 
 # Tests for autohiding base-identical annotations
 == ruby-autohide-001.html ruby-autohide-001-ref.html
 == ruby-autohide-002.html ruby-autohide-002-ref.html
 == ruby-autohide-003.html ruby-autohide-003-ref.html