Bug 1190721 - Throttle animations that produce any transform change hint if the target element is out-of-view. r=birtles
authorHiroyuki Ikezoe <hikezoe@mozilla.com>
Tue, 31 Oct 2017 09:45:41 +0900
changeset 389217 fa94f7205173d34f23975c6af9cb95237b28c8b8
parent 389216 401840a241b9861cac205bd866ba24e69d11b7e2
child 389218 bf4fd832f591878b0c8838179f6f62e34fd573e3
push id32779
push userebalazs@mozilla.com
push dateTue, 31 Oct 2017 10:45:04 +0000
treeherdermozilla-central@a16cc603d061 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbirtles
bugs1190721
milestone58.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1190721 - Throttle animations that produce any transform change hint if the target element is out-of-view. r=birtles And unthrottle them on every 200ms just like we do for transform animations on the compositor. To unthrottle the transform animations properly, we need to update UpdateLastTransformSyncTime each time we compose the style for the animations instead of updating it when we send the transform animation to the compositor. That's because display item for transform is built even while we are throttling the transform animations for some reasons, so if we updated the last transform sync time there, the time will not match what it should be. MozReview-Commit-ID: GwMzJqUlzd2
dom/animation/KeyframeEffectReadOnly.cpp
dom/animation/test/mozilla/file_restyles.html
layout/base/nsChangeHint.h
layout/painting/nsDisplayList.cpp
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -728,16 +728,34 @@ KeyframeEffectReadOnly::ComposeStyle(
                  prop.mSegments.Length(),
                "out of array bounds");
 
     ComposeStyleRule(Forward<ComposeAnimationResult>(aComposeResult),
                      prop,
                      *segment,
                      computedTiming);
   }
+
+  // If the animation produces any transform change hint, we need to record the
+  // current time to unthrottle the animation periodically when the animation is
+  // being throttled because it's scrolled out of view.
+  if (mCumulativeChangeHint & (nsChangeHint_UpdatePostTransformOverflow |
+                               nsChangeHint_AddOrRemoveTransform |
+                               nsChangeHint_UpdateTransformLayer)) {
+    nsPresContext* presContext =
+      nsContentUtils::GetContextForContent(mTarget->mElement);
+    if (presContext) {
+      TimeStamp now = presContext->RefreshDriver()->MostRecentRefresh();
+      EffectSet* effectSet =
+        EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+      MOZ_ASSERT(effectSet, "ComposeStyle should only be called on an effect "
+                            "that is part of an effect set");
+      effectSet->UpdateLastTransformSyncTime(now);
+    }
+  }
 }
 
 bool
 KeyframeEffectReadOnly::IsRunningOnCompositor() const
 {
   // We consider animation is running on compositor if there is at least
   // one property running on compositor.
   // Animation.IsRunningOnCompotitor will return more fine grained
@@ -1387,18 +1405,27 @@ KeyframeEffectReadOnly::CanThrottle() co
     return true;
   }
 
   // Unless we are newly in-effect, we can throttle the animation if the
   // animation is paint only and the target frame is out of view or the document
   // is in background tabs.
   if (mInEffectOnLastAnimationTimingUpdate && CanIgnoreIfNotVisible()) {
     nsIPresShell* presShell = GetPresShell();
-    if ((presShell && !presShell->IsActive()) ||
-        frame->IsScrolledOutOfView()) {
+    if (presShell && !presShell->IsActive()) {
+      return true;
+    }
+    if (frame->IsScrolledOutOfView()) {
+      // If there are transform change hints, unthrottle the animation
+      // periodically since it might affect the overflow region.
+      if (mCumulativeChangeHint & (nsChangeHint_UpdatePostTransformOverflow |
+                                   nsChangeHint_AddOrRemoveTransform |
+                                   nsChangeHint_UpdateTransformLayer)) {
+        return CanThrottleTransformChanges(*frame);
+      }
       return true;
     }
   }
 
   // First we need to check layer generation and transform overflow
   // prior to the property.mIsRunningOnCompositor check because we should
   // occasionally unthrottle these animations even if the animations are
   // already running on compositor.
--- a/dom/animation/test/mozilla/file_restyles.html
+++ b/dom/animation/test/mozilla/file_restyles.html
@@ -295,16 +295,59 @@ waitForAllPaints(function() {
 
     is(markers.length, 0,
        'Animations running on the compositor for elements ' +
        'which are scrolled out should never cause restyles');
 
     await ensureElementRemoval(parentElement);
   });
 
+  add_task(
+    async function restyling_transform_animations_in_scrolled_out_element() {
+      if (!offscreenThrottlingEnabled) {
+        return;
+      }
+
+      await SpecialPowers.pushPrefEnv({ set: [["ui.showHideScrollbars", 1]] });
+
+      var parentElement = addDiv(null,
+        { style: 'overflow-y: scroll; height: 20px;' });
+      var div = addDiv(null,
+        { style: 'animation: rotate 100s; position: relative; top: 100px;' });
+      parentElement.appendChild(div);
+      var animation = div.getAnimations()[0];
+      var timeAtStart = document.timeline.currentTime;
+
+      ok(!animation.isRunningOnCompositor,
+         'The transform animation is not running on the compositor');
+
+      var markers;
+      var now;
+      while (true) {
+        markers = await observeStyling(1);
+        // Check restyle markers until 200ms is elapsed.
+        now = document.timeline.currentTime;
+        if ((now - timeAtStart) >= 200) {
+          break;
+        }
+
+        is(markers.length, 0,
+           'Transform animation running on the element which is scrolled out ' +
+           'should be throttled until 200ms is elapsed');
+      }
+
+      is(markers.length, 1,
+         'Transform animation running on the element which is scrolled out ' +
+         'should be unthrottled after around 200ms have elapsed. now: ' +
+         now + ' start time: ' + timeAtStart);
+
+      await ensureElementRemoval(parentElement);
+    }
+  );
+
   add_task(async function restyling_main_thread_animations_in_scrolled_out_element() {
     if (!offscreenThrottlingEnabled) {
       return;
     }
 
     /*
      On Android throttled animations are left behind on the main thread in some
      frames, We will fix this in bug 1247800.
--- a/layout/base/nsChangeHint.h
+++ b/layout/base/nsChangeHint.h
@@ -444,21 +444,23 @@ static_assert(!(nsChangeHint_Hints_Alway
 #define nsChangeHint_ReflowHintsForFloatAreaChange            \
   nsChangeHint(nsChangeHint_AllReflowHints &              \
                ~(nsChangeHint_ClearDescendantIntrinsics | \
                  nsChangeHint_NeedDirtyReflow))
 
 #define NS_STYLE_HINT_REFLOW \
   nsChangeHint(NS_STYLE_HINT_VISUAL | nsChangeHint_AllReflowHints)
 
-#define nsChangeHint_Hints_CanIgnoreIfNotVisible   \
-  nsChangeHint(NS_STYLE_HINT_VISUAL |              \
-               nsChangeHint_NeutralChange |        \
-               nsChangeHint_UpdateOpacityLayer |   \
-               nsChangeHint_UpdateTransformLayer | \
+#define nsChangeHint_Hints_CanIgnoreIfNotVisible           \
+  nsChangeHint(NS_STYLE_HINT_VISUAL |                      \
+               nsChangeHint_NeutralChange |                \
+               nsChangeHint_UpdateOpacityLayer |           \
+               nsChangeHint_AddOrRemoveTransform |         \
+               nsChangeHint_UpdatePostTransformOverflow  | \
+               nsChangeHint_UpdateTransformLayer |         \
                nsChangeHint_UpdateUsesOpacity)
 
 // NB: Once we drop support for the old style system, this logic should be
 // inlined in the Servo style system to eliminate the FFI call.
 inline nsChangeHint NS_HintsNotHandledForDescendantsIn(nsChangeHint aChangeHint) {
   nsChangeHint result =
     aChangeHint & nsChangeHint_Hints_NeverHandledForDescendants;
 
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -679,20 +679,16 @@ AddAnimationsForProperty(nsIFrame* aFram
   } else if (aProperty == eCSSProperty_opacity) {
     data = null_t();
   }
 
   MOZ_ASSERT(nsCSSProps::PropHasFlags(aProperty,
                                       CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
              "inconsistent property flags");
 
-  EffectSet* effects = EffectSet::GetEffectSet(aFrame);
-  MOZ_ASSERT(effects);
-
-  bool sentAnimations = false;
   // Add from first to last (since last overrides)
   for (size_t animIdx = 0; animIdx < compositorAnimations.Length(); animIdx++) {
     dom::Animation* anim = compositorAnimations[animIdx];
     if (!anim->IsRelevant()) {
       continue;
     }
 
     dom::KeyframeEffectReadOnly* keyframeEffect =
@@ -706,17 +702,17 @@ AddAnimationsForProperty(nsIFrame* aFram
     }
 
     // Note that if the property is overridden by !important rules,
     // GetEffectiveAnimationOfProperty returns null instead.
     // This is what we want, since if we have animations overridden by
     // !important rules, we don't want to send them to the compositor.
     MOZ_ASSERT(anim->CascadeLevel() !=
                  EffectCompositor::CascadeLevel::Animations ||
-               !effects->PropertiesWithImportantRules()
+               !EffectSet::GetEffectSet(aFrame)->PropertiesWithImportantRules()
                   .HasProperty(aProperty),
                "GetEffectiveAnimationOfProperty already tested the property "
                "is not overridden by !important rules");
 
     // Don't add animations that are pending if their timeline does not
     // track wallclock time. This is because any pending animations on layers
     // will have their start time updated with the current wallclock time.
     // If we can't convert that wallclock time back to an equivalent timeline
@@ -729,22 +725,16 @@ AddAnimationsForProperty(nsIFrame* aFram
     if (anim->PlayState() == AnimationPlayState::Pending &&
         (anim->GetTimeline() &&
          !anim->GetTimeline()->TracksWallclockTime())) {
       continue;
     }
 
     AddAnimationForProperty(aFrame, *property, anim, aAnimationInfo, data, aPending);
     keyframeEffect->SetIsRunningOnCompositor(aProperty, true);
-    sentAnimations = true;
-  }
-
-  if (sentAnimations && aProperty == eCSSProperty_transform) {
-    TimeStamp now = aFrame->PresContext()->RefreshDriver()->MostRecentRefresh();
-    effects->UpdateLastTransformSyncTime(now);
   }
 }
 
 static bool
 GenerateAndPushTextMask(nsIFrame* aFrame, gfxContext* aContext,
                         const nsRect& aFillRect, nsDisplayListBuilder* aBuilder)
 {
   if (aBuilder->IsForGenerateGlyphMask() ||