Bug 1253476 - Add remove events; r=boris,bzbarsky
authorBrian Birtles <birtles@gmail.com>
Mon, 20 May 2019 05:48:29 +0000
changeset 474488 8642d6a60ad9ff41d3fa5e2f04bc957bd2d6ed81
parent 474487 f0273e0339381e099a35aef2ae6a65fe3149e415
child 474489 25238a996da405bd6464af8d01f7a89edb392eca
push id36040
push userrgurzau@mozilla.com
push dateMon, 20 May 2019 13:43:21 +0000
treeherdermozilla-central@319a369ccde4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersboris, bzbarsky
bugs1253476
milestone68.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 1253476 - Add remove events; r=boris,bzbarsky This patch introduces the machinery for dispatching remove events but does not actually cause removing to do anything to the output of the animation beyond updating its replaceState member. The expected behavior is defined in: https://drafts.csswg.org/web-animations-1/#removing-replaced-animations And the corresponding IDL members are defined in: https://drafts.csswg.org/web-animations-1/#animation https://drafts.csswg.org/web-animations-1/#enumdef-animationreplacestate Tests for these events are added in the next patch in this series. Differential Revision: https://phabricator.services.mozilla.com/D30322
dom/animation/Animation.cpp
dom/animation/Animation.h
dom/animation/EffectCompositor.cpp
dom/animation/EffectCompositor.h
dom/animation/KeyframeEffect.cpp
dom/webidl/Animation.webidl
layout/base/nsRefreshDriver.cpp
modules/libpref/init/StaticPrefList.h
xpcom/ds/StaticAtoms.py
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -163,16 +163,18 @@ void Animation::SetEffectNoUpdate(Animat
     // AutoMutationBatchForAnimation.
     if (wasRelevant && mIsRelevant) {
       nsNodeUtils::AnimationChanged(this);
     }
 
     ReschedulePendingTasks();
   }
 
+  MaybeScheduleReplacementCheck();
+
   UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
 }
 
 void Animation::SetTimeline(AnimationTimeline* aTimeline) {
   SetTimelineNoUpdate(aTimeline);
   PostUpdate();
 }
 
@@ -647,16 +649,23 @@ void Animation::Tick() {
   if (IsPossiblyOrphanedPendingAnimation()) {
     MOZ_ASSERT(mTimeline && !mTimeline->GetCurrentTimeAsDuration().IsNull(),
                "Orphaned pending animations should have an active timeline");
     FinishPendingAt(mTimeline->GetCurrentTimeAsDuration().Value());
   }
 
   UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);
 
+  // Check for changes to whether or not this animation is replaceable.
+  bool isReplaceable = IsReplaceable();
+  if (isReplaceable && !mWasReplaceableAtLastTick) {
+    ScheduleReplacementCheck();
+  }
+  mWasReplaceableAtLastTick = isReplaceable;
+
   if (!mEffect) {
     return;
   }
 
   // Update layers if we are newly finished.
   KeyframeEffect* keyframeEffect = mEffect->AsKeyframeEffect();
   if (keyframeEffect && !keyframeEffect->Properties().IsEmpty() &&
       !mFinishedAtLastComposeStyle &&
@@ -842,16 +851,125 @@ void Animation::UpdateRelevance() {
   // Notify animation observers.
   if (wasRelevant && !mIsRelevant) {
     nsNodeUtils::AnimationRemoved(this);
   } else if (!wasRelevant && mIsRelevant) {
     nsNodeUtils::AnimationAdded(this);
   }
 }
 
+template <class T>
+bool IsMarkupAnimation(T* aAnimation) {
+  return aAnimation && aAnimation->IsTiedToMarkup();
+}
+
+// https://drafts.csswg.org/web-animations/#replaceable-animation
+bool Animation::IsReplaceable() const {
+  // We never replace CSS animations or CSS transitions since they are managed
+  // by CSS.
+  if (IsMarkupAnimation(AsCSSAnimation()) ||
+      IsMarkupAnimation(AsCSSTransition())) {
+    return false;
+  }
+
+  // Only finished animations can be replaced.
+  if (PlayState() != AnimationPlayState::Finished) {
+    return false;
+  }
+
+  // Already removed animations cannot be replaced.
+  if (ReplaceState() == AnimationReplaceState::Removed) {
+    return false;
+  }
+
+  // We can only replace an animation if we know that, uninterfered, it would
+  // never start playing again. That excludes any animations on timelines that
+  // aren't monotonically increasing.
+  //
+  // If we don't have any timeline at all, then we can't be in the finished
+  // state (since we need both a resolved start time and current time for that)
+  // and will have already returned false above.
+  //
+  // (However, if it ever does become possible to be finished without a timeline
+  // then we will want to return false here since it probably suggests an
+  // animation being driven directly by script, in which case we can't assume
+  // anything about how they will behave.)
+  if (!GetTimeline() || !GetTimeline()->TracksWallclockTime()) {
+    return false;
+  }
+
+  // If the animation doesn't have an effect then we can't determine if it is
+  // filling or not so just leave it alone.
+  if (!GetEffect()) {
+    return false;
+  }
+
+  // At the time of writing we only know about KeyframeEffects. If we introduce
+  // other types of effects we will need to decide if they are replaceable or
+  // not.
+  MOZ_ASSERT(GetEffect()->AsKeyframeEffect(),
+             "Effect should be a keyframe effect");
+
+  // We only replace animations that are filling.
+  if (GetEffect()->GetComputedTiming().mProgress.IsNull()) {
+    return false;
+  }
+
+  // We should only replace animations with a target element (since otherwise
+  // what other effects would we consider when determining if they are covered
+  // or not?).
+  if (!GetEffect()->AsKeyframeEffect()->GetTarget()) {
+    return false;
+  }
+
+  return true;
+}
+
+bool Animation::IsRemovable() const {
+  return ReplaceState() == AnimationReplaceState::Active && IsReplaceable();
+}
+
+void Animation::ScheduleReplacementCheck() {
+  MOZ_ASSERT(
+      IsReplaceable(),
+      "Should only schedule a replacement check for a replaceable animation");
+
+  // If IsReplaceable() is true, the following should also hold
+  MOZ_ASSERT(GetEffect());
+  MOZ_ASSERT(GetEffect()->AsKeyframeEffect());
+  MOZ_ASSERT(GetEffect()->AsKeyframeEffect()->GetTarget());
+
+  Maybe<NonOwningAnimationTarget> target =
+      GetEffect()->AsKeyframeEffect()->GetTarget();
+
+  nsPresContext* presContext =
+      nsContentUtils::GetContextForContent(target->mElement);
+  if (presContext) {
+    presContext->EffectCompositor()->NoteElementForReducing(*target);
+  }
+}
+
+void Animation::MaybeScheduleReplacementCheck() {
+  if (!IsReplaceable()) {
+    return;
+  }
+
+  ScheduleReplacementCheck();
+}
+
+void Animation::Remove() {
+  MOZ_ASSERT(IsRemovable(),
+             "Should not be trying to remove an effect that is not removable");
+
+  mReplaceState = AnimationReplaceState::Removed;
+
+  QueuePlaybackEvent(NS_LITERAL_STRING("remove"),
+                     GetTimelineCurrentTimeAsTimeStamp());
+}
+
 bool Animation::HasLowerCompositeOrderThan(const Animation& aOther) const {
   // 0. Object-equality case
   if (&aOther == this) {
     return false;
   }
 
   // 1. CSS Transitions sort lowest
   {
@@ -984,21 +1102,37 @@ void Animation::ComposeStyle(RawServoAni
 
   MOZ_ASSERT(
       pending == Pending(),
       "Pending state should not change during the course of compositing");
 }
 
 void Animation::NotifyEffectTimingUpdated() {
   MOZ_ASSERT(mEffect,
-             "We should only update timing effect when we have a target "
+             "We should only update effect timing when we have a target "
              "effect");
   UpdateTiming(Animation::SeekFlag::NoSeek, Animation::SyncNotifyFlag::Async);
 }
 
+void Animation::NotifyEffectPropertiesUpdated() {
+  MOZ_ASSERT(mEffect,
+             "We should only update effect properties when we have a target "
+             "effect");
+
+  MaybeScheduleReplacementCheck();
+}
+
+void Animation::NotifyEffectTargetUpdated() {
+  MOZ_ASSERT(mEffect,
+             "We should only update the effect target when we have a target "
+             "effect");
+
+  MaybeScheduleReplacementCheck();
+}
+
 void Animation::NotifyGeometricAnimationsStartingThisFrame() {
   if (!IsNewlyStarted() || !mEffect) {
     return;
   }
 
   mSyncWithGeometricAnimations = true;
 }
 
@@ -1498,17 +1632,17 @@ void Animation::QueuePlaybackEvent(const
 
   nsPresContext* presContext = doc->GetPresContext();
   if (!presContext) {
     return;
   }
 
   AnimationPlaybackEventInit init;
 
-  if (aName.EqualsLiteral("finish")) {
+  if (aName.EqualsLiteral("finish") || aName.EqualsLiteral("remove")) {
     init.mCurrentTime = GetCurrentTimeAsDouble();
   }
   if (mTimeline) {
     init.mTimelineTime = mTimeline->GetCurrentTimeAsDouble();
   }
 
   RefPtr<AnimationPlaybackEvent> event =
       AnimationPlaybackEvent::Constructor(this, aName, init);
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -107,22 +107,24 @@ class Animation : public DOMEventTargetH
   double PlaybackRate() const { return mPlaybackRate; }
   void SetPlaybackRate(double aPlaybackRate);
 
   AnimationPlayState PlayState() const;
   virtual AnimationPlayState PlayStateFromJS() const { return PlayState(); }
 
   bool Pending() const { return mPendingState != PendingState::NotPending; }
   virtual bool PendingFromJS() const { return Pending(); }
+  AnimationReplaceState ReplaceState() const { return mReplaceState; }
 
   virtual Promise* GetReady(ErrorResult& aRv);
   Promise* GetFinished(ErrorResult& aRv);
 
   IMPL_EVENT_HANDLER(finish);
   IMPL_EVENT_HANDLER(cancel);
+  IMPL_EVENT_HANDLER(remove);
 
   void Cancel(PostRestyleMode aPostRestyle = PostRestyleMode::IfNeeded);
 
   void Finish(ErrorResult& aRv);
 
   virtual void Play(ErrorResult& aRv, LimitBehavior aLimitBehavior);
   virtual void PlayFromJS(ErrorResult& aRv) {
     Play(aRv, LimitBehavior::AutoRewind);
@@ -316,16 +318,35 @@ class Animation : public DOMEventTargetH
 
   bool ShouldBeSynchronizedWithMainThread(
       const nsCSSPropertyIDSet& aPropertySet, const nsIFrame* aFrame,
       AnimationPerformanceWarning::Type& aPerformanceWarning /* out */) const;
 
   bool IsRelevant() const { return mIsRelevant; }
   void UpdateRelevance();
 
+  // https://drafts.csswg.org/web-animations-1/#replaceable-animation
+  bool IsReplaceable() const;
+
+  /**
+   * Returns true if this Animation satisfies the requirements for being
+   * removed when it is replaced.
+   *
+   * Returning true does not imply this animation _should_ be removed.
+   * Determining that depends on the other effects in the same EffectSet to
+   * which this animation's effect, if any, contributes.
+   */
+  bool IsRemovable() const;
+
+  /**
+   * Make this animation's target effect no-longer part of the effect stack
+   * while preserving its timing information.
+   */
+  void Remove();
+
   /**
    * Returns true if this Animation has a lower composite order than aOther.
    */
   bool HasLowerCompositeOrderThan(const Animation& aOther) const;
 
   /**
    * Returns the level at which the effect(s) associated with this Animation
    * are applied to the CSS cascade.
@@ -353,16 +374,18 @@ class Animation : public DOMEventTargetH
    * effect, if any.
    * Any properties contained in |aPropertiesToSkip| will not be added or
    * updated in |aComposeResult|.
    */
   void ComposeStyle(RawServoAnimationValueMap& aComposeResult,
                     const nsCSSPropertyIDSet& aPropertiesToSkip);
 
   void NotifyEffectTimingUpdated();
+  void NotifyEffectPropertiesUpdated();
+  void NotifyEffectTargetUpdated();
   void NotifyGeometricAnimationsStartingThisFrame();
 
   /**
    * Reschedule pending pause or pending play tasks when updating the target
    * effect.
    *
    * If we are pending, we will either be registered in the pending animation
    * tracker and have a null pending ready time, or, after our effect has been
@@ -468,16 +491,19 @@ class Animation : public DOMEventTargetH
   StickyTimeDuration EffectEnd() const;
 
   Nullable<TimeDuration> GetCurrentTimeForHoldTime(
       const Nullable<TimeDuration>& aHoldTime) const;
   Nullable<TimeDuration> GetUnconstrainedCurrentTime() const {
     return GetCurrentTimeForHoldTime(Nullable<TimeDuration>());
   }
 
+  void ScheduleReplacementCheck();
+  void MaybeScheduleReplacementCheck();
+
   // Earlier side of the elapsed time range reported in CSS Animations and CSS
   // Transitions events.
   //
   // https://drafts.csswg.org/css-animations-2/#interval-start
   // https://drafts.csswg.org/css-transitions-2/#interval-start
   StickyTimeDuration IntervalStartTime(
       const StickyTimeDuration& aActiveDuration) const {
     MOZ_ASSERT(AsCSSTransition() || AsCSSAnimation(),
@@ -552,17 +578,22 @@ class Animation : public DOMEventTargetH
   // waiting to enter when it finished pending). We use this rather than
   // checking if this animation is tracked by a PendingAnimationTracker because
   // the animation will continue to be pending even after it has been removed
   // from the PendingAnimationTracker while it is waiting for the next tick
   // (see TriggerOnNextTick for details).
   enum class PendingState : uint8_t { NotPending, PlayPending, PausePending };
   PendingState mPendingState = PendingState::NotPending;
 
+  // Handling of this animation's target effect when filling while finished.
+  AnimationReplaceState mReplaceState = AnimationReplaceState::Active;
+
   bool mFinishedAtLastComposeStyle = false;
+  bool mWasReplaceableAtLastTick = false;
+
   // Indicates that the animation should be exposed in an element's
   // getAnimations() list.
   bool mIsRelevant = false;
 
   // True if mFinished is resolved or would be resolved if mFinished has
   // yet to be created. This is not set when mFinished is rejected since
   // in that case mFinished is immediately reset to represent a new current
   // finished promise.
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -857,9 +857,58 @@ bool EffectCompositor::PreTraverseInSubt
     if (!aRoot && flushThrottledRestyles) {
       elementSet.Clear();
     }
   }
 
   return foundElementsNeedingRestyle;
 }
 
+void EffectCompositor::NoteElementForReducing(
+    const NonOwningAnimationTarget& aTarget) {
+  if (!StaticPrefs::dom_animations_api_autoremove_enabled()) {
+    return;
+  }
+
+  Unused << mElementsToReduce.put(
+      OwningAnimationTarget{aTarget.mElement, aTarget.mPseudoType});
+}
+
+static void ReduceEffectSet(EffectSet& aEffectSet) {
+  // Get a list of effects sorted by composite order.
+  nsTArray<KeyframeEffect*> sortedEffectList(aEffectSet.Count());
+  for (KeyframeEffect* effect : aEffectSet) {
+    sortedEffectList.AppendElement(effect);
+  }
+  sortedEffectList.Sort(EffectCompositeOrderComparator());
+
+  nsCSSPropertyIDSet setProperties;
+
+  // Iterate in reverse
+  for (auto iter = sortedEffectList.rbegin(); iter != sortedEffectList.rend();
+       ++iter) {
+    MOZ_ASSERT(*iter && (*iter)->GetAnimation(),
+               "Effect in an EffectSet should have an animation");
+    KeyframeEffect& effect = **iter;
+    Animation& animation = *effect.GetAnimation();
+    if (animation.IsRemovable() &&
+        effect.GetPropertySet().IsSubsetOf(setProperties)) {
+      animation.Remove();
+    } else if (animation.IsReplaceable()) {
+      setProperties |= effect.GetPropertySet();
+    }
+  }
+}
+
+void EffectCompositor::ReduceAnimations() {
+  for (auto iter = mElementsToReduce.iter(); !iter.done(); iter.next()) {
+    const OwningAnimationTarget& target = iter.get();
+    EffectSet* effectSet =
+        EffectSet::GetEffectSet(target.mElement, target.mPseudoType);
+    if (effectSet) {
+      ReduceEffectSet(*effectSet);
+    }
+  }
+
+  mElementsToReduce.clear();
+}
+
 }  // namespace mozilla
--- a/dom/animation/EffectCompositor.h
+++ b/dom/animation/EffectCompositor.h
@@ -3,17 +3,19 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_EffectCompositor_h
 #define mozilla_EffectCompositor_h
 
 #include "mozilla/AnimationPerformanceWarning.h"
+#include "mozilla/AnimationTarget.h"
 #include "mozilla/EnumeratedArray.h"
+#include "mozilla/HashTable.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/OwningNonNull.h"
 #include "mozilla/PseudoElementHashEntry.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/ServoTypes.h"
 #include "nsCSSPropertyID.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsDataHashtable.h"
@@ -194,16 +196,22 @@ class EffectCompositor {
   //
   // Returns true if there are elements needing a restyle for animation.
   bool PreTraverse(ServoTraversalFlags aFlags);
 
   // Similar to the above but for all elements in the subtree rooted
   // at aElement.
   bool PreTraverseInSubtree(ServoTraversalFlags aFlags, dom::Element* aRoot);
 
+  // Record a (pseudo-)element that may have animations that can be removed.
+  void NoteElementForReducing(const NonOwningAnimationTarget& aTarget);
+
+  bool NeedsReducing() const { return !mElementsToReduce.empty(); }
+  void ReduceAnimations();
+
   // Returns the target element for restyling.
   //
   // If |aPseudoType| is ::after, ::before or ::marker, returns the generated
   // content element of which |aElement| is the parent. If |aPseudoType| is any
   // other pseudo type (other than PseudoStyleType::NotPseudo) returns nullptr.
   // Otherwise, returns |aElement|.
   static dom::Element* GetElementToRestyle(dom::Element* aElement,
                                            PseudoStyleType aPseudoType);
@@ -234,13 +242,15 @@ class EffectCompositor {
   // animations that can be throttled, we will add an entry to the hashtable to
   // indicate that the style rule on the element is out of date but without
   // posting a restyle to update it.
   EnumeratedArray<CascadeLevel, CascadeLevel(kCascadeLevelCount),
                   nsDataHashtable<PseudoElementHashEntry, bool>>
       mElementsToRestyle;
 
   bool mIsInPreTraverse = false;
+
+  HashSet<OwningAnimationTarget> mElementsToReduce;
 };
 
 }  // namespace mozilla
 
 #endif  // mozilla_EffectCompositor_h
--- a/dom/animation/KeyframeEffect.cpp
+++ b/dom/animation/KeyframeEffect.cpp
@@ -413,16 +413,20 @@ void KeyframeEffect::UpdateProperties(co
     property.mIsRunningOnCompositor =
         runningOnCompositorProperties.HasProperty(property.mProperty);
   }
 
   CalculateCumulativeChangeHint(aStyle);
 
   MarkCascadeNeedsUpdate();
 
+  if (mAnimation) {
+    mAnimation->NotifyEffectPropertiesUpdated();
+  }
+
   RequestRestyle(EffectCompositor::RestyleType::Layer);
 }
 
 void KeyframeEffect::EnsureBaseStyles(
     const ComputedStyle* aComputedValues,
     const nsTArray<AnimationProperty>& aProperties, bool* aBaseStylesChanged) {
   if (aBaseStylesChanged != nullptr) {
     *aBaseStylesChanged = false;
@@ -994,16 +998,20 @@ void KeyframeEffect::SetTarget(
     RequestRestyle(EffectCompositor::RestyleType::Layer);
 
     nsAutoAnimationMutationBatch mb(mTarget->mElement->OwnerDoc());
     if (mAnimation) {
       nsNodeUtils::AnimationAdded(mAnimation);
       mAnimation->ReschedulePendingTasks();
     }
   }
+
+  if (mAnimation) {
+    mAnimation->NotifyEffectTargetUpdated();
+  }
 }
 
 static void CreatePropertyValue(
     nsCSSPropertyID aProperty, float aOffset,
     const Maybe<ComputedTimingFunction>& aTimingFunction,
     const AnimationValue& aValue, dom::CompositeOperation aComposite,
     AnimationPropertyValueDetails& aResult) {
   aResult.mOffset = aOffset;
--- a/dom/webidl/Animation.webidl
+++ b/dom/webidl/Animation.webidl
@@ -7,16 +7,18 @@
  * https://drafts.csswg.org/web-animations/#animation
  *
  * Copyright © 2015 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
  * liability, trademark and document use rules apply.
  */
 
 enum AnimationPlayState { "idle", "running", "paused", "finished" };
 
+enum AnimationReplaceState { "active", "removed", "persisted" };
+
 [Constructor (optional AnimationEffect? effect = null,
               optional AnimationTimeline? timeline)]
 interface Animation : EventTarget {
   attribute DOMString id;
   [Func="Document::IsWebAnimationsEnabled", Pure]
   attribute AnimationEffect? effect;
   [Func="Document::AreWebAnimationsTimelinesEnabled"]
   attribute AnimationTimeline? timeline;
@@ -25,22 +27,26 @@ interface Animation : EventTarget {
   [SetterThrows, BinaryName="currentTimeAsDouble"]
   attribute double? currentTime;
 
            attribute double             playbackRate;
   [BinaryName="playStateFromJS"]
   readonly attribute AnimationPlayState playState;
   [BinaryName="pendingFromJS"]
   readonly attribute boolean            pending;
+  [Pref="dom.animations-api.autoremove.enabled"]
+  readonly attribute AnimationReplaceState replaceState;
   [Func="Document::IsWebAnimationsEnabled", Throws]
   readonly attribute Promise<Animation> ready;
   [Func="Document::IsWebAnimationsEnabled", Throws]
   readonly attribute Promise<Animation> finished;
            attribute EventHandler       onfinish;
            attribute EventHandler       oncancel;
+  [Pref="dom.animations-api.autoremove.enabled"]
+           attribute EventHandler       onremove;
   void cancel ();
   [Throws]
   void finish ();
   [Throws, BinaryName="playFromJS"]
   void play ();
   [Throws, BinaryName="pauseFromJS"]
   void pause ();
   void updatePlaybackRate (double playbackRate);
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -36,16 +36,17 @@
 #include "mozilla/PresShell.h"
 #include "mozilla/dom/FontTableURIProtocolHandler.h"
 #include "nsITimer.h"
 #include "nsLayoutUtils.h"
 #include "nsPresContext.h"
 #include "nsComponentManagerUtils.h"
 #include "mozilla/Logging.h"
 #include "mozilla/dom/Document.h"
+#include "mozilla/dom/DocumentInlines.h"
 #include "nsIXULRuntime.h"
 #include "jsapi.h"
 #include "nsContentUtils.h"
 #include "mozilla/PendingAnimationTracker.h"
 #include "mozilla/PendingFullscreenEvent.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/StaticPrefs.h"
 #include "nsViewManager.h"
@@ -1742,16 +1743,26 @@ void nsRefreshDriver::CancelIdleRunnable
   }
 
   if (sPendingIdleRunnables->IsEmpty()) {
     delete sPendingIdleRunnables;
     sPendingIdleRunnables = nullptr;
   }
 }
 
+static bool ReduceAnimations(Document* aDocument, void* aData) {
+  if (aDocument->GetPresContext() &&
+      aDocument->GetPresContext()->EffectCompositor()->NeedsReducing()) {
+    aDocument->GetPresContext()->EffectCompositor()->ReduceAnimations();
+  }
+  aDocument->EnumerateSubDocuments(ReduceAnimations, nullptr);
+
+  return true;
+}
+
 void nsRefreshDriver::Tick(VsyncId aId, TimeStamp aNowTime) {
   MOZ_ASSERT(!nsContentUtils::GetCurrentJSContext(),
              "Shouldn't have a JSContext on the stack");
 
   if (nsNPAPIPluginInstance::InPluginCallUnsafeForReentry()) {
     NS_ERROR("Refresh driver should not run during plugin call!");
     // Try to survive this by just ignoring the refresh tick.
     return;
@@ -1896,23 +1907,25 @@ void nsRefreshDriver::Tick(VsyncId aId, 
         return;
       }
     }
 
     // Any animation timelines updated above may cause animations to queue
     // Promise resolution microtasks. We shouldn't run these, however, until we
     // have fully updated the animation state.
     //
-    // However, we need to be sure to run these before dispatching the
-    // corresponding animation events as required by the spec[1].
+    // As per the "update animations and send events" procedure[1], we should
+    // remove replaced animations and then run these microtasks before
+    // dispatching the corresponding animation events.
     //
     // [1]
     // https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events
     if (i == 1) {
       nsAutoMicroTask mt;
+      ReduceAnimations(mPresContext->Document(), nullptr);
     }
 
     // Check if running the microtask checkpoint caused the pres context to
     // be destroyed.
     if (i == 1 && (!mPresContext || !mPresContext->GetPresShell())) {
       StopTimer();
       return;
     }
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -122,16 +122,23 @@ VARCACHE_PREF(
   bool, PREF_VALUE
 )
 #undef PREF_VALUE
 
 //---------------------------------------------------------------------------
 // DOM prefs
 //---------------------------------------------------------------------------
 
+// Is support for automatically removing replaced filling animations enabled?
+VARCACHE_PREF(
+  "dom.animations-api.autoremove.enabled",
+   dom_animations_api_autoremove_enabled,
+  bool, false
+)
+
 // Is support for composite operations from the Web Animations API enabled?
 #ifdef RELEASE_OR_BETA
 # define PREF_VALUE false
 #else
 # define PREF_VALUE true
 #endif
 VARCACHE_PREF(
   "dom.animations-api.compositing.enabled",
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -859,16 +859,17 @@ STATIC_ATOMS = [
     Atom("onpopupshowing", "onpopupshowing"),
     Atom("onpopupshown", "onpopupshown"),
     Atom("onprocessorerror", "onprocessorerror"),
     Atom("onpush", "onpush"),
     Atom("onpushsubscriptionchange", "onpushsubscriptionchange"),
     Atom("onRadioStateChange", "onRadioStateChange"),
     Atom("onreadystatechange", "onreadystatechange"),
     Atom("onrejectionhandled", "onrejectionhandled"),
+    Atom("onremove", "onremove"),
     Atom("onrequestprogress", "onrequestprogress"),
     Atom("onresourcetimingbufferfull", "onresourcetimingbufferfull"),
     Atom("onresponseprogress", "onresponseprogress"),
     Atom("onRequest", "onRequest"),
     Atom("onreset", "onreset"),
     Atom("onresize", "onresize"),
     Atom("onscroll", "onscroll"),
     Atom("onselect", "onselect"),