Bug 1425315 - wip draft
authorEthan Lin <ethlin@mozilla.com>
Mon, 15 Jan 2018 17:55:09 +0800
changeset 720807 bdd52831e6d846fd8b3b3505b811075edbedbb48
parent 720349 028b78616a1fb85620a713fee97c42e8cac52756
child 746159 18fbe93015cae69cc1d789e59e4976c525b0bf9a
push id95651
push userbmo:ethlin@mozilla.com
push dateTue, 16 Jan 2018 09:34:18 +0000
bugs1425315
milestone59.0a1
Bug 1425315 - wip MozReview-Commit-ID: IifY7bs9jnz
dom/animation/EffectCompositor.cpp
dom/animation/EffectCompositor.h
dom/animation/KeyframeEffectReadOnly.cpp
dom/base/nsDOMWindowUtils.cpp
dom/interfaces/base/nsIDOMWindowUtils.idl
layout/base/GeckoRestyleManager.cpp
layout/base/RestyleManager.cpp
layout/base/RestyleManager.h
layout/base/ServoRestyleManager.cpp
layout/base/nsChangeHint.h
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/painting/nsDisplayList.cpp
layout/reftests/bugs/1078262-1.html
layout/reftests/css-animations/opacity-animation-in-fixed-opacity-parent.html
layout/reftests/css-animations/stacking-context-opacity-wins-over-transition.html
layout/reftests/css-animations/stacking-context-transform-wins-over-transition.html
layout/reftests/css-transitions/stacking-context-opacity-wins-over-important-style.html
layout/reftests/css-transitions/stacking-context-transform-wins-over-important-style.html
layout/reftests/web-animations/stacking-context-opacity-changing-effect.html
layout/reftests/web-animations/stacking-context-opacity-changing-keyframe.html
layout/reftests/web-animations/stacking-context-opacity-changing-target.html
layout/reftests/web-animations/stacking-context-transform-changing-display-property.html
layout/reftests/web-animations/stacking-context-transform-changing-effect.html
layout/reftests/web-animations/stacking-context-transform-changing-keyframe.html
layout/reftests/web-animations/stacking-context-transform-changing-target.html
layout/reftests/web-animations/stacking-context-transform-none-animation-before-appending-element.html
layout/style/nsStyleSet.cpp
layout/tools/reftest/reftest-content.js
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -298,38 +298,42 @@ EffectCompositor::RequestRestyle(dom::El
       effectSet->UpdateAnimationGeneration(mPresContext);
     }
   }
 }
 
 void
 EffectCompositor::PostRestyleForAnimation(dom::Element* aElement,
                                           CSSPseudoElementType aPseudoType,
-                                          CascadeLevel aCascadeLevel)
+                                          CascadeLevel aCascadeLevel,
+                                          bool aThrottled)
 {
   if (!mPresContext) {
     return;
   }
 
   dom::Element* element = GetElementToRestyle(aElement, aPseudoType);
   if (!element) {
     return;
   }
 
   nsRestyleHint hint = aCascadeLevel == CascadeLevel::Transitions ?
                                         eRestyle_CSSTransitions :
                                         eRestyle_CSSAnimations;
+  if (aThrottled) {
+    hint = nsRestyleHint(hint | eRestyle_CSSThrottledAnimations);
+  }
 
   if (mPresContext->StyleSet()->IsServo()) {
     MOZ_ASSERT(NS_IsMainThread(),
                "Restyle request during restyling should be requested only on "
                "the main-thread. e.g. after the parallel traversal");
     if (ServoStyleSet::IsInServoTraversal() || mIsInPreTraverse) {
-      MOZ_ASSERT(hint == eRestyle_CSSAnimations ||
-                 hint == eRestyle_CSSTransitions);
+      MOZ_ASSERT(hint & eRestyle_CSSAnimations ||
+                 hint & eRestyle_CSSTransitions);
 
       // We can't call Servo_NoteExplicitHints here since AtomicRefCell does not
       // allow us mutate ElementData of the |aElement| in SequentialTask.
       // Instead we call Servo_NoteExplicitHints for the element in PreTraverse()
       // which will be called right before the second traversal that we do for
       // updating CSS animations.
       // In that case PreTraverse() will return true so that we know to do the
       // second traversal so we don't need to post any restyle requests to the
@@ -352,17 +356,17 @@ EffectCompositor::PostRestyleForThrottle
     for (auto iter = elementSet.Iter(); !iter.Done(); iter.Next()) {
       bool& postedRestyle = iter.Data();
       if (postedRestyle) {
         continue;
       }
 
       PostRestyleForAnimation(iter.Key().mElement,
                               iter.Key().mPseudoType,
-                              cascadeLevel);
+                              cascadeLevel, true);
       postedRestyle = true;
     }
   }
 }
 
 template<typename StyleType>
 void
 EffectCompositor::UpdateEffectProperties(StyleType* aStyleType,
--- a/dom/animation/EffectCompositor.h
+++ b/dom/animation/EffectCompositor.h
@@ -105,17 +105,18 @@ public:
                       CascadeLevel aCascadeLevel);
 
   // Schedule an animation restyle. This is called automatically by
   // RequestRestyle when necessary. However, it is exposed here since we also
   // need to perform this step when triggering transitions *without* also
   // invalidating the animation style rule (which RequestRestyle would do).
   void PostRestyleForAnimation(dom::Element* aElement,
                                CSSPseudoElementType aPseudoType,
-                               CascadeLevel aCascadeLevel);
+                               CascadeLevel aCascadeLevel,
+                               bool aThrottled = false);
 
   // Posts an animation restyle for any elements whose animation style rule
   // is out of date but for which an animation restyle has not yet been
   // posted because updates on the main thread are throttled.
   void PostRestyleForThrottledAnimations();
 
   // Called when computed style on the specified (pseudo-) element might
   // have changed so that any context-sensitive values stored within
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -131,16 +131,17 @@ KeyframeEffectReadOnly::NotifyAnimationT
   }
 
   // Request restyle if necessary.
   if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
     EffectCompositor::RestyleType restyleType =
       CanThrottle() ?
       EffectCompositor::RestyleType::Throttled :
       EffectCompositor::RestyleType::Standard;
+
     RequestRestyle(restyleType);
   }
 
   // Detect changes to "in effect" status since we need to recalculate the
   // animation cascade for this element whenever that changes.
   // Note that updating mInEffectOnLastAnimationTimingUpdate has to be done
   // after above CanThrottle() call since the function uses the flag inside it.
   bool inEffect = IsInEffect();
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -1592,16 +1592,31 @@ nsDOMWindowUtils::GetIsMozAfterPaintPend
   nsPresContext* presContext = GetPresContext();
   if (!presContext)
     return NS_OK;
   *aResult = presContext->IsDOMPaintEventPending();
   return NS_OK;
 }
 
 NS_IMETHODIMP
+nsDOMWindowUtils::GetIsMozAfterAnimationPending(bool *aResult)
+{
+  NS_ENSURE_ARG_POINTER(aResult);
+  *aResult = false;
+
+  nsPresContext* presContext = GetPresContext();
+  if (!presContext)
+    return NS_OK;
+
+  *aResult = presContext->GetRootPresContext()->RefreshDriver()->OnlyAnimationFlush();
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsDOMWindowUtils::DisableNonTestMouseEvents(bool aDisable)
 {
   nsCOMPtr<nsPIDOMWindowOuter> window = do_QueryReferent(mWindow);
   NS_ENSURE_TRUE(window, NS_ERROR_FAILURE);
   nsIDocShell *docShell = window->GetDocShell();
   NS_ENSURE_TRUE(docShell, NS_ERROR_FAILURE);
   nsCOMPtr<nsIPresShell> presShell = docShell->GetPresShell();
   NS_ENSURE_TRUE(presShell, NS_ERROR_FAILURE);
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -849,16 +849,18 @@ interface nsIDOMWindowUtils : nsISupport
                            out unsigned long aMaxDifference);
 
   /**
    * Returns true if a MozAfterPaint event has been queued but not yet
    * fired.
    */
   readonly attribute boolean isMozAfterPaintPending;
 
+  readonly attribute boolean isMozAfterAnimationPending;
+
   /**
    * Suppresses/unsuppresses user initiated event handling in window's document
    * and subdocuments.
    *
    * @throw NS_ERROR_DOM_SECURITY_ERR if called without chrome privileges and
    *        NS_ERROR_FAILURE if window doesn't have a document.
    */
   void suppressEventHandling(in boolean aSuppress);
--- a/layout/base/GeckoRestyleManager.cpp
+++ b/layout/base/GeckoRestyleManager.cpp
@@ -612,16 +612,17 @@ GeckoRestyleManager::ProcessPendingResty
     PresContext()->TransitionManager()->SetInAnimationOnlyStyleUpdate(false);
   }
 
   mIsProcessingRestyles = false;
 
   NS_ASSERTION(haveNonAnimation || !mHavePendingNonAnimationRestyles,
                "should not have added restyles");
   mHavePendingNonAnimationRestyles = false;
+  mOnlyThrottledAnimation = false;
 
   if (mDoRebuildAllStyleData) {
     // We probably wasted a lot of work up above, but this seems safest
     // and it should be rarely used.
     // This might add us as a refresh observer again; that's ok.
     ProcessPendingRestyles();
 
     NS_ASSERTION(!mDoRebuildAllStyleData,
@@ -747,16 +748,18 @@ GeckoRestyleManager::PostRestyleEvent(El
                                      aRestyleHintData);
 
   // Set mHavePendingNonAnimationRestyles for any restyle that could
   // possibly contain non-animation styles (i.e., those that require us
   // to do an animation-only style flush before processing style changes
   // to ensure correct initialization of CSS transitions).
   if (aRestyleHint & ~eRestyle_AllHintsWithAnimations) {
     mHavePendingNonAnimationRestyles = true;
+  } else if (aRestyleHint & eRestyle_CSSThrottledAnimations) {
+    mOnlyThrottledAnimation = true;
   }
 
   PostRestyleEventInternal();
 }
 
 void
 GeckoRestyleManager::PostRebuildAllStyleDataEvent(nsChangeHint aExtraHint,
                                                   nsRestyleHint aRestyleHint)
--- a/layout/base/RestyleManager.cpp
+++ b/layout/base/RestyleManager.cpp
@@ -20,16 +20,17 @@ RestyleManager::RestyleManager(StyleBack
                                nsPresContext* aPresContext)
   : mPresContext(aPresContext)
   , mRestyleGeneration(1)
   , mUndisplayedRestyleGeneration(1)
   , mHoverGeneration(0)
   , mType(aType)
   , mInStyleRefresh(false)
   , mAnimationGeneration(0)
+  , mOnlyThrottledAnimation(false)
 {
   MOZ_ASSERT(mPresContext);
 }
 
 void
 RestyleManager::ContentInserted(nsINode* aContainer, nsIContent* aChild)
 {
   RestyleForInsertOrChange(aContainer, aChild);
@@ -1738,16 +1739,18 @@ RestyleManager::ProcessRestyledFrames(ns
     } else if (!data.mFrame || !data.mFrame->IsViewportFrame()) {
       NS_WARNING("Unable to test style tree integrity -- no content node "
                  "(and not a viewport frame)");
     }
   }
 #endif
 
   aChangeList.Clear();
+
+  mOnlyThrottledAnimation = false;
 }
 
 /* static */ uint64_t
 RestyleManager::GetAnimationGenerationForFrame(nsIFrame* aFrame)
 {
   EffectSet* effectSet = EffectSet::GetEffectSet(aFrame);
   return effectSet ? effectSet->GetAnimationGeneration() : 0;
 }
--- a/layout/base/RestyleManager.h
+++ b/layout/base/RestyleManager.h
@@ -83,16 +83,18 @@ public:
   // ProcessRestyledFrames call in a view update batch and a script blocker.
   // This function does not call ProcessAttachedQueue() on the binding manager.
   // If the caller wants that to happen synchronously, it needs to handle that
   // itself.
   void ProcessRestyledFrames(nsStyleChangeList& aChangeList);
 
   bool IsInStyleRefresh() const { return mInStyleRefresh; }
 
+  bool OnlyThrottledAnimation() const { return mOnlyThrottledAnimation; }
+
   // AnimationsWithDestroyedFrame is used to stop animations and transitions
   // on elements that have no frame at the end of the restyling process.
   // It only lives during the restyling process.
   class MOZ_STACK_CLASS AnimationsWithDestroyedFrame final {
   public:
     // Construct a AnimationsWithDestroyedFrame object.  The caller must
     // ensure that aRestyleManager lives at least as long as the
     // object.  (This is generally easy since the caller is typically a
@@ -281,15 +283,17 @@ protected:
   // The total number of animation flushes by this frame constructor.
   // Used to keep the layer and animation manager in sync.
   uint64_t mAnimationGeneration;
 
   OverflowChangedTracker mOverflowChangedTracker;
 
   AnimationsWithDestroyedFrame* mAnimationsWithDestroyedFrame = nullptr;
 
+  bool mOnlyThrottledAnimation;
+
   friend class mozilla::GeckoRestyleManager;
   friend class mozilla::ServoRestyleManager;
 };
 
 } // namespace mozilla
 
 #endif
--- a/layout/base/ServoRestyleManager.cpp
+++ b/layout/base/ServoRestyleManager.cpp
@@ -348,16 +348,18 @@ ServoRestyleManager::PostRestyleEvent(El
   // hints manually to avoid re-traversing the DOM to find them.
   if (mReentrantChanges && !aRestyleHint) {
     mReentrantChanges->AppendElement(ReentrantChange { aElement, aMinChangeHint });
     return;
   }
 
   if (aRestyleHint & ~eRestyle_AllHintsWithAnimations) {
     mHaveNonAnimationRestyles = true;
+  } else if (aRestyleHint & eRestyle_CSSThrottledAnimations) {
+    mOnlyThrottledAnimation = true;
   }
 
   if (aRestyleHint & eRestyle_LaterSiblings) {
     aRestyleHint &= ~eRestyle_LaterSiblings;
 
     nsRestyleHint siblingHint = eRestyle_Subtree;
     Element* current = aElement->GetNextElementSibling();
     while (current) {
@@ -1197,16 +1199,17 @@ ServoRestyleManager::DoProcessPendingRes
 
   doc->ClearServoRestyleRoot();
 
   FlushOverflowChangedTracker();
 
   ClearSnapshots();
   styleSet->AssertTreeIsClean();
   mHaveNonAnimationRestyles = false;
+  mOnlyThrottledAnimation = false;
   mRestyleForCSSRuleChanges = false;
   mInStyleRefresh = false;
 
   // Now that everything has settled, see if we have enough free rule nodes in
   // the tree to warrant sweeping them.
   styleSet->MaybeGCRuleTree();
 
   // Note: We are in the scope of |animationsWithDestroyedFrame|, so
@@ -1412,16 +1415,17 @@ ServoRestyleManager::TakeSnapshotForAttr
   // undisplayed elements, because we don't actually restyle those elements
   // during the restyle traversal. So just assume that the attribute change can
   // cause the style to change.
   IncrementUndisplayedRestyleGeneration();
 
   // Some other random attribute changes may also affect the transitions,
   // so we also set this true here.
   mHaveNonAnimationRestyles = true;
+  mOnlyThrottledAnimation = false;
 
   ServoElementSnapshot& snapshot = SnapshotFor(aElement);
   snapshot.AddAttrs(aElement, aNameSpaceID, aAttribute);
 
   if (influencesOtherPseudoClassState) {
     snapshot.AddOtherPseudoClassState(aElement);
   }
 }
@@ -1494,16 +1498,17 @@ ServoRestyleManager::AttributeChanged(El
   if (restyleHint) {
     // Assuming we need to invalidate cached style in getComputedStyle for
     // undisplayed elements, since we don't know if it is needed.
     IncrementUndisplayedRestyleGeneration();
 
     // If we change attributes, we have to mark this to be true, so we will
     // increase the animation generation for the new created transition if any.
     mHaveNonAnimationRestyles = true;
+    mOnlyThrottledAnimation = false;
   }
 }
 
 nsresult
 ServoRestyleManager::ReparentStyleContext(nsIFrame* aFrame)
 {
   // This is only called when moving frames in or out of the first-line
   // pseudo-element (or one of its descendants).  We can't say much about
--- a/layout/base/nsChangeHint.h
+++ b/layout/base/nsChangeHint.h
@@ -598,19 +598,22 @@ enum nsRestyleHint : uint32_t {
 
   // Continue the restyling process to all of the current frame's
   // descendants, even if any frame's restyling resulted in no style
   // changes.  (Implies eRestyle_Force.)  Note that this is weaker than
   // eRestyle_Subtree, which makes us rerun selector matching on all
   // descendants rather than just continuing the restyling process.
   eRestyle_ForceDescendants = 1 << 9,
 
+  eRestyle_CSSThrottledAnimations = 1 << 10,
+
   // Useful unions:
   eRestyle_AllHintsWithAnimations = eRestyle_CSSTransitions |
                                     eRestyle_CSSAnimations |
+                                    eRestyle_CSSThrottledAnimations |
                                     eRestyle_StyleAttribute_Animations,
 };
 
 // The functions below need an integral type to cast to to avoid
 // infinite recursion.
 typedef decltype(nsRestyleHint(0) + nsRestyleHint(0)) nsRestyleHint_size_t;
 
 inline constexpr nsRestyleHint operator|(nsRestyleHint aLeft,
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -1153,16 +1153,17 @@ nsRefreshDriver::nsRefreshDriver(nsPresC
     mThrottledFrameRequestInterval(TimeDuration::FromMilliseconds(
                                      GetThrottledTimerInterval())),
     mMinRecomputeVisibilityInterval(GetMinRecomputeVisibilityInterval()),
     mThrottled(false),
     mNeedToRecomputeVisibility(false),
     mTestControllingRefreshes(false),
     mViewManagerFlushIsPending(false),
     mHasScheduleFlush(false),
+    mOnlyAnimationFlush(false),
     mInRefresh(false),
     mWaitingForTransaction(false),
     mSkippedPaints(false),
     mResizeSuppressed(false),
     mWarningThreshold(REFRESH_WAIT_WARNING)
 {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(mPresContext,
@@ -2329,16 +2330,21 @@ nsRefreshDriver::IsRefreshObserver(nsARe
 
 void
 nsRefreshDriver::ScheduleViewManagerFlush()
 {
   NS_ASSERTION(mPresContext->IsRoot(),
                "Should only schedule view manager flush on root prescontexts");
   mViewManagerFlushIsPending = true;
   mHasScheduleFlush = true;
+  mOnlyAnimationFlush = false;
+  if (mPresContext->RestyleManager() && !mPresContext->RestyleManager()->OnlyThrottledAnimation()) {
+  } else {
+    mOnlyAnimationFlush = true;
+  }
   EnsureTimerStarted(eNeverAdjustTimer);
 }
 
 void
 nsRefreshDriver::ScheduleFrameRequestCallbacks(nsIDocument* aDocument)
 {
   NS_ASSERTION(mFrameRequestCallbackDocs.IndexOf(aDocument) ==
                mFrameRequestCallbackDocs.NoIndex &&
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -216,16 +216,19 @@ public:
     mViewManagerFlushIsPending = false;
   }
   bool ViewManagerFlushIsPending() {
     return mViewManagerFlushIsPending;
   }
   bool HasScheduleFlush() {
     return mHasScheduleFlush;
   }
+  bool OnlyAnimationFlush() {
+    return mOnlyAnimationFlush;
+  }
 
   /**
    * Add a document for which we have FrameRequestCallbacks
    */
   void ScheduleFrameRequestCallbacks(nsIDocument* aDocument);
 
   /**
    * Remove a document for which we have FrameRequestCallbacks
@@ -442,16 +445,17 @@ private:
   bool mThrottled;
   bool mNeedToRecomputeVisibility;
   bool mTestControllingRefreshes;
   bool mViewManagerFlushIsPending;
 
   // True if the view manager needs a flush. Layers-free mode uses this value
   // to know when to notify invalidation.
   bool mHasScheduleFlush;
+  bool mOnlyAnimationFlush;
 
   bool mInRefresh;
 
   // True if the refresh driver is suspended waiting for transaction
   // id's to be returned and shouldn't do any work during Tick().
   bool mWaitingForTransaction;
   // True if Tick() was skipped because of mWaitingForTransaction and
   // we should schedule a new Tick immediately when resumed instead
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -2461,16 +2461,19 @@ already_AddRefed<LayerManager> nsDisplay
     }
 
     aBuilder->SetIsCompositingCheap(temp);
     if (document && widgetTransaction) {
       TriggerPendingAnimations(document, layerManager->GetAnimationReadyTime());
     }
 
     if (presContext->RefreshDriver()->HasScheduleFlush()) {
+      if (XRE_IsContentProcess()) {
+        //printf_stderr("@@@NotifyInvalidation!\n");
+      }
       presContext->NotifyInvalidation(layerManager->GetLastTransactionId(), frame->GetRect());
     }
 
     return layerManager.forget();
   }
 
   NotifySubDocInvalidationFunc computeInvalidFunc =
     presContext->MayHavePaintEventListenerInSubDocument() ? nsPresContext::NotifySubDocInvalidation : 0;
--- a/layout/reftests/bugs/1078262-1.html
+++ b/layout/reftests/bugs/1078262-1.html
@@ -1,12 +1,12 @@
 <!-- Any copyright is dedicated to the Public Domain.
      http://creativecommons.org/publicdomain/zero/1.0/ -->
 <!DOCTYPE html>
-<html lang="en">
+<html lang="en" class="reftest-wait-except-animation">
 <meta charset="utf-8">
 <title>Test for bug 1078262</title>
 
 <style>
 #outer {
   perspective: 10000px;
   width: 200px;
   height: 4000px;
@@ -27,8 +27,17 @@
   from { transform: rotateX(10.01deg); }
   to { transform: rotateX(10.00deg); }
 }
 </style>
 
 <div id="outer">
   <div id="inner"></div>
 </div>
+<script>
+document.addEventListener('MozReftestInvalidate', () => {
+  requestAnimationFrame(() => {
+    requestAnimationFrame(() => {
+      document.documentElement.classList.remove('reftest-wait-except-animation');
+    });
+  });
+}, false);
+</script>
--- a/layout/reftests/css-animations/opacity-animation-in-fixed-opacity-parent.html
+++ b/layout/reftests/css-animations/opacity-animation-in-fixed-opacity-parent.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <head>
   <meta http-equiv="content-type" content="text/html; charset=UTF-8">
   <title>Testcase, bug 1395151</title>
   <style type="text/css">
 @keyframes test-anim {
   from { opacity : 1 }
   to   { opacity : 1 }
 }
@@ -34,15 +34,15 @@
 <body>
 <div class="ok">
   <div class="error"><span></span></div>
 </div>
 <script>
 document.addEventListener('MozReftestInvalidate', () => {
   requestAnimationFrame(() => {
     requestAnimationFrame(() => {
-      document.documentElement.classList.remove('reftest-wait');
+      document.documentElement.classList.remove('reftest-wait-except-animation');
     });
   });
 }, false);
 </script>
 </body>
 </html>
--- a/layout/reftests/css-animations/stacking-context-opacity-wins-over-transition.html
+++ b/layout/reftests/css-animations/stacking-context-opacity-wins-over-transition.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait">
+<html class="reftest-wait-except-animation">
 <title>
 Opacity animation winning over opacity transition creates a stacking context
 for correct style.
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
@@ -29,12 +29,12 @@ span {
 window.addEventListener("load", () => {
   var target = document.getElementById("test");
   getComputedStyle(target).opacity;
 
   // CSS animation wins over transitions, so transition won't be visible during
   // the CSS animation.
   target.style.opacity = 0;
   requestAnimationFrame(() => {
-    document.documentElement.classList.remove("reftest-wait");
+    document.documentElement.classList.remove("reftest-wait-except-animation");
   });
 });
 </script>
--- a/layout/reftests/css-animations/stacking-context-transform-wins-over-transition.html
+++ b/layout/reftests/css-animations/stacking-context-transform-wins-over-transition.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait">
+<html class="reftest-wait-except-animation">
 <title>
 Transform animation winning over transition creates a stacking context
 for correct style
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
@@ -29,12 +29,12 @@ span {
 window.addEventListener("load", () => {
   var target = document.getElementById("test");
   getComputedStyle(target).transform;
 
   // CSS animation wins over transition, so transition won't be visible during
   // the CSS animation.
   target.style.transform = "translateX(100px)";
   requestAnimationFrame(() => {
-    document.documentElement.classList.remove("reftest-wait");
+    document.documentElement.classList.remove("reftest-wait-except-animation");
   });
 });
 </script>
--- a/layout/reftests/css-transitions/stacking-context-opacity-wins-over-important-style.html
+++ b/layout/reftests/css-transitions/stacking-context-opacity-wins-over-important-style.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait">
+<html class="reftest-wait-except-animation">
 <title>
 Opacity transition winning over !important rule creates a stacking context
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -31,12 +31,12 @@ span {
 window.addEventListener("load", () => {
   var target = document.getElementById("test");
   getComputedStyle(target).opacity;
 
   target.style.setProperty("opacity", "0", "important");
   getComputedStyle(target).opacity;
 
   requestAnimationFrame(() => {
-    document.documentElement.classList.remove("reftest-wait");
+    document.documentElement.classList.remove("reftest-wait-except-animation");
   });
 });
 </script>
--- a/layout/reftests/css-transitions/stacking-context-transform-wins-over-important-style.html
+++ b/layout/reftests/css-transitions/stacking-context-transform-wins-over-important-style.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait">
+<html class="reftest-wait-except-animation">
 <title>
 Transform transition winning over !important rule creates a stacking context
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -31,12 +31,12 @@ span {
 window.addEventListener("load", () => {
   var target = document.getElementById("test");
   getComputedStyle(target).transform;
 
   target.style.setProperty("transform", "translateX(200px)", "important");
   getComputedStyle(target).transform;
 
   requestAnimationFrame(() => {
-    document.documentElement.classList.remove("reftest-wait");
+    document.documentElement.classList.remove("reftest-wait-except-animation");
   });
 });
 </script>
--- a/layout/reftests/web-animations/stacking-context-opacity-changing-effect.html
+++ b/layout/reftests/web-animations/stacking-context-opacity-changing-effect.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>
   Opacity animation creates a stacking context after changing effects
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -21,13 +21,13 @@ div {
 <div id="test"></div>
 <script>
   var elem = document.getElementById("test");
   var anim = elem.animate(null, { duration: 100000 });
   var newEffect = new KeyframeEffect(elem, { opacity: [1, 1] }, 100000);
   anim.ready.then(function() {
     anim.effect = newEffect;
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
 </html>
--- a/layout/reftests/web-animations/stacking-context-opacity-changing-keyframe.html
+++ b/layout/reftests/web-animations/stacking-context-opacity-changing-keyframe.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>Changing keyframes to opacity frames creates a stacking context</title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
   background: green;
   top: 50px;
@@ -17,12 +17,12 @@ span {
 <span></span>
 <div id="test"></div>
 <script>
   var anim = document.getElementById("test")
     .animate({ }, { duration: 100000 });
   anim.ready.then(function() {
     anim.effect.setKeyframes({ opacity: [1, 1] });
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
--- a/layout/reftests/web-animations/stacking-context-opacity-changing-target.html
+++ b/layout/reftests/web-animations/stacking-context-opacity-changing-target.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>
 Opacity animation creates a stacking context when changing its target
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -20,12 +20,12 @@ div {
 <div id="test"></div>
 <div id="another"></div>
 <script>
   var anim = document.getElementById("test")
     .animate({ opacity: [1, 1] }, { duration: 100000 });
   anim.ready.then(function() {
     anim.effect.target = document.getElementById("another");
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
--- a/layout/reftests/web-animations/stacking-context-transform-changing-display-property.html
+++ b/layout/reftests/web-animations/stacking-context-transform-changing-display-property.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>
 Transform animation creates a stacking context when changing its display style
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -18,14 +18,15 @@ div {
 }
 </style>
 <span></span>
 <div id="test"></div>
 <script>
   var anim = document.getElementById("test")
     .animate({ transform: ['none', 'none'] }, { duration: 100000 });
   anim.ready.then(function() {
+    dump("@@@change color black\n");
     anim.effect.target.style.display = 'block';
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
--- a/layout/reftests/web-animations/stacking-context-transform-changing-effect.html
+++ b/layout/reftests/web-animations/stacking-context-transform-changing-effect.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>
   Transform animation creates a stacking context after changing effects
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -23,13 +23,13 @@ div {
   var elem = document.getElementById("test");
   var anim = elem.animate(null, { duration: 100000 });
   var newEffect = new KeyframeEffect(elem,
                                      { transform: ['none', 'none']},
                                      100000);
   anim.ready.then(function() {
     anim.effect = newEffect;
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
 </html>
--- a/layout/reftests/web-animations/stacking-context-transform-changing-keyframe.html
+++ b/layout/reftests/web-animations/stacking-context-transform-changing-keyframe.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>Changing keyframes to transform frames creates a stacking context</title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
   background: green;
   top: 50px;
@@ -17,12 +17,12 @@ span {
 <span></span>
 <div id="test"></div>
 <script>
   var anim = document.getElementById("test")
     .animate({ }, { duration: 100000 });
   anim.ready.then(function() {
     anim.effect.setKeyframes({ transform: ['none', 'none'] });
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
--- a/layout/reftests/web-animations/stacking-context-transform-changing-target.html
+++ b/layout/reftests/web-animations/stacking-context-transform-changing-target.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait reftest-no-flush">
+<html class="reftest-wait-except-animation reftest-no-flush">
 <title>
 Transform animation creates a stacking context when changing its target
 </title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
@@ -20,12 +20,12 @@ div {
 <div id="test"></div>
 <div id="another"></div>
 <script>
   var anim = document.getElementById("test")
     .animate({ transform: ['none', 'none'] }, { duration: 100000 });
   anim.ready.then(function() {
     anim.effect.target = document.getElementById("another");
     requestAnimationFrame(function() {
-      document.documentElement.classList.remove("reftest-wait");
+      document.documentElement.classList.remove("reftest-wait-except-animation");
     });
   });
 </script>
--- a/layout/reftests/web-animations/stacking-context-transform-none-animation-before-appending-element.html
+++ b/layout/reftests/web-animations/stacking-context-transform-none-animation-before-appending-element.html
@@ -1,10 +1,10 @@
 <!DOCTYPE html>
-<html class="reftest-wait">
+<html class="reftest-wait-except-animation">
 <title>Transform animation whose target is not initially associated with any document creates a stacking context even if it has only 'transform:none' in its keyframe</title>
 <style>
 span {
   height: 100px;
   width: 100px;
   position: fixed;
   background: green;
   top: 50px;
@@ -16,11 +16,11 @@ div {
 </style>
 <span></span>
 <script>
   var div = document.createElement("div")
   var anim = div.animate({ transform: ['none', 'none'] },
                          { duration: 100000 });
   document.body.appendChild(div);
   anim.ready.then(function() {
-    document.documentElement.classList.remove("reftest-wait");
+    document.documentElement.classList.remove("reftest-wait-except-animation");
   });
 </script>
--- a/layout/style/nsStyleSet.cpp
+++ b/layout/style/nsStyleSet.cpp
@@ -1496,22 +1496,23 @@ nsStyleSet::RuleNodeWithReplacement(Elem
   MOZ_ASSERT(!aPseudoElement ==
              (aPseudoType >= CSSPseudoElementType::Count ||
               !(nsCSSPseudoElements::PseudoElementSupportsStyleAttribute(aPseudoType) ||
                 nsCSSPseudoElements::PseudoElementSupportsUserActionState(aPseudoType))),
              "should have aPseudoElement only for certain pseudo elements");
 
   // Remove the Force bits, which we don't need and which could confuse
   // the remainingReplacements code below.
-  aReplacements &= ~(eRestyle_Force | eRestyle_ForceDescendants);
+  aReplacements &= ~(eRestyle_Force | eRestyle_ForceDescendants | eRestyle_CSSThrottledAnimations);
 
   MOZ_ASSERT(!(aReplacements & ~(eRestyle_CSSTransitions |
                                  eRestyle_CSSAnimations |
                                  eRestyle_StyleAttribute |
-                                 eRestyle_StyleAttribute_Animations)),
+                                 eRestyle_StyleAttribute_Animations |
+                                 eRestyle_CSSThrottledAnimations)),
              "unexpected replacement bits");
 
   // This array can be hot and often grows to ~20 elements, so inline storage
   // is best.
   AutoTArray<RuleNodeInfo, 30> rules;
 
   const CascadeLevel* startingLevel = gCascadeLevels;
   nsRuleNode* startingNode = mRuleTree;
@@ -1755,17 +1756,17 @@ nsStyleSet::ResolveStyleWithReplacement(
     // because at this point the parameter is more than just the element
     // for animation; it's also used for the SetBodyTextColor call when
     // it's the body element.
     // However, we only want to set the flag to call UpdateAnimations
     // if we're dealing with a replacement (such as style attribute
     // replacement) that could lead to the animation property changing,
     // and we explicitly do NOT want to call UpdateAnimations when
     // we're trying to do an animation-only update.
-    if (aReplacements & ~(eRestyle_CSSTransitions | eRestyle_CSSAnimations)) {
+    if (aReplacements & ~(eRestyle_CSSTransitions | eRestyle_CSSAnimations | eRestyle_CSSThrottledAnimations)) {
       flags |= eDoAnimation;
     }
     elementForAnimation = aElement;
 #ifdef DEBUG
     {
       nsIFrame* styleFrame = nsLayoutUtils::GetStyleFrame(elementForAnimation);
       NS_ASSERTION(pseudoType == CSSPseudoElementType::NotPseudo ||
                    !styleFrame ||
--- a/layout/tools/reftest/reftest-content.js
+++ b/layout/tools/reftest/reftest-content.js
@@ -23,16 +23,18 @@ const BLANK_URL_FOR_CLEARING = "data:tex
 
 CU.import("resource://gre/modules/Timer.jsm");
 CU.import("chrome://reftest/content/AsyncSpellCheckTestHelper.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 
 var gBrowserIsRemote;
 var gIsWebRenderEnabled;
 var gHaveCanvasSnapshot = false;
+var gWaitForAnimationPaint = true;
+var gAnimationCounter = 0;
 // Plugin layers can be updated asynchronously, so to make sure that all
 // layer surfaces have the right content, we need to listen for explicit
 // "MozPaintWait" and "MozPaintWaitFinished" events that signal when it's OK
 // to take snapshots. We cannot take a snapshot while the number of
 // "MozPaintWait" events fired exceeds the number of "MozPaintWaitFinished"
 // events fired. We count the number of such excess events here. When
 // the counter reaches zero we call gExplicitPendingPaintsCompleteHook.
 var gExplicitPendingPaintCount = 0;
@@ -423,27 +425,51 @@ function resetDisplayportAndViewport() {
 
 function shouldWaitForExplicitPaintWaiters() {
     return gExplicitPendingPaintCount > 0;
 }
 
 function shouldWaitForPendingPaints() {
     // 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;
+    if (gWaitForAnimationPaint) {
+        return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending;
+    } else {
+        if (hasAnimationPending()) {
+            gAnimationCounter++;
+        } else {
+            gAnimationCounter = 0;
+        }
+        //return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending && gAnimationCounter < 2;
+        return gHaveCanvasSnapshot && windowUtils().isMozAfterPaintPending && !windowUtils().isMozAfterAnimationPending;
+    }
+}
+
+function hasAnimationPending() {
+    // 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().isMozAfterAnimationPending;
 }
 
 function shouldWaitForReftestWaitRemoval(contentRootElement) {
     // use getAttribute because className works differently in HTML and SVG
     return contentRootElement &&
            contentRootElement.hasAttribute('class') &&
            contentRootElement.getAttribute('class').split(/\s+/)
                              .indexOf("reftest-wait") != -1;
 }
 
+function shouldWaitForReftestWaitRemovalExceptAnimation(contentRootElement) {
+    // use getAttribute because className works differently in HTML and SVG
+    return contentRootElement &&
+           contentRootElement.hasAttribute('class') &&
+           contentRootElement.getAttribute('class').split(/\s+/)
+                             .indexOf("reftest-wait-except-animation") != -1;
+}
+
 function shouldSnapshotWholePage(contentRootElement) {
     // use getAttribute because className works differently in HTML and SVG
     return contentRootElement &&
            contentRootElement.hasAttribute('class') &&
            contentRootElement.getAttribute('class').split(/\s+/)
                              .indexOf("reftest-snapshot-all") != -1;
 }
 
@@ -537,16 +563,20 @@ function WaitForTestEnd(contentRootEleme
     function AfterPaintListener(event) {
         LogInfo("AfterPaintListener in " + event.target.document.location.href);
         if (event.target.document != currentDoc) {
             // ignore paint events for subframes or old documents in the window.
             // Invalidation in subframes will cause invalidation in the toplevel document anyway.
             return;
         }
 
+        //if (!gWaitForAnimationPaint && hasAnimationPending()) {
+          //  return;
+        //}
+
         SendUpdateCanvasForEvent(event, contentRootElement);
         // These events are fired immediately after a paint. Don't
         // confuse ourselves by firing synchronously if we triggered the
         // paint ourselves.
         setTimeout(MakeProgress, 0);
     }
 
     function AttrModifiedListener() {
@@ -586,32 +616,33 @@ function WaitForTestEnd(contentRootEleme
             LogInfo("MakeProgress: STATE_COMPLETED");
             return;
         }
 
         FlushRendering();
 
         switch (state) {
         case STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: {
-            LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT");
+            LogInfo("MakeProgress: STATE_WAITING_TO_FIRE_INVALIDATE_EVENT: " + shouldWaitForExplicitPaintWaiters() + ", " + shouldWaitForPendingPaints());
             if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
                 gFailureReason = "timed out waiting for pending paint count to reach zero";
                 if (shouldWaitForExplicitPaintWaiters()) {
                     gFailureReason += " (waiting for MozPaintWaitFinished)";
                     LogInfo("MakeProgress: waiting for MozPaintWaitFinished");
                 }
                 if (shouldWaitForPendingPaints()) {
                     gFailureReason += " (waiting for MozAfterPaint)";
                     LogInfo("MakeProgress: waiting for MozAfterPaint");
                 }
                 return;
             }
 
             state = STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL;
             var hasReftestWait = shouldWaitForReftestWaitRemoval(contentRootElement);
+            var hasReftestWaitExceptAnimation = shouldWaitForReftestWaitRemovalExceptAnimation(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);
@@ -635,29 +666,39 @@ function WaitForTestEnd(contentRootEleme
             if (hasReftestWait && !shouldWaitForReftestWaitRemoval(contentRootElement)) {
                 // MozReftestInvalidate handler removed reftest-wait.
                 // We expect something to have been invalidated...
                 FlushRendering();
                 if (!shouldWaitForPendingPaints() && !shouldWaitForExplicitPaintWaiters()) {
                     LogWarning("MozInvalidateEvent didn't invalidate");
                 }
             }
+
+            if (hasReftestWaitExceptAnimation && !shouldWaitForReftestWaitRemovalExceptAnimation(contentRootElement)) {
+                //@@@ todo
+            }
             // Try next state
             MakeProgress();
             return;
         }
 
         case STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL:
             LogInfo("MakeProgress: STATE_WAITING_FOR_REFTEST_WAIT_REMOVAL");
             if (shouldWaitForReftestWaitRemoval(contentRootElement)) {
                 gFailureReason = "timed out waiting for reftest-wait to be removed";
                 LogInfo("MakeProgress: waiting for reftest-wait to be removed");
                 return;
             }
 
+            if (shouldWaitForReftestWaitRemovalExceptAnimation(contentRootElement)) {
+                gFailureReason = "timed out waiting for reftest-wait-except-animation to be removed";
+                LogInfo("MakeProgress: waiting for reftest-wait-except-animation to be removed");
+                return;
+            }
+
             // Try next state
             state = STATE_WAITING_FOR_SPELL_CHECKS;
             MakeProgress();
             return;
 
         case STATE_WAITING_FOR_SPELL_CHECKS:
             LogInfo("MakeProgress: STATE_WAITING_FOR_SPELL_CHECKS");
             if (numPendingSpellChecks) {
@@ -695,17 +736,17 @@ 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");
+            LogInfo("MakeProgress: STATE_WAITING_TO_FINISH: " + shouldWaitForExplicitPaintWaiters() + ", " + shouldWaitForPendingPaints());
             if (shouldWaitForExplicitPaintWaiters() || shouldWaitForPendingPaints()) {
                 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()) {
@@ -854,17 +895,21 @@ function OnDocumentLoad(event)
             WaitForTestEnd(contentRootElement, inPrintMode, []);
         } else {
             CheckLayerAssertions(contentRootElement);
             CheckForProcessCrashExpectation(contentRootElement);
             RecordResult();
         }
     }
 
-    if (shouldWaitForReftestWaitRemoval(contentRootElement) ||
+    gAnimationCounter = 0;
+    gWaitForAnimationPaint = !shouldWaitForReftestWaitRemovalExceptAnimation(contentRootElement);
+
+    if (shouldWaitForReftestWaitRemovalExceptAnimation(contentRootElement) ||
+        shouldWaitForReftestWaitRemoval(contentRootElement) ||
         shouldWaitForExplicitPaintWaiters() ||
         spellCheckedElements.length) {
         // Go into reftest-wait mode immediately after painting has been
         // unsuppressed, after the onload event has finished dispatching.
         gFailureReason = "timed out waiting for test to complete (trying to get into WaitForTestEnd)";
         LogInfo("OnDocumentLoad triggering WaitForTestEnd");
         setTimeout(function () { WaitForTestEnd(contentRootElement, inPrintMode, spellCheckedElements); }, 0);
     } else {