Bug 1167519 - Part 1: Calculate plausible starting value on compositor with TimeStamp::Now() when replacing an old transtion. r?birtles draft
authorHiroyuki Ikezoe <hiikezoe@mozilla-japan.org>
Wed, 25 May 2016 05:51:57 +0900
changeset 370486 929998811f833c0928ff00b9548a210542a904ee
parent 370474 5511d54a3f172c1d68f98cc55dce4de1d0ba1b51
child 370487 a52d0f8f06345266bfe649579c454a0c1bf47aef
child 370583 ae0558b177770d720bbc796b6bfcc6bdeac3c432
push id19080
push userbmo:hiikezoe@mozilla-japan.org
push dateTue, 24 May 2016 21:45:50 +0000
reviewersbirtles
bugs1167519, 1273834
milestone49.0a1
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
@@ -385,16 +385,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