Bug 1180125 part 1 - Queue and dispatch CSS animation events as a separate step; r=dbaron
authorBrian Birtles <birtles@gmail.com>
Wed, 29 Jul 2015 10:57:39 +0900
changeset 256724 f30ecbdba384066bf0e5b8ac7ded27c88160304c
parent 256723 94a07b627f21a086015e45941b33ea4f8280dead
child 256725 7c94690a852b2cb1632ef18f1b3d8c5d898296a3
push id29187
push usercbook@mozilla.com
push dateFri, 07 Aug 2015 11:13:32 +0000
treeherdermozilla-central@3e51753a099f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdbaron
bugs1180125
milestone42.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 1180125 part 1 - Queue and dispatch CSS animation events as a separate step; r=dbaron This patch prepares the way for script-generated events by making event dispatch a separate process that happens after sampling animations. This will allow us to sample animations from their associated timeline (removing the need for a further manager to tracker script-generated animations). Furthermore, once we sample animations from timelines the order in which they are sampled is likely to be more or less random so by making event dispatch at separate step, we have an opportunity to sort the events and dispatch in a consistent and sensible order. It also ensures that event callbacks will not be run until all animations (including transitions) have been updated ensuring they see a consistent view of timing properties. This patch only affects event handling for CSS animations. Transitions will be dealt with in a subsequent patch.
dom/animation/Animation.h
layout/base/nsPresShell.cpp
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/style/AnimationCommon.cpp
layout/style/AnimationCommon.h
layout/style/nsAnimationManager.cpp
layout/style/nsAnimationManager.h
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -136,17 +136,17 @@ public:
    * CSSAnimation::PauseFromJS so we leave it for now.
    */
   void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
 
   // Wrapper functions for Animation DOM methods when called from style.
 
   virtual void CancelFromStyle() { DoCancel(); }
 
-  void Tick();
+  virtual void Tick();
 
   /**
    * Set the time to use for starting or pausing a pending animation.
    *
    * Typically, when an animation is played, it does not start immediately but
    * is added to a table of pending animations on the document of its effect.
    * In the meantime it sets its hold time to the time from which playback
    * should begin.
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -1245,16 +1245,20 @@ PresShell::Destroy()
     NS_ASSERTION(mDocument->GetShell() == this, "Wrong shell?");
     mDocument->DeleteShell();
 
     if (mDocument->HasAnimationController()) {
       mDocument->GetAnimationController()->NotifyRefreshDriverDestroying(rd);
     }
   }
 
+  if (mPresContext) {
+    mPresContext->AnimationManager()->ClearEventQueue();
+  }
+
   // Revoke any pending events.  We need to do this and cancel pending reflows
   // before we destroy the frame manager, since apparently frame destruction
   // sometimes spins the event queue when plug-ins are involved(!).
   rd->RemoveLayoutFlushObserver(this);
   if (mHiddenInvalidationObserverRefreshDriver) {
     mHiddenInvalidationObserverRefreshDriver->RemovePresShellToInvalidateIfHidden(this);
   }
 
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -59,16 +59,17 @@
 #include "mozilla/ipc/PBackgroundChild.h"
 #include "nsIIPCBackgroundChildCreateCallback.h"
 #include "mozilla/layout/VsyncChild.h"
 #include "VsyncSource.h"
 #include "mozilla/VsyncDispatcher.h"
 #include "nsThreadUtils.h"
 #include "mozilla/unused.h"
 #include "mozilla/TimelineConsumers.h"
+#include "nsAnimationManager.h"
 
 #ifdef MOZ_NUWA_PROCESS
 #include "ipc/Nuwa.h"
 #endif
 
 using namespace mozilla;
 using namespace mozilla::widget;
 using namespace mozilla::ipc;
@@ -1484,16 +1485,50 @@ nsRefreshDriver::DispatchPendingEvents()
   // Swap out the current pending events
   nsTArray<PendingEvent> pendingEvents(Move(mPendingEvents));
   for (PendingEvent& event : pendingEvents) {
     bool dummy;
     event.mTarget->DispatchEvent(event.mEvent, &dummy);
   }
 }
 
+static bool
+DispatchAnimationEventsOnSubDocuments(nsIDocument* aDocument,
+                                      void* aRefreshDriver)
+{
+  nsIPresShell* shell = aDocument->GetShell();
+  if (!shell) {
+    return true;
+  }
+
+  nsPresContext* context = shell->GetPresContext();
+  if (!context || context->RefreshDriver() != aRefreshDriver) {
+    return true;
+  }
+
+  nsCOMPtr<nsIDocument> kungFuDeathGrip(aDocument);
+
+  context->AnimationManager()->DispatchEvents();
+  aDocument->EnumerateSubDocuments(DispatchAnimationEventsOnSubDocuments,
+                                   nullptr);
+
+  return true;
+}
+
+void
+nsRefreshDriver::DispatchAnimationEvents()
+{
+  if (!mPresContext) {
+    return;
+  }
+
+  nsIDocument* doc = mPresContext->Document();
+  DispatchAnimationEventsOnSubDocuments(doc, this);
+}
+
 void
 nsRefreshDriver::RunFrameRequestCallbacks(TimeStamp aNowTime)
 {
   // Grab all of our frame request callbacks up front.
   nsTArray<DocumentFrameCallbacks>
     frameRequestCallbacks(mFrameRequestCallbackDocs.Length() +
                           mThrottledFrameRequestCallbackDocs.Length());
 
@@ -1659,16 +1694,17 @@ nsRefreshDriver::Tick(int64_t aNowEpoch,
         StopTimer();
         return;
       }
     }
 
     if (i == 0) {
       // This is the Flush_Style case.
 
+      DispatchAnimationEvents();
       DispatchPendingEvents();
       RunFrameRequestCallbacks(aNowTime);
 
       if (mPresContext && mPresContext->GetPresShell()) {
         bool tracingStyleFlush = false;
         nsAutoTArray<nsIPresShell*, 16> observers;
         observers.AppendElements(mStyleFlushObservers);
         for (uint32_t j = observers.Length();
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -317,16 +317,17 @@ private:
     }
 
     mozilla::Maybe<mozilla::TimeStamp> mStartTime;
     RequestTable mEntries;
   };
   typedef nsClassHashtable<nsUint32HashKey, ImageStartData> ImageStartTable;
 
   void DispatchPendingEvents();
+  void DispatchAnimationEvents();
   void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
 
   void Tick(int64_t aNowEpoch, mozilla::TimeStamp aNowTime);
 
   enum EnsureTimerStartedFlags {
     eNone = 0,
     eAdjustingTimer = 1 << 0,
     eAllowTimeToGoBackwards = 1 << 1
--- a/layout/style/AnimationCommon.cpp
+++ b/layout/style/AnimationCommon.cpp
@@ -992,9 +992,29 @@ AnimationCollection::HasCurrentAnimation
         effect->HasAnimationOfProperties(aProperties, aPropertyCount)) {
       return true;
     }
   }
 
   return false;
 }
 
+nsPresContext*
+OwningElementRef::GetRenderedPresContext() const
+{
+  if (!mElement) {
+    return nullptr;
+  }
+
+  nsIDocument* doc = mElement->GetComposedDoc();
+  if (!doc) {
+    return nullptr;
+  }
+
+  nsIPresShell* shell = doc->GetShell();
+  if (!shell) {
+    return nullptr;
+  }
+
+  return shell->GetPresContext();
+}
+
 } // namespace mozilla
--- a/layout/style/AnimationCommon.h
+++ b/layout/style/AnimationCommon.h
@@ -502,16 +502,24 @@ public:
 
     return mPseudoType == nsCSSPseudoElements::ePseudo_NotPseudoElement ||
           (mPseudoType == nsCSSPseudoElements::ePseudo_before &&
            aOther.mPseudoType == nsCSSPseudoElements::ePseudo_after);
   }
 
   bool IsSet() const { return !!mElement; }
 
+  void GetElement(dom::Element*& aElement,
+                  nsCSSPseudoElements::Type& aPseudoType) const {
+    aElement = mElement;
+    aPseudoType = mPseudoType;
+  }
+
+  nsPresContext* GetRenderedPresContext() const;
+
 private:
   dom::Element* MOZ_NON_OWNING_REF mElement;
   nsCSSPseudoElements::Type        mPseudoType;
 };
 
 } // namespace mozilla
 
 #endif /* !defined(mozilla_css_AnimationCommon_h) */
--- a/layout/style/nsAnimationManager.cpp
+++ b/layout/style/nsAnimationManager.cpp
@@ -108,16 +108,23 @@ CSSAnimation::PauseFromStyle()
   // and then updated animation-play-state. It's an unusual case and there's
   // no obvious way to pass on the exception information so we just silently
   // fail for now.
   if (rv.Failed()) {
     NS_WARNING("Unexpected exception pausing animation - silently failing");
   }
 }
 
+void
+CSSAnimation::Tick()
+{
+  Animation::Tick();
+  QueueEvents();
+}
+
 bool
 CSSAnimation::HasLowerCompositeOrderThan(const Animation& aOther) const
 {
   // 0. Object-equality case
   if (&aOther == this) {
     return false;
   }
 
@@ -153,22 +160,50 @@ CSSAnimation::HasLowerCompositeOrderThan
     return mOwningElement.LessThan(otherAnimation->OwningElement());
   }
 
   // 4. (Same element and pseudo): Sort by position in animation-name
   return mSequenceNum < otherAnimation->mSequenceNum;
 }
 
 void
-CSSAnimation::QueueEvents(EventArray& aEventsToDispatch)
+CSSAnimation::QueueEvents()
 {
   if (!mEffect) {
     return;
   }
 
+  // CSS animations dispatch events at their owning element. This allows
+  // script to repurpose a CSS animation to target a different element,
+  // to use a group effect (which has no obvious "target element"), or
+  // to remove the animation effect altogether whilst still getting
+  // animation events.
+  //
+  // It does mean, however, that for a CSS animation that has no owning
+  // element (e.g. it was created using the CSSAnimation constructor or
+  // disassociated from CSS) no events are fired. If it becomes desirable
+  // for these animations to still fire events we should spec the concept
+  // of the "original owning element" or "event target" and allow script
+  // to set it when creating a CSSAnimation object.
+  if (!mOwningElement.IsSet()) {
+    return;
+  }
+
+  dom::Element* owningElement;
+  nsCSSPseudoElements::Type owningPseudoType;
+  mOwningElement.GetElement(owningElement, owningPseudoType);
+  MOZ_ASSERT(owningElement, "Owning element should be set");
+
+  // Get the nsAnimationManager so we can queue events on it
+  nsPresContext* presContext = mOwningElement.GetRenderedPresContext();
+  if (!presContext) {
+    return;
+  }
+  nsAnimationManager* manager = presContext->AnimationManager();
+
   ComputedTiming computedTiming = mEffect->GetComputedTiming();
 
   if (computedTiming.mPhase == ComputedTiming::AnimationPhase_Null) {
     return; // do nothing
   }
 
   // Note that script can change the start time, so we have to handle moving
   // backwards through the animation as well as forwards. An 'animationstart'
@@ -195,38 +230,34 @@ CSSAnimation::QueueEvents(EventArray& aE
   if (computedTiming.mPhase == ComputedTiming::AnimationPhase_Before) {
     mPreviousPhaseOrIteration = PREVIOUS_PHASE_BEFORE;
   } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase_Active) {
     mPreviousPhaseOrIteration = computedTiming.mCurrentIteration;
   } else if (computedTiming.mPhase == ComputedTiming::AnimationPhase_After) {
     mPreviousPhaseOrIteration = PREVIOUS_PHASE_AFTER;
   }
 
-  dom::Element* target;
-  nsCSSPseudoElements::Type targetPseudoType;
-  mEffect->GetTarget(target, targetPseudoType);
-
   uint32_t message;
 
   if (!wasActive && isActive) {
     message = NS_ANIMATION_START;
   } else if (wasActive && !isActive) {
     message = NS_ANIMATION_END;
   } else if (wasActive && isActive && !isSameIteration) {
     message = NS_ANIMATION_ITERATION;
   } else if (skippedActivePhase) {
     // First notifying for start of 0th iteration by appending an
     // 'animationstart':
     StickyTimeDuration elapsedTime =
       std::min(StickyTimeDuration(mEffect->InitialAdvance()),
                computedTiming.mActiveDuration);
-    AnimationEventInfo ei(target, mAnimationName, NS_ANIMATION_START,
+    AnimationEventInfo ei(owningElement, mAnimationName, NS_ANIMATION_START,
                           elapsedTime,
-                          PseudoTypeAsString(targetPseudoType));
-    aEventsToDispatch.AppendElement(ei);
+                          PseudoTypeAsString(owningPseudoType));
+    manager->QueueEvent(ei);
     // Then have the shared code below append an 'animationend':
     message = NS_ANIMATION_END;
   } else {
     return; // No events need to be sent
   }
 
   StickyTimeDuration elapsedTime;
 
@@ -236,19 +267,19 @@ CSSAnimation::QueueEvents(EventArray& aE
                                     computedTiming.mCurrentIteration;
     elapsedTime = StickyTimeDuration(std::max(iterationStart,
                                               mEffect->InitialAdvance()));
   } else {
     MOZ_ASSERT(message == NS_ANIMATION_END);
     elapsedTime = computedTiming.mActiveDuration;
   }
 
-  AnimationEventInfo ei(target, mAnimationName, message, elapsedTime,
-                        PseudoTypeAsString(targetPseudoType));
-  aEventsToDispatch.AppendElement(ei);
+  AnimationEventInfo ei(owningElement, mAnimationName, message, elapsedTime,
+                        PseudoTypeAsString(owningPseudoType));
+  manager->QueueEvent(ei);
 }
 
 CommonAnimationManager*
 CSSAnimation::GetAnimationManager() const
 {
   nsPresContext* context = GetPresContext();
   if (!context) {
     return nullptr;
@@ -266,36 +297,16 @@ CSSAnimation::PseudoTypeAsString(nsCSSPs
     case nsCSSPseudoElements::ePseudo_after:
       return NS_LITERAL_STRING("::after");
     default:
       return EmptyString();
   }
 }
 
 void
-nsAnimationManager::UpdateStyleAndEvents(AnimationCollection* aCollection,
-                                         TimeStamp aRefreshTime,
-                                         EnsureStyleRuleFlags aFlags)
-{
-  aCollection->EnsureStyleRuleFor(aRefreshTime, aFlags);
-  QueueEvents(aCollection, mPendingEvents);
-}
-
-void
-nsAnimationManager::QueueEvents(AnimationCollection* aCollection,
-                                EventArray& aEventsToDispatch)
-{
-  for (size_t animIdx = aCollection->mAnimations.Length(); animIdx-- != 0; ) {
-    CSSAnimation* anim = aCollection->mAnimations[animIdx]->AsCSSAnimation();
-    MOZ_ASSERT(anim, "Expected a collection of CSS Animations");
-    anim->QueueEvents(aEventsToDispatch);
-  }
-}
-
-void
 nsAnimationManager::MaybeUpdateCascadeResults(AnimationCollection* aCollection)
 {
   for (size_t animIdx = aCollection->mAnimations.Length(); animIdx-- != 0; ) {
     CSSAnimation* anim = aCollection->mAnimations[animIdx]->AsCSSAnimation();
     if (anim->IsInEffect() != anim->mInEffectForCascadeResults) {
       // Update our own cascade results.
       mozilla::dom::Element* element = aCollection->GetElementToRestyle();
       bool updatedCascadeResults = false;
@@ -498,29 +509,34 @@ nsAnimationManager::CheckAnimationRule(n
   // Cancel removed animations
   for (size_t newAnimIdx = newAnimations.Length(); newAnimIdx-- != 0; ) {
     newAnimations[newAnimIdx]->CancelFromStyle();
   }
 
   UpdateCascadeResults(aStyleContext, collection);
 
   TimeStamp refreshTime = mPresContext->RefreshDriver()->MostRecentRefresh();
-  UpdateStyleAndEvents(collection, refreshTime,
-                       EnsureStyleRule_IsNotThrottled);
+  collection->EnsureStyleRuleFor(refreshTime, EnsureStyleRule_IsNotThrottled);
   // We don't actually dispatch the mPendingEvents now.  We'll either
   // dispatch them the next time we get a refresh driver notification
   // or the next time somebody calls
   // nsPresShell::FlushPendingNotifications.
   if (!mPendingEvents.IsEmpty()) {
     mPresContext->Document()->SetNeedStyleFlush();
   }
 
   return GetAnimationRule(aElement, aStyleContext->GetPseudoType());
 }
 
+void
+nsAnimationManager::QueueEvent(AnimationEventInfo& aEventInfo)
+{
+  mPendingEvents.AppendElement(aEventInfo);
+}
+
 struct KeyframeData {
   float mKey;
   uint32_t mIndex; // store original order since sort algorithm is not stable
   nsCSSKeyframeRule *mRule;
 };
 
 struct KeyframeDataComparator {
   bool Equals(const KeyframeData& A, const KeyframeData& B) const {
@@ -963,40 +979,39 @@ nsAnimationManager::FlushAnimations(Flus
 
     collection->Tick();
     bool canThrottleTick = aFlags == Can_Throttle &&
       collection->CanPerformOnCompositorThread(
         AnimationCollection::CanAnimateFlags(0)) &&
       collection->CanThrottleAnimation(now);
 
     nsRefPtr<css::AnimValuesStyleRule> oldStyleRule = collection->mStyleRule;
-    UpdateStyleAndEvents(collection, now, canThrottleTick
-                                          ? EnsureStyleRule_IsThrottled
-                                          : EnsureStyleRule_IsNotThrottled);
+    collection->EnsureStyleRuleFor(now, canThrottleTick
+                                        ? EnsureStyleRule_IsThrottled
+                                        : EnsureStyleRule_IsNotThrottled);
     if (oldStyleRule != collection->mStyleRule) {
       collection->PostRestyleForAnimation(mPresContext);
     } else {
       didThrottle = true;
     }
   }
 
   if (didThrottle) {
     mPresContext->Document()->SetNeedStyleFlush();
   }
 
   MaybeStartOrStopObservingRefreshDriver();
-
-  DispatchEvents(); // may destroy us
 }
 
 void
 nsAnimationManager::DoDispatchEvents()
 {
   EventArray events;
   mPendingEvents.SwapElements(events);
+  // FIXME: Sort events here in timeline order, then document order
   for (uint32_t i = 0, i_end = events.Length(); i < i_end; ++i) {
     AnimationEventInfo &info = events[i];
     EventDispatcher::Dispatch(info.mElement, mPresContext, &info.mEvent);
 
     if (!mPresContext) {
       break;
     }
   }
--- a/layout/style/nsAnimationManager.h
+++ b/layout/style/nsAnimationManager.h
@@ -96,16 +96,18 @@ public:
   void PauseFromStyle();
   void CancelFromStyle() override
   {
     mOwningElement = OwningElementRef();
     Animation::CancelFromStyle();
     MOZ_ASSERT(mSequenceNum == kUnsequenced);
   }
 
+  void Tick() override;
+
   bool IsStylePaused() const { return mIsStylePaused; }
 
   bool HasLowerCompositeOrderThan(const Animation& aOther) const override;
   bool IsUsingCustomCompositeOrder() const override
   {
     return mOwningElement.IsSet();
   }
 
@@ -116,18 +118,16 @@ public:
   }
   void CopyAnimationIndex(const CSSAnimation& aOther)
   {
     MOZ_ASSERT(IsUsingCustomCompositeOrder() &&
                aOther.IsUsingCustomCompositeOrder());
     mSequenceNum = aOther.mSequenceNum;
   }
 
-  void QueueEvents(EventArray& aEventsToDispatch);
-
   // Returns the element or pseudo-element whose animation-name property
   // this CSSAnimation corresponds to (if any). This is used for determining
   // the relative composite order of animations generated from CSS markup.
   //
   // Typically this will be the same as the target element of the keyframe
   // effect associated with this animation. However, it can differ in the
   // following circumstances:
   //
@@ -160,16 +160,18 @@ public:
 protected:
   virtual ~CSSAnimation()
   {
     MOZ_ASSERT(!mOwningElement.IsSet(), "Owning element should be cleared "
                                         "before a CSS animation is destroyed");
   }
   virtual css::CommonAnimationManager* GetAnimationManager() const override;
 
+  void QueueEvents();
+
   static nsString PseudoTypeAsString(nsCSSPseudoElements::Type aPseudoType);
 
   nsString mAnimationName;
 
   // The (pseudo-)element whose computed animation-name refers to this
   // animation (if any).
   OwningElementRef mOwningElement;
 
@@ -241,22 +243,16 @@ class nsAnimationManager final
   : public mozilla::css::CommonAnimationManager
 {
 public:
   explicit nsAnimationManager(nsPresContext *aPresContext)
     : mozilla::css::CommonAnimationManager(aPresContext)
   {
   }
 
-  void UpdateStyleAndEvents(mozilla::AnimationCollection* aEA,
-                            mozilla::TimeStamp aRefreshTime,
-                            mozilla::EnsureStyleRuleFlags aFlags);
-  void QueueEvents(mozilla::AnimationCollection* aEA,
-                   mozilla::EventArray &aEventsToDispatch);
-
   void MaybeUpdateCascadeResults(mozilla::AnimationCollection* aCollection);
 
   // nsIStyleRuleProcessor (parts)
   virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf)
     const MOZ_MUST_OVERRIDE override;
   virtual size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
     const MOZ_MUST_OVERRIDE override;
 
@@ -275,30 +271,39 @@ public:
    *
    * aStyleContext may be a style context for aElement or for its
    * :before or :after pseudo-element.
    */
   nsIStyleRule* CheckAnimationRule(nsStyleContext* aStyleContext,
                                    mozilla::dom::Element* aElement);
 
   /**
+   * Add a pending event.
+   */
+  void QueueEvent(mozilla::AnimationEventInfo& aEventInfo);
+
+  /**
    * Dispatch any pending events.  We accumulate animationend and
    * animationiteration events only during refresh driver notifications
    * (and dispatch them at the end of such notifications), but we
    * accumulate animationstart events at other points when style
    * contexts are created.
    */
   void DispatchEvents() {
     // Fast-path the common case: no events
     if (!mPendingEvents.IsEmpty()) {
       DoDispatchEvents();
     }
   }
 
+  void ClearEventQueue() { mPendingEvents.Clear(); }
+
 protected:
+  virtual ~nsAnimationManager() {}
+
   virtual nsIAtom* GetAnimationsAtom() override {
     return nsGkAtoms::animationsProperty;
   }
   virtual nsIAtom* GetAnimationsBeforeAtom() override {
     return nsGkAtoms::animationsOfBeforeProperty;
   }
   virtual nsIAtom* GetAnimationsAfterAtom() override {
     return nsGkAtoms::animationsOfAfterProperty;