Bug 1167519 - Part 1: Calculate plausible starting value on compositor with TimeStamp::Now() when replacing an old transtion. r=birtles
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Wed, 25 May 2016 05:51:57 +0900
changeset 337891 72c16a3d0b0a13a4f05929a326d5fe859a9a3108
parent 337890 8ea3030332a90003b4a2dff0daa8329cf2109792
child 337892 05ebd1f677ed3454efc7bb939767dbc45c826792
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbirtles
bugs1167519, 1273834
milestone49.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 1167519 - Part 1: Calculate plausible starting value on compositor with TimeStamp::Now() when replacing an old transtion. r=birtles Transitions on the compositor sometimes go further ahead while the main-thread is busy. When the transition on the compositor is replaced by a new one, until now we calculate the current position of the old one with the most recent refresh time. But if the replace is done on a busy frame, the calculated position will be far from the real position on the compositor. As a result, we can see jumping transitions after busy frames. To mitigate this issue, we should calculate a plausible current position of the old one with the current time just before sending the new transition to the compositor, i.e., after all JS callback works have done. The plausible value is stored into KeyframeEffectReadOnly::mProperties, not into mKeyframes. If we store the value into keyframes, KeyframeEffectReadOnly::GetKeyframes() will return a different values after busy frames. Note that with this patch getComputedStyle() does not return the plausible value because this patch does not affect styling process. getComputedStyle issue will be fixed in bug 1273834. MozReview-Commit-ID: B85kIx6qeyy
layout/base/nsDisplayList.cpp
layout/style/nsTransitionManager.cpp
layout/style/nsTransitionManager.h
--- a/layout/base/nsDisplayList.cpp
+++ b/layout/base/nsDisplayList.cpp
@@ -384,16 +384,39 @@ AddAnimationForProperty(nsIFrame* aFrame
   TransformReferenceBox refBox(aFrame);
 
   layers::Animation* animation =
     aPending ?
     aLayer->AddAnimationForNextTransaction() :
     aLayer->AddAnimation();
 
   const TimingParams& timing = aAnimation->GetEffect()->SpecifiedTiming();
+
+  // If we are starting a new transition that replaces an existing transition
+  // running on the compositor, it is possible that the animation on the
+  // compositor will have advanced ahead of the main thread. If we use as
+  // the starting point of the new transition, the current value of the
+  // replaced transition as calculated on the main thread using the refresh
+  // driver time, the new transition will jump when it starts. Instead, we
+  // re-calculate the starting point of the new transition by applying the
+  // current TimeStamp to the parameters of the replaced transition.
+  //
+  // We need to do this here, rather than when we generate the new transition,
+  // since after generating the new transition other requestAnimationFrame
+  // callbacks may run that introduce further lag between the main thread and
+  // the compositor.
+  if (aAnimation->AsCSSTransition() &&
+      aAnimation->GetEffect()) {
+    MOZ_ASSERT(aAnimation->GetEffect()->AsTransition(),
+               "CSSTransition' effect should be an ElementPropertyTransition "
+               "until we fix bug 1049975");
+    aAnimation->GetEffect()->AsTransition()->
+      UpdateStartValueFromReplacedTransition();
+  }
+
   const ComputedTiming computedTiming =
     aAnimation->GetEffect()->GetComputedTiming();
   Nullable<TimeDuration> startTime = aAnimation->GetCurrentOrPendingStartTime();
   animation->startTime() = startTime.IsNull()
                            ? TimeStamp()
                            : aAnimation->AnimationTimeToTimeStamp(
                               StickyTimeDuration(timing.mDelay));
   animation->initialCurrentTime() = aAnimation->GetCurrentTime().Value()
--- a/layout/style/nsTransitionManager.cpp
+++ b/layout/style/nsTransitionManager.cpp
@@ -65,16 +65,57 @@ ElementPropertyTransition::CurrentValueP
              "Got a null progress for a fill mode of 'both'");
   MOZ_ASSERT(mKeyframes.Length() == 2,
              "Should have two animation keyframes for a transition");
   return ComputedTimingFunction::GetPortion(mKeyframes[0].mTimingFunction,
                                             computedTiming.mProgress.Value(),
                                             computedTiming.mBeforeFlag);
 }
 
+void
+ElementPropertyTransition::UpdateStartValueFromReplacedTransition()
+{
+  if (!mReplacedTransition) {
+    return;
+  }
+  MOZ_ASSERT(nsCSSProps::PropHasFlags(TransitionProperty(),
+                                      CSS_PROPERTY_CAN_ANIMATE_ON_COMPOSITOR),
+             "The transition property should be able to be run on the "
+             "compositor");
+  MOZ_ASSERT(mTarget && mTarget->mElement->OwnerDoc(),
+             "We should have a valid document at this moment");
+
+  dom::DocumentTimeline* timeline = mTarget->mElement->OwnerDoc()->Timeline();
+  ComputedTiming computedTiming = GetComputedTimingAt(
+    dom::CSSTransition::GetCurrentTimeAt(*timeline,
+                                         TimeStamp::Now(),
+                                         mReplacedTransition->mStartTime,
+                                         mReplacedTransition->mPlaybackRate),
+    mReplacedTransition->mTiming);
+
+  if (!computedTiming.mProgress.IsNull()) {
+    double valuePosition =
+      ComputedTimingFunction::GetPortion(mReplacedTransition->mTimingFunction,
+                                         computedTiming.mProgress.Value(),
+                                         computedTiming.mBeforeFlag);
+    StyleAnimationValue startValue;
+    if (StyleAnimationValue::Interpolate(mProperties[0].mProperty,
+                                         mReplacedTransition->mFromValue,
+                                         mReplacedTransition->mToValue,
+                                         valuePosition, startValue)) {
+      MOZ_ASSERT(mProperties.Length() == 1 &&
+                 mProperties[0].mSegments.Length() == 1,
+                 "The transition should have one property and one segment");
+      mProperties[0].mSegments[0].mFromValue = Move(startValue);
+    }
+  }
+
+  mReplacedTransition.reset();
+}
+
 ////////////////////////// CSSTransition ////////////////////////////
 
 JSObject*
 CSSTransition::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
 {
   return dom::CSSTransitionBinding::Wrap(aCx, this, aGivenProto);
 }
 
@@ -199,16 +240,33 @@ CSSTransition::HasLowerCompositeOrderTha
     return mAnimationIndex < aOther.mAnimationIndex;
   }
 
   // 3. (Same transition generation): Sort by transition property
   return nsCSSProps::GetStringValue(TransitionProperty()) <
          nsCSSProps::GetStringValue(aOther.TransitionProperty());
 }
 
+/* static */ Nullable<TimeDuration>
+CSSTransition::GetCurrentTimeAt(const DocumentTimeline& aTimeline,
+                                const TimeStamp& aBaseTime,
+                                const TimeDuration& aStartTime,
+                                double aPlaybackRate)
+{
+  Nullable<TimeDuration> result;
+
+  Nullable<TimeDuration> timelineTime = aTimeline.ToTimelineTime(aBaseTime);
+  if (!timelineTime.IsNull()) {
+    result.SetValue((timelineTime.Value() - aStartTime)
+                      .MultDouble(aPlaybackRate));
+  }
+
+  return result;
+}
+
 ////////////////////////// nsTransitionManager ////////////////////////////
 
 NS_IMPL_CYCLE_COLLECTION(nsTransitionManager, mEventDispatcher)
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsTransitionManager, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsTransitionManager, Release)
 
 void
@@ -741,16 +799,39 @@ nsTransitionManager::ConsiderStartingTra
       i == currentIndex ||
       (animations[i]->GetEffect() &&
        animations[i]->GetEffect()->AsTransition()->TransitionProperty()
          != aProperty),
       "duplicate transitions for property");
   }
 #endif
   if (haveCurrentTransition) {
+    // If this new transition is replacing an existing transition that is running
+    // on the compositor, we store select parameters from the replaced transition
+    // so that later, once all scripts have run, we can update the start value
+    // of the transition using TimeStamp::Now(). This allows us to avoid a
+    // large jump when starting a new transition when the main thread lags behind
+    // the compositor.
+    if (oldPT->IsCurrent() &&
+        oldPT->IsRunningOnCompositor() &&
+        !oldPT->GetAnimation()->GetStartTime().IsNull() &&
+        timeline == oldPT->GetAnimation()->GetTimeline()) {
+      const AnimationPropertySegment& segment =
+        oldPT->Properties()[0].mSegments[0];
+      pt->mReplacedTransition.emplace(
+        ElementPropertyTransition::ReplacedTransitionProperties({
+          oldPT->GetAnimation()->GetStartTime().Value(),
+          oldPT->GetAnimation()->PlaybackRate(),
+          oldPT->SpecifiedTiming(),
+          segment.mTimingFunction,
+          segment.mFromValue,
+          segment.mToValue
+        })
+      );
+    }
     animations[currentIndex]->CancelFromStyle();
     oldPT = nullptr; // Clear pointer so it doesn't dangle
     animations[currentIndex] = animation;
   } else {
     if (!animations.AppendElement(animation)) {
       NS_WARNING("out of memory");
       return;
     }
--- a/layout/style/nsTransitionManager.h
+++ b/layout/style/nsTransitionManager.h
@@ -90,16 +90,30 @@ struct ElementPropertyTransition : publi
   // in again when the transition is back to 2px, the mReversePortion
   // for the third transition (from 0px/2px to 10px) will be 0.8.
   double mReversePortion;
 
   // Compute the portion of the *value* space that we should be through
   // at the current time.  (The input to the transition timing function
   // has time units, the output has value units.)
   double CurrentValuePortion() const;
+
+  // For a new transition interrupting an existing transition on the
+  // compositor, update the start value to match the value of the replaced
+  // transitions at the current time.
+  void UpdateStartValueFromReplacedTransition();
+
+  struct ReplacedTransitionProperties {
+    TimeDuration mStartTime;
+    double mPlaybackRate;
+    TimingParams mTiming;
+    Maybe<ComputedTimingFunction> mTimingFunction;
+    StyleAnimationValue mFromValue, mToValue;
+  };
+  Maybe<ReplacedTransitionProperties> mReplacedTransition;
 };
 
 namespace dom {
 
 class CSSTransition final : public Animation
 {
 public:
  explicit CSSTransition(nsIGlobalObject* aGlobal)
@@ -179,16 +193,27 @@ public:
   void SetOwningElement(const OwningElementRef& aElement)
   {
     mOwningElement = aElement;
   }
   // True for transitions that are generated from CSS markup and continue to
   // reflect changes to that markup.
   bool IsTiedToMarkup() const { return mOwningElement.IsSet(); }
 
+  // Return the animation current time based on a given TimeStamp, a given
+  // start time and a given playbackRate on a given timeline.  This is useful
+  // when we estimate the current animated value running on the compositor
+  // because the animation on the compositor may be running ahead while
+  // main-thread is busy.
+  static Nullable<TimeDuration> GetCurrentTimeAt(
+      const DocumentTimeline& aTimeline,
+      const TimeStamp& aBaseTime,
+      const TimeDuration& aStartTime,
+      double aPlaybackRate);
+
 protected:
   virtual ~CSSTransition()
   {
     MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared "
                                         "before a CSS transition is destroyed");
   }
 
   // Animation overrides