Merge autoland to mozilla-central. a=merge
authorGurzau Raul <rgurzau@mozilla.com>
Mon, 20 May 2019 16:42:23 +0300
changeset 474499 319a369ccde4ff1c4842c62fe90e9adf4eb5c028
parent 474461 0a9b9344eb3507714ac3c30485d71bb8387ff3c2 (current diff)
parent 474498 7b90628fa1e7f153e574c07626b6af5378593a45 (diff)
child 474516 b0234f11a9ef11b1e6a13c26182e441357fbe35c
child 474724 7c884ec3dbd7a8d712d899640bed07d8dc2768b2
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)
reviewersmerge
milestone69.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
Merge autoland to mozilla-central. a=merge
dom/ipc/tests/browser_remote_navigation_delay_telemetry.js
testing/web-platform/meta/html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/disallow-crossorigin.html.ini
--- a/browser/components/sessionstore/ContentSessionStore.jsm
+++ b/browser/components/sessionstore/ContentSessionStore.jsm
@@ -748,21 +748,16 @@ class ContentSessionStore {
       this.epoch = data.epoch;
     }
 
     switch (name) {
       case "SessionStore:restoreHistory":
         this.restoreHistory(data);
         break;
       case "SessionStore:restoreTabContent":
-        if (data.isRemotenessUpdate) {
-          let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
-          histogram.add("SessionStore:restoreTabContent",
-                        Services.telemetry.msSystemNow() - data.requestTime);
-        }
         this.restoreTabContent(data);
         break;
       case "SessionStore:resetRestore":
         this.contentRestore.resetRestore();
         break;
       case "SessionStore:flush":
         this.flush(data);
         break;
--- a/browser/components/sessionstore/SessionStore.jsm
+++ b/browser/components/sessionstore/SessionStore.jsm
@@ -4305,18 +4305,17 @@ var SessionStoreInternal = {
     // view source browser that will load the required frame script.
     if (uri && ViewSourceBrowser.isViewSource(uri)) {
       new ViewSourceBrowser(browser);
     }
 
     browser.messageManager.sendAsyncMessage("SessionStore:restoreTabContent",
       {loadArguments, isRemotenessUpdate,
        reason: aOptions.restoreContentReason ||
-               RESTORE_TAB_CONTENT_REASON.SET_STATE,
-       requestTime: Services.telemetry.msSystemNow()});
+               RESTORE_TAB_CONTENT_REASON.SET_STATE});
 
     // Focus the tab's content area.
     if (aTab.selected && !window.isBlankPageURL(uri)) {
       browser.focus();
     }
   },
 
   /**
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -50,16 +50,17 @@ const PORTRAIT_MODE_WIDTH_THRESHOLD = 70
 // the sidebar automatically switches from 'landscape/horizontal' to 'portrait/vertical'
 // mode.
 const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
 
 const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
 const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
 const THREE_PANE_CHROME_ENABLED_PREF = "devtools.inspector.chrome.three-pane-enabled";
 const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
+const TELEMETRY_SCALAR_NODE_SELECTION_COUNT = "devtools.inspector.node_selection_count";
 
 /**
  * Represents an open instance of the Inspector for a tab.
  * The inspector controls the breadcrumbs, the markup view, and the sidebar
  * (computed view, rule view, font view and animation inspector).
  *
  * Events:
  * - ready
@@ -1294,16 +1295,17 @@ Inspector.prototype = {
 
     this.updateAddElementButton();
     this.updateSelectionCssSelector();
 
     const selfUpdate = this.updating("inspector-panel");
     executeSoon(() => {
       try {
         selfUpdate(this.selection.nodeFront);
+        this.telemetry.scalarAdd(TELEMETRY_SCALAR_NODE_SELECTION_COUNT, 1);
       } catch (ex) {
         console.error(ex);
       }
     });
   },
 
   /**
    * Delay the "inspector-updated" notification while a tool
--- a/devtools/client/responsive.html/browser/web-navigation.js
+++ b/devtools/client/responsive.html/browser/web-navigation.js
@@ -4,19 +4,16 @@
 
 "use strict";
 
 const { Cc, Ci, Cu, Cr } = require("chrome");
 const ChromeUtils = require("ChromeUtils");
 const Services = require("Services");
 const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
 const { E10SUtils } = require("resource://gre/modules/E10SUtils.jsm");
-const Telemetry = require("devtools/client/shared/telemetry");
-
-const telemetry = new Telemetry();
 
 function readInputStreamToString(stream) {
   return NetUtil.readInputStreamToString(stream, stream.available());
 }
 
 /**
  * This object aims to provide the nsIWebNavigation interface for mozbrowser elements.
  * nsIWebNavigation is one of the interfaces expected on <xul:browser>s, so this wrapper
@@ -80,17 +77,16 @@ BrowserElementWebNavigation.prototype = 
       flags,
       referrerInfo,
       postData: postData ? readInputStreamToString(postData) : null,
       headers: headers ? readInputStreamToString(headers) : null,
       baseURI: baseURI ? baseURI.spec : null,
       triggeringPrincipal: E10SUtils.serializePrincipal(
                            triggeringPrincipal ||
                            Services.scriptSecurityManager.createNullPrincipal({})),
-      requestTime: telemetry.msSystemNow(),
     });
   },
 
   setOriginAttributesBeforeLoading(originAttributes) {
     // No equivalent in the current BrowserElement API
     this._sendMessage("WebNavigation:SetOriginAttributes", {
       originAttributes,
     });
--- a/dom/animation/Animation.cpp
+++ b/dom/animation/Animation.cpp
@@ -1,28 +1,33 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "Animation.h"
+
 #include "AnimationUtils.h"
+#include "mozAutoDocUpdate.h"
 #include "mozilla/dom/AnimationBinding.h"
 #include "mozilla/dom/AnimationPlaybackEvent.h"
 #include "mozilla/dom/Document.h"
 #include "mozilla/dom/DocumentInlines.h"
 #include "mozilla/dom/DocumentTimeline.h"
 #include "mozilla/AnimationEventDispatcher.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/AutoRestore.h"
-#include "mozilla/Maybe.h"          // For Maybe
-#include "mozilla/TypeTraits.h"     // For std::forward<>
-#include "nsAnimationManager.h"     // For CSSAnimation
-#include "nsDOMMutationObserver.h"  // For nsAutoAnimationMutationBatch
+#include "mozilla/DeclarationBlock.h"
+#include "mozilla/Maybe.h"       // For Maybe
+#include "mozilla/TypeTraits.h"  // For std::forward<>
+#include "nsAnimationManager.h"  // For CSSAnimation
+#include "nsComputedDOMStyle.h"
+#include "nsDOMMutationObserver.h"    // For nsAutoAnimationMutationBatch
+#include "nsDOMCSSAttrDeclaration.h"  // For nsDOMCSSAttributeDeclaration
 #include "nsThreadUtils.h"  // For nsRunnableMethod and nsRevocableEventPtr
 #include "nsTransitionManager.h"      // For CSSTransition
 #include "PendingAnimationTracker.h"  // For PendingAnimationTracker
 
 namespace mozilla {
 namespace dom {
 
 // Static members
@@ -163,16 +168,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();
 }
 
@@ -589,16 +596,134 @@ void Animation::Reverse(ErrorResult& aRv
   if (aRv.Failed()) {
     mPendingPlaybackRate = originalPendingPlaybackRate;
   }
 
   // Play(), above, unconditionally calls PostUpdate so we don't need to do
   // it here.
 }
 
+void Animation::Persist() {
+  if (mReplaceState == AnimationReplaceState::Persisted) {
+    return;
+  }
+
+  bool wasRemoved = mReplaceState == AnimationReplaceState::Removed;
+
+  mReplaceState = AnimationReplaceState::Persisted;
+
+  // If the animation is not (yet) removed, there should be no side effects of
+  // persisting it.
+  if (wasRemoved) {
+    UpdateEffect(PostRestyleMode::IfNeeded);
+    PostUpdate();
+  }
+}
+
+// https://drafts.csswg.org/web-animations/#dom-animation-commitstyles
+void Animation::CommitStyles(ErrorResult& aRv) {
+  if (!mEffect) {
+    return;
+  }
+
+  // Take an owning reference to the keyframe effect. This will ensure that
+  // this Animation and the target element remain alive after flushing style.
+  RefPtr<KeyframeEffect> keyframeEffect = mEffect->AsKeyframeEffect();
+  if (!keyframeEffect) {
+    return;
+  }
+
+  Maybe<NonOwningAnimationTarget> target = keyframeEffect->GetTarget();
+  if (!target) {
+    return;
+  }
+
+  if (target->mPseudoType != PseudoStyleType::NotPseudo) {
+    aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
+    return;
+  }
+
+  // Check it is an element with a style attribute
+  nsCOMPtr<nsStyledElement> styledElement = do_QueryInterface(target->mElement);
+  if (!styledElement) {
+    aRv.Throw(NS_ERROR_DOM_NO_MODIFICATION_ALLOWED_ERR);
+    return;
+  }
+
+  // Flush style before checking if the target element is rendered since the
+  // result could depend on pending style changes.
+  if (Document* doc = target->mElement->GetComposedDoc()) {
+    doc->FlushPendingNotifications(FlushType::Style);
+  }
+  if (!target->mElement->IsRendered()) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  nsPresContext* presContext =
+      nsContentUtils::GetContextForContent(target->mElement);
+  if (!presContext) {
+    aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+    return;
+  }
+
+  // Get the computed animation values
+  UniquePtr<RawServoAnimationValueMap> animationValues =
+      Servo_AnimationValueMap_Create().Consume();
+  if (!presContext->EffectCompositor()->ComposeServoAnimationRuleForEffect(
+          *keyframeEffect, CascadeLevel(), animationValues.get())) {
+    NS_WARNING("Failed to compose animation style to commit");
+    return;
+  }
+
+  // Calling SetCSSDeclaration will trigger attribute setting code.
+  // Start the update now so that the old rule doesn't get used
+  // between when we mutate the declaration and when we set the new
+  // rule.
+  mozAutoDocUpdate autoUpdate(target->mElement->OwnerDoc(), true);
+
+  // Get the inline style to append to
+  RefPtr<DeclarationBlock> declarationBlock;
+  if (auto* existing = target->mElement->GetInlineStyleDeclaration()) {
+    declarationBlock = existing->EnsureMutable();
+  } else {
+    declarationBlock = new DeclarationBlock();
+    declarationBlock->SetDirty();
+  }
+
+  // Prepare the callback
+  MutationClosureData closureData;
+  closureData.mClosure = nsDOMCSSAttributeDeclaration::MutationClosureFunction;
+  closureData.mElement = target->mElement;
+  DeclarationBlockMutationClosure beforeChangeClosure = {
+      nsDOMCSSAttributeDeclaration::MutationClosureFunction,
+      &closureData,
+  };
+
+  // Set the animated styles
+  bool changed = false;
+  nsCSSPropertyIDSet properties = keyframeEffect->GetPropertySet();
+  for (nsCSSPropertyID property : properties) {
+    RefPtr<RawServoAnimationValue> computedValue =
+        Servo_AnimationValueMap_GetValue(animationValues.get(), property)
+            .Consume();
+    if (computedValue) {
+      changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(
+          declarationBlock->Raw(), computedValue, beforeChangeClosure);
+    }
+  }
+
+  if (!changed) {
+    return;
+  }
+
+  // Update inline style declaration
+  target->mElement->SetInlineStyleDeclaration(*declarationBlock, closureData);
+}
+
 // ---------------------------------------------------------------------------
 //
 // JS wrappers for Animation interface:
 //
 // ---------------------------------------------------------------------------
 
 Nullable<double> Animation::GetStartTimeAsDouble() const {
   return AnimationUtils::TimeDurationToDouble(mStartTime);
@@ -645,17 +770,24 @@ 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::Async);
+  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() &&
@@ -832,26 +964,139 @@ bool Animation::ShouldBeSynchronizedWith
   }
 
   return keyframeEffect->ShouldBlockAsyncTransformAnimations(
       aFrame, aPerformanceWarning);
 }
 
 void Animation::UpdateRelevance() {
   bool wasRelevant = mIsRelevant;
-  mIsRelevant = HasCurrentEffect() || IsInEffect();
+  mIsRelevant = mReplaceState != AnimationReplaceState::Removed &&
+                (HasCurrentEffect() || IsInEffect());
 
   // 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;
+
+  UpdateEffect(PostRestyleMode::IfNeeded);
+  PostUpdate();
+
+  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 +1229,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;
 }
 
@@ -1174,17 +1435,17 @@ void Animation::ResumeAt(const TimeDurat
                                            mPlaybackRate);
     if (mPlaybackRate == 0) {
       mHoldTime.SetValue(currentTimeToMatch);
     }
   }
 
   mPendingState = PendingState::NotPending;
 
-  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Async);
+  UpdateTiming(SeekFlag::NoSeek, SyncNotifyFlag::Sync);
 
   // If we had a pending playback rate, we will have now applied it so we need
   // to notify observers.
   if (hadPendingPlaybackRate && IsRelevant()) {
     nsNodeUtils::AnimationChanged(this);
   }
 
   if (mReady) {
@@ -1498,17 +1759,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
@@ -46,25 +46,17 @@ class Document;
 
 class Animation : public DOMEventTargetHelper,
                   public LinkedListElement<Animation> {
  protected:
   virtual ~Animation() {}
 
  public:
   explicit Animation(nsIGlobalObject* aGlobal)
-      : DOMEventTargetHelper(aGlobal),
-        mPlaybackRate(1.0),
-        mAnimationIndex(sNextAnimationIndex++),
-        mCachedChildIndex(-1),
-        mPendingState(PendingState::NotPending),
-        mFinishedAtLastComposeStyle(false),
-        mIsRelevant(false),
-        mFinishedIsResolved(false),
-        mSyncWithGeometricAnimations(false) {}
+      : DOMEventTargetHelper(aGlobal), mAnimationIndex(sNextAnimationIndex++) {}
 
   NS_DECL_ISUPPORTS_INHERITED
   NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(Animation, DOMEventTargetHelper)
 
   nsIGlobalObject* GetParentObject() const { return GetOwnerGlobal(); }
   virtual JSObject* WrapObject(JSContext* aCx,
                                JS::Handle<JSObject*> aGivenProto) override;
 
@@ -115,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);
@@ -142,16 +136,19 @@ class Animation : public DOMEventTargetH
    * in future we will likely have to flush style in
    * CSSAnimation::PauseFromJS so we leave it for now.
    */
   void PauseFromJS(ErrorResult& aRv) { Pause(aRv); }
 
   void UpdatePlaybackRate(double aPlaybackRate);
   void Reverse(ErrorResult& aRv);
 
+  void Persist();
+  void CommitStyles(ErrorResult& aRv);
+
   bool IsRunningOnCompositor() const;
 
   virtual void Tick();
   bool NeedsTicks() const {
     return Pending() ||
            (PlayState() == AnimationPlayState::Running &&
             // An animation with a zero playback rate doesn't need ticks even if
             // it is running since it effectively behaves as if it is paused.
@@ -324,16 +321,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.
@@ -361,16 +377,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
@@ -476,16 +494,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(),
@@ -522,17 +543,17 @@ class Animation : public DOMEventTargetH
 
   RefPtr<AnimationTimeline> mTimeline;
   RefPtr<AnimationEffect> mEffect;
   // The beginning of the delay period.
   Nullable<TimeDuration> mStartTime;            // Timeline timescale
   Nullable<TimeDuration> mHoldTime;             // Animation timescale
   Nullable<TimeDuration> mPendingReadyTime;     // Timeline timescale
   Nullable<TimeDuration> mPreviousCurrentTime;  // Animation timescale
-  double mPlaybackRate;
+  double mPlaybackRate = 1.0;
   Maybe<double> mPendingPlaybackRate;
 
   // A Promise that is replaced on each call to Play()
   // and fulfilled when Play() is successfully completed.
   // This object is lazily created by GetReady.
   // See http://drafts.csswg.org/web-animations/#current-ready-promise
   RefPtr<Promise> mReady;
 
@@ -549,42 +570,47 @@ class Animation : public DOMEventTargetH
   //
   // Note that subclasses such as CSSTransition and CSSAnimation may repurpose
   // this member to implement their own brand of sorting. As a result, it is
   // possible for two different objects to have the same index.
   uint64_t mAnimationIndex;
 
   // While ordering Animation objects for event dispatch, the index of the
   // target node in its parent may be cached in mCachedChildIndex.
-  int32_t mCachedChildIndex;
+  int32_t mCachedChildIndex = -1;
 
   // Indicates if the animation is in the pending state (and what state it is
   // 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 mPendingState = PendingState::NotPending;
 
-  bool mFinishedAtLastComposeStyle;
+  // 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;
+  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.
-  bool mFinishedIsResolved;
+  bool mFinishedIsResolved = false;
 
   // True if this animation was triggered at the same time as one or more
   // geometric animations and hence we should run any transform animations on
   // the main thread.
-  bool mSyncWithGeometricAnimations;
+  bool mSyncWithGeometricAnimations = false;
 
   RefPtr<MicroTaskRunnable> mFinishNotificationTask;
 
   nsString mId;
 };
 
 }  // namespace dom
 }  // namespace mozilla
--- a/dom/animation/AnimationTarget.h
+++ b/dom/animation/AnimationTarget.h
@@ -2,17 +2,19 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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_AnimationTarget_h
 #define mozilla_AnimationTarget_h
 
-#include "mozilla/Attributes.h"  // For MOZ_NON_OWNING_REF
+#include "mozilla/Attributes.h"     // For MOZ_NON_OWNING_REF
+#include "mozilla/HashFunctions.h"  // For HashNumber, AddToHash
+#include "mozilla/HashTable.h"      // For DefaultHasher, PointerHasher
 #include "mozilla/Maybe.h"
 #include "mozilla/RefPtr.h"
 #include "nsCSSPseudoElements.h"
 
 namespace mozilla {
 
 namespace dom {
 class Element;
@@ -64,11 +66,31 @@ inline void ImplCycleCollectionTraverse(
 }
 
 inline void ImplCycleCollectionUnlink(Maybe<OwningAnimationTarget>& aTarget) {
   if (aTarget) {
     ImplCycleCollectionUnlink(aTarget->mElement);
   }
 }
 
+// A DefaultHasher specialization for OwningAnimationTarget.
+template <>
+struct DefaultHasher<OwningAnimationTarget> {
+  using Key = OwningAnimationTarget;
+  using Lookup = OwningAnimationTarget;
+  using PtrHasher = PointerHasher<dom::Element*>;
+
+  static HashNumber hash(const Lookup& aLookup) {
+    return AddToHash(PtrHasher::hash(aLookup.mElement.get()),
+                     static_cast<uint8_t>(aLookup.mPseudoType));
+  }
+
+  static bool match(const Key& aKey, const Lookup& aLookup) {
+    return PtrHasher::match(aKey.mElement.get(), aLookup.mElement.get()) &&
+           aKey.mPseudoType == aLookup.mPseudoType;
+  }
+
+  static void rekey(Key& aKey, Key&& aNewKey) { aKey = std::move(aNewKey); }
+};
+
 }  // namespace mozilla
 
 #endif  // mozilla_AnimationTarget_h
--- a/dom/animation/DocumentTimeline.cpp
+++ b/dom/animation/DocumentTimeline.cpp
@@ -183,23 +183,16 @@ void DocumentTimeline::MostRecentRefresh
     // of mDocument's PresShell.
     MOZ_ASSERT(GetRefreshDriver(),
                "Refresh driver should still be valid at end of WillRefresh");
     UnregisterFromRefreshDriver();
   }
 }
 
 void DocumentTimeline::WillRefresh(mozilla::TimeStamp aTime) {
-  // https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events,
-  // step2.
-  // Note that this should be done before nsAutoAnimationMutationBatch which is
-  // inside MostRecentRefreshTimeUpdated().  If PerformMicroTaskCheckpoint was
-  // called before nsAutoAnimationMutationBatch is destroyed, some mutation
-  // records might not be delivered in this checkpoint.
-  nsAutoMicroTask mt;
   MostRecentRefreshTimeUpdated();
 }
 
 void DocumentTimeline::NotifyTimerAdjusted(TimeStamp aTime) {
   MostRecentRefreshTimeUpdated();
 }
 
 void DocumentTimeline::ObserveRefreshDriver(nsRefreshDriver* aDriver) {
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -389,16 +389,36 @@ class EffectCompositeOrderComparator {
         Equals(a, b) ||
         a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation()) !=
             b->GetAnimation()->HasLowerCompositeOrderThan(*a->GetAnimation()));
     return a->GetAnimation()->HasLowerCompositeOrderThan(*b->GetAnimation());
   }
 };
 }  // namespace
 
+static void ComposeSortedEffects(
+    const nsTArray<KeyframeEffect*>& aSortedEffects,
+    const EffectSet* aEffectSet, EffectCompositor::CascadeLevel aCascadeLevel,
+    RawServoAnimationValueMap* aAnimationValues) {
+  // If multiple animations affect the same property, animations with higher
+  // composite order (priority) override or add to animations with lower
+  // priority.
+  nsCSSPropertyIDSet propertiesToSkip;
+  if (aEffectSet) {
+    propertiesToSkip =
+        aCascadeLevel == EffectCompositor::CascadeLevel::Animations
+            ? aEffectSet->PropertiesForAnimationsLevel().Inverse()
+            : aEffectSet->PropertiesForAnimationsLevel();
+  }
+
+  for (KeyframeEffect* effect : aSortedEffects) {
+    effect->GetAnimation()->ComposeStyle(*aAnimationValues, propertiesToSkip);
+  }
+}
+
 bool EffectCompositor::GetServoAnimationRule(
     const dom::Element* aElement, PseudoStyleType aPseudoType,
     CascadeLevel aCascadeLevel, RawServoAnimationValueMap* aAnimationValues) {
   MOZ_ASSERT(aAnimationValues);
   MOZ_ASSERT(mPresContext && mPresContext->IsDynamic(),
              "Should not be in print preview");
   // Gecko_GetAnimationRule should have already checked this
   MOZ_ASSERT(nsContentUtils::GetPresShellForContent(aElement),
@@ -412,28 +432,73 @@ bool EffectCompositor::GetServoAnimation
 
   // Get a list of effects sorted by composite order.
   nsTArray<KeyframeEffect*> sortedEffectList(effectSet->Count());
   for (KeyframeEffect* effect : *effectSet) {
     sortedEffectList.AppendElement(effect);
   }
   sortedEffectList.Sort(EffectCompositeOrderComparator());
 
-  // If multiple animations affect the same property, animations with higher
-  // composite order (priority) override or add or animations with lower
-  // priority.
-  const nsCSSPropertyIDSet propertiesToSkip =
-      aCascadeLevel == CascadeLevel::Animations
-          ? effectSet->PropertiesForAnimationsLevel().Inverse()
-          : effectSet->PropertiesForAnimationsLevel();
-  for (KeyframeEffect* effect : sortedEffectList) {
-    effect->GetAnimation()->ComposeStyle(*aAnimationValues, propertiesToSkip);
+  ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel,
+                       aAnimationValues);
+
+  MOZ_ASSERT(effectSet == EffectSet::GetEffectSet(aElement, aPseudoType),
+             "EffectSet should not change while composing style");
+
+  return true;
+}
+
+bool EffectCompositor::ComposeServoAnimationRuleForEffect(
+    KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
+    RawServoAnimationValueMap* aAnimationValues) {
+  MOZ_ASSERT(aAnimationValues);
+  MOZ_ASSERT(mPresContext && mPresContext->IsDynamic(),
+             "Should not be in print preview");
+
+  Maybe<NonOwningAnimationTarget> target = aEffect.GetTarget();
+  if (!target) {
+    return false;
+  }
+
+  // Don't try to compose animations for elements in documents without a pres
+  // shell (e.g. XMLHttpRequest documents).
+  if (!nsContentUtils::GetPresShellForContent(target->mElement)) {
+    return false;
   }
 
-  MOZ_ASSERT(effectSet == EffectSet::GetEffectSet(aElement, aPseudoType),
+  // GetServoAnimationRule is called as part of the regular style resolution
+  // where the cascade results are updated in the pre-traversal as needed.
+  // This function, however, is only called when committing styles so we
+  // need to ensure the cascade results are up-to-date manually.
+  EffectCompositor::MaybeUpdateCascadeResults(target->mElement,
+                                              target->mPseudoType);
+
+  EffectSet* effectSet =
+      EffectSet::GetEffectSet(target->mElement, target->mPseudoType);
+
+  // Get a list of effects sorted by composite order up to and including
+  // |aEffect|, even if it is not in the EffectSet.
+  auto comparator = EffectCompositeOrderComparator();
+  nsTArray<KeyframeEffect*> sortedEffectList(effectSet ? effectSet->Count() + 1
+                                                       : 1);
+  if (effectSet) {
+    for (KeyframeEffect* effect : *effectSet) {
+      if (comparator.LessThan(effect, &aEffect)) {
+        sortedEffectList.AppendElement(effect);
+      }
+    }
+    sortedEffectList.Sort(comparator);
+  }
+  sortedEffectList.AppendElement(&aEffect);
+
+  ComposeSortedEffects(sortedEffectList, effectSet, aCascadeLevel,
+                       aAnimationValues);
+
+  MOZ_ASSERT(effectSet ==
+                 EffectSet::GetEffectSet(target->mElement, target->mPseudoType),
              "EffectSet should not change while composing style");
 
   return true;
 }
 
 /* static */ dom::Element* EffectCompositor::GetElementToRestyle(
     dom::Element* aElement, PseudoStyleType aPseudoType) {
   if (aPseudoType == PseudoStyleType::NotPseudo) {
@@ -857,9 +922,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"
@@ -33,16 +35,17 @@ class EffectSet;
 class RestyleTracker;
 class StyleAnimationValue;
 struct AnimationProperty;
 struct NonOwningAnimationTarget;
 
 namespace dom {
 class Animation;
 class Element;
+class KeyframeEffect;
 }  // namespace dom
 
 class EffectCompositor {
  public:
   explicit EffectCompositor(nsPresContext* aPresContext)
       : mPresContext(aPresContext) {}
 
   NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(EffectCompositor)
@@ -111,26 +114,36 @@ class EffectCompositor {
   // Called when computed style on the specified (pseudo-) element might
   // have changed so that any context-sensitive values stored within
   // animation effects (e.g. em-based endpoints used in keyframe effects)
   // can be re-resolved to computed values.
   void UpdateEffectProperties(const ComputedStyle* aStyle,
                               dom::Element* aElement,
                               PseudoStyleType aPseudoType);
 
-  // Get animation rule for stylo. This is an equivalent of GetAnimationRule
-  // and will be called from servo side.
+  // Get the animation rule for the appropriate level of the cascade for
+  // a (pseudo-)element. Called from the Servo side.
+  //
   // The animation rule is stored in |RawServoAnimationValueMap|.
   // We need to be careful while doing any modification because it may cause
   // some thread-safe issues.
   bool GetServoAnimationRule(const dom::Element* aElement,
                              PseudoStyleType aPseudoType,
                              CascadeLevel aCascadeLevel,
                              RawServoAnimationValueMap* aAnimationValues);
 
+  // A variant on GetServoAnimationRule that composes all the effects for an
+  // element up to and including |aEffect|.
+  //
+  // Note that |aEffect| might not be in the EffectSet since we can use this for
+  // committing the computed style of a removed Animation.
+  bool ComposeServoAnimationRuleForEffect(
+      dom::KeyframeEffect& aEffect, CascadeLevel aCascadeLevel,
+      RawServoAnimationValueMap* aAnimationValues);
+
   bool HasPendingStyleUpdates() const;
 
   static bool HasAnimationsForCompositor(const nsIFrame* aFrame,
                                          DisplayItemType aType);
 
   static nsTArray<RefPtr<dom::Animation>> GetAnimationsForCompositor(
       const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet);
 
@@ -194,16 +207,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 +253,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
@@ -312,17 +312,17 @@ bool KeyframeEffect::HasEffectiveAnimati
 
 nsCSSPropertyIDSet KeyframeEffect::GetPropertiesForCompositor(
     EffectSet& aEffects, const nsIFrame* aFrame) const {
   MOZ_ASSERT(&aEffects ==
              EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType));
 
   nsCSSPropertyIDSet properties;
 
-  if (!IsInEffect() && !IsCurrent()) {
+  if (!mAnimation || !mAnimation->IsRelevant()) {
     return properties;
   }
 
   static constexpr nsCSSPropertyIDSet compositorAnimatables =
       nsCSSPropertyIDSet::CompositorAnimatables();
   for (const AnimationProperty& property : mProperties) {
     if (!compositorAnimatables.HasProperty(property.mProperty)) {
       continue;
@@ -337,24 +337,24 @@ nsCSSPropertyIDSet KeyframeEffect::GetPr
       continue;
     }
 
     properties.AddProperty(property.mProperty);
   }
   return properties;
 }
 
-bool KeyframeEffect::HasAnimationOfPropertySet(
-    const nsCSSPropertyIDSet& aPropertySet) const {
+nsCSSPropertyIDSet KeyframeEffect::GetPropertySet() const {
+  nsCSSPropertyIDSet result;
+
   for (const AnimationProperty& property : mProperties) {
-    if (aPropertySet.HasProperty(property.mProperty)) {
-      return true;
-    }
+    result.AddProperty(property.mProperty);
   }
-  return false;
+
+  return result;
 }
 
 #ifdef DEBUG
 bool SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
                                      const nsTArray<Keyframe>& aB) {
   if (aA.Length() != aB.Length()) {
     return false;
   }
@@ -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;
@@ -570,23 +574,20 @@ void KeyframeEffect::ComposeStyle(RawSer
 
   // If the animation produces a change hint that affects the overflow region,
   // 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 (HasPropertiesThatMightAffectOverflow()) {
     nsPresContext* presContext =
         nsContentUtils::GetContextForContent(mTarget->mElement);
-    if (presContext) {
+    EffectSet* effectSet =
+        EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
+    if (presContext && effectSet) {
       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->UpdateLastOverflowAnimationSyncTime(now);
     }
   }
 }
 
 bool KeyframeEffect::IsRunningOnCompositor() const {
   // We consider animation is running on compositor if there is at least
   // one property running on compositor.
@@ -787,17 +788,19 @@ void KeyframeEffect::UpdateTargetRegistr
   }
 
   bool isRelevant = mAnimation && mAnimation->IsRelevant();
 
   // Animation::IsRelevant() returns a cached value. It only updates when
   // something calls Animation::UpdateRelevance. Whenever our timing changes,
   // we should be notifying our Animation before calling this, so
   // Animation::IsRelevant() should be up-to-date by the time we get here.
-  MOZ_ASSERT(isRelevant == IsCurrent() || IsInEffect(),
+  MOZ_ASSERT(isRelevant ==
+                 ((IsCurrent() || IsInEffect()) && mAnimation &&
+                  mAnimation->ReplaceState() != AnimationReplaceState::Removed),
              "Out of date Animation::IsRelevant value");
 
   if (isRelevant && !mInEffectSet) {
     EffectSet* effectSet = EffectSet::GetOrCreateEffectSet(
         mTarget->mElement, mTarget->mPseudoType);
     effectSet->AddEffect(*this);
     mInEffectSet = true;
     UpdateEffectSet(effectSet);
@@ -994,16 +997,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;
@@ -1723,16 +1730,21 @@ bool KeyframeEffect::ContainsAnimatedSca
   // means we looked up the wrong EffectSet so for now we just assert instead.
   MOZ_ASSERT(aFrame && aFrame->IsFrameOfType(nsIFrame::eSupportsCSSTransforms),
              "We should be passed a frame that supports transforms");
 
   if (!IsCurrent()) {
     return false;
   }
 
+  if (!mAnimation ||
+      mAnimation->ReplaceState() == AnimationReplaceState::Removed) {
+    return false;
+  }
+
   for (const AnimationProperty& prop : mProperties) {
     if (prop.mProperty != eCSSProperty_transform &&
         prop.mProperty != eCSSProperty_scale &&
         prop.mProperty != eCSSProperty_rotate) {
       continue;
     }
 
     AnimationValue baseStyle = BaseStyle(prop.mProperty);
--- a/dom/animation/KeyframeEffect.h
+++ b/dom/animation/KeyframeEffect.h
@@ -178,19 +178,25 @@ class KeyframeEffect : public AnimationE
   void NotifyAnimationTimingUpdated(PostRestyleMode aPostRestyle);
   void RequestRestyle(EffectCompositor::RestyleType aRestyleType);
   void SetAnimation(Animation* aAnimation) override;
   void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
                     ErrorResult& aRv);
   void SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
                     const ComputedStyle* aStyle);
 
+  // Returns the set of properties affected by this effect regardless of
+  // whether any of these properties is overridden by an !important rule.
+  nsCSSPropertyIDSet GetPropertySet() const;
+
   // Returns true if the effect includes a property in |aPropertySet| regardless
-  // of whether any property in the set is overridden by !important rule.
-  bool HasAnimationOfPropertySet(const nsCSSPropertyIDSet& aPropertySet) const;
+  // of whether any property in the set is overridden by an !important rule.
+  bool HasAnimationOfPropertySet(const nsCSSPropertyIDSet& aPropertySet) const {
+    return GetPropertySet().Intersects(aPropertySet);
+  }
 
   // GetEffectiveAnimationOfProperty returns AnimationProperty corresponding
   // to a given CSS property if the effect includes the property and the
   // property is not overridden by !important rules.
   // Also EffectiveAnimationOfProperty returns true under the same condition.
   //
   // |aEffect| should be the EffectSet containing this KeyframeEffect since
   // this function is typically called for all KeyframeEffects on an element
--- a/dom/animation/test/chrome.ini
+++ b/dom/animation/test/chrome.ini
@@ -1,10 +1,11 @@
 [DEFAULT]
 prefs =
+  dom.animations-api.autoremove.enabled=true
   dom.animations-api.compositing.enabled=true
   gfx.omta.background-color=true
   layout.css.individual-transform.enabled=true
 support-files =
   testcommon.js
   ../../imptests/testharness.js
   ../../imptests/testharnessreport.js
   !/dom/animation/test/chrome/file_animate_xrays.html
--- a/dom/animation/test/chrome/test_animation_observers_async.html
+++ b/dom/animation/test/chrome/test_animation_observers_async.html
@@ -573,10 +573,79 @@ promise_test(t => {
     div.classList.remove("after");
     div.style = "";
     childA.remove();
     childB.remove();
     extraStyle.remove();
   });
 }, "tree_ordering: subtree");
 
+// Test that animations removed by auto-removal trigger an event
+promise_test(async t => {
+  setupAsynchronousObserver(t, { observe: div, subtree: false });
+
+  // Start two animations such that one will be auto-removed
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+
+  // Wait for the MutationRecords corresponding to each addition.
+  await waitForNextFrame();
+
+  assert_records(
+    [
+      { added: [animA], changed: [], removed: [] },
+      { added: [animB], changed: [], removed: [] },
+    ],
+    'records after animation start'
+  );
+
+  // Finish the animations -- this should cause animA to be replaced, and
+  // automatically removed.
+  animA.finish();
+  animB.finish();
+
+  // Wait for the MutationRecords corresponding to the timing changes and the
+  // subsequent removal to be delivered.
+  await waitForNextFrame();
+
+  assert_records(
+    [
+      { added: [], changed: [animA], removed: [] },
+      { added: [], changed: [animB], removed: [] },
+      { added: [], changed: [], removed: [animA] },
+    ],
+    'records after finishing'
+  );
+
+  // Restore animA.
+  animA.persist();
+
+  // Wait for the MutationRecord corresponding to the re-addition of animA.
+  await waitForNextFrame();
+
+  assert_records(
+    [{ added: [animA], changed: [], removed: [] }],
+    'records after persisting'
+  );
+
+  // Tidy up
+  animA.cancel();
+  animB.cancel();
+
+  await waitForNextFrame();
+
+  assert_records(
+    [
+      { added: [], changed: [], removed: [animA] },
+      { added: [], changed: [], removed: [animB] },
+    ],
+    'records after tidying up end'
+  );
+}, 'Animations automatically removed are reported');
+
 runTest();
 </script>
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -9,16 +9,17 @@ prefs =
   layout.css.motion-path.enabled=true
   layout.css.individual-transform.enabled=true
 # Support files for chrome tests that we want to load over HTTP need
 # to go in here, not chrome.ini.
 support-files =
   chrome/file_animate_xrays.html
   mozilla/xhr_doc.html
   mozilla/file_deferred_start.html
+  mozilla/file_disable_animations_api_autoremove.html
   mozilla/file_disable_animations_api_compositing.html
   mozilla/file_disable_animations_api_get_animations.html
   mozilla/file_disable_animations_api_implicit_keyframes.html
   mozilla/file_disable_animations_api_timelines.html
   mozilla/file_discrete_animations.html
   mozilla/file_restyles.html
   mozilla/file_transition_finish_on_compositor.html
   ../../../layout/style/test/property_database.js
@@ -27,16 +28,17 @@ support-files =
 
 [document-timeline/test_document-timeline.html]
 skip-if = (verify && !debug && (os == 'mac'))
 [document-timeline/test_request_animation_frame.html]
 [mozilla/test_cascade.html]
 [mozilla/test_cubic_bezier_limits.html]
 [mozilla/test_deferred_start.html]
 skip-if = (toolkit == 'android' && debug) || (os == 'win' && bits == 64) # Bug 1363957
+[mozilla/test_disable_animations_api_autoremove.html]
 [mozilla/test_disable_animations_api_compositing.html]
 [mozilla/test_disable_animations_api_get_animations.html]
 [mozilla/test_disable_animations_api_implicit_keyframes.html]
 [mozilla/test_disable_animations_api_timelines.html]
 [mozilla/test_disabled_properties.html]
 [mozilla/test_discrete_animations.html]
 [mozilla/test_distance_of_basic_shape.html]
 [mozilla/test_distance_of_filter.html]
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/mozilla/file_disable_animations_api_autoremove.html
@@ -0,0 +1,69 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+promise_test(async t => {
+  const div = addDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  // This should be assert_not_own_property but our local copy of testharness.js
+  // is old.
+  assert_equals(
+    animA.replaceState,
+    undefined,
+    'Should not have a replaceState member'
+  );
+
+  animA.addEventListener(
+    'remove',
+    t.step_func(() => {
+      assert_unreached('Should not fire a remove event');
+    })
+  );
+
+  // Allow a chance for the remove event to be fired
+
+  await animA.finished;
+  await waitForNextFrame();
+}, 'Remove events should not be fired if the pref is not set');
+
+promise_test(async t => {
+  const div = addDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  assert_approx_equals(
+    parseFloat(getComputedStyle(div).opacity),
+    0.5,
+    0.0001,
+    'Covered animation should still contribute to effect stack when adding'
+  );
+
+  animB.cancel();
+
+  assert_approx_equals(
+    parseFloat(getComputedStyle(div).opacity),
+    0.2,
+    0.0001,
+    'Covered animation should still contribute to animated style when replacing'
+  );
+}, 'Covered animations should still affect style if the pref is not set');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/mozilla/test_disable_animations_api_autoremove.html
@@ -0,0 +1,15 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+setup({ explicit_done: true });
+SpecialPowers.pushPrefEnv(
+  { set: [['dom.animations-api.autoremove.enabled', false]] },
+  function() {
+    window.open('file_disable_animations_api_autoremove.html');
+  }
+);
+</script>
--- a/dom/base/Element.h
+++ b/dom/base/Element.h
@@ -1402,16 +1402,24 @@ class Element : public FragmentOrElement
   static nsresult DispatchEvent(nsPresContext* aPresContext,
                                 WidgetEvent* aEvent, nsIContent* aTarget,
                                 bool aFullDispatch, nsEventStatus* aStatus);
 
   bool IsDisplayContents() const {
     return HasServoData() && Servo_Element_IsDisplayContents(this);
   }
 
+  /*
+   * https://html.spec.whatwg.org/#being-rendered
+   *
+   * With a gotcha for display contents:
+   *   https://github.com/whatwg/html/issues/1837
+   */
+  bool IsRendered() const { return GetPrimaryFrame() || IsDisplayContents(); }
+
   const nsAttrValue* GetParsedAttr(const nsAtom* aAttr) const {
     return mAttrs.GetAttr(aAttr);
   }
 
   const nsAttrValue* GetParsedAttr(const nsAtom* aAttr,
                                    int32_t aNameSpaceID) const {
     return mAttrs.GetAttr(aAttr, aNameSpaceID);
   }
--- a/dom/base/nsCopySupport.cpp
+++ b/dom/base/nsCopySupport.cpp
@@ -341,19 +341,19 @@ static nsresult PutToClipboard(
   NS_ENSURE_SUCCESS(rv, rv);
 
   rv = clipboard->SetData(transferable, nullptr, aClipboardID);
   NS_ENSURE_SUCCESS(rv, rv);
 
   return rv;
 }
 
-nsresult nsCopySupport::HTMLCopy(Selection* aSel, Document* aDoc,
-                                 int16_t aClipboardID,
-                                 bool aWithRubyAnnotation) {
+nsresult nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
+    Selection* aSel, Document* aDoc, int16_t aClipboardID,
+    bool aWithRubyAnnotation) {
   NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER);
 
   uint32_t additionalFlags = nsIDocumentEncoder::SkipInvisibleContent;
   if (aWithRubyAnnotation) {
     additionalFlags |= nsIDocumentEncoder::OutputRubyAnnotation;
   }
 
   EncodedDocumentWithContext encodedDocumentWithContext;
@@ -910,18 +910,18 @@ bool nsCopySupport::FireClipboardEvent(E
         }
         return false;
       }
       // XXX Code which decides whether we should copy text with ruby
       // annotation is currenct depending on whether each range of the
       // selection is inside a same ruby container. But we really should
       // expose the full functionality in browser. See bug 1130891.
       bool withRubyAnnotation = IsSelectionInsideRuby(sel);
-      // call the copy code
-      nsresult rv = HTMLCopy(sel, doc, aClipboardType, withRubyAnnotation);
+      nsresult rv = EncodeDocumentWithContextAndPutToClipboard(
+          sel, doc, aClipboardType, withRubyAnnotation);
       if (NS_FAILED(rv)) {
         return false;
       }
     } else {
       return false;
     }
   } else if (clipboardData) {
     // check to see if any data was put on the data transfer.
--- a/dom/base/nsCopySupport.h
+++ b/dom/base/nsCopySupport.h
@@ -28,32 +28,31 @@ class Selection;
 class nsCopySupport {
   // class of static helper functions for copy support
  public:
   static nsresult ClearSelectionCache();
 
   /**
    * @param aDoc Needs to be not nullptr.
    */
-  static nsresult HTMLCopy(mozilla::dom::Selection* aSel,
-                           mozilla::dom::Document* aDoc, int16_t aClipboardID,
-                           bool aWithRubyAnnotation);
+  static nsresult EncodeDocumentWithContextAndPutToClipboard(
+      mozilla::dom::Selection* aSel, mozilla::dom::Document* aDoc,
+      int16_t aClipboardID, bool aWithRubyAnnotation);
 
   // Get the selection, or entire document, in the format specified by the mime
   // type (text/html or text/plain). If aSel is non-null, use it, otherwise get
   // the entire doc.
   static nsresult GetContents(const nsACString& aMimeType, uint32_t aFlags,
                               mozilla::dom::Selection* aSel,
                               mozilla::dom::Document* aDoc, nsAString& outdata);
 
   static nsresult ImageCopy(nsIImageLoadingContent* aImageElement,
                             nsILoadContext* aLoadContext, int32_t aCopyFlags);
 
-  // Get the selection as a transferable. Similar to HTMLCopy except does
-  // not deal with the clipboard.
+  // Get the selection as a transferable.
   // @param aSelection Can be nullptr.
   // @param aDocument Needs to be not nullptr.
   // @param aTransferable Needs to be not nullptr.
   static nsresult GetTransferableForSelection(
       mozilla::dom::Selection* aSelection, mozilla::dom::Document* aDocument,
       nsITransferable** aTransferable);
 
   // Same as GetTransferableForSelection, but doesn't skip invisible content.
--- a/dom/base/nsDocumentEncoder.cpp
+++ b/dom/base/nsDocumentEncoder.cpp
@@ -80,55 +80,56 @@ class nsDocumentEncoder : public nsIDocu
 
   virtual int32_t GetImmediateContextCount(
       const nsTArray<nsINode*>& aAncestorArray) {
     return -1;
   }
 
   nsresult FlushText(nsAString& aString, bool aForce);
 
-  bool IsVisibleNode(nsINode* aNode) {
-    MOZ_ASSERT(aNode, "null node");
-
+  bool IsInvisibleNodeAndShouldBeSkipped(nsINode& aNode) const {
     if (mFlags & SkipInvisibleContent) {
       // Treat the visibility of the ShadowRoot as if it were
       // the host content.
       //
       // FIXME(emilio): I suspect instead of this a bunch of the GetParent()
       // calls here should be doing GetFlattenedTreeParent, then this condition
       // should be unreachable...
-      if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(aNode)) {
-        aNode = shadowRoot->GetHost();
+      nsINode* node{&aNode};
+      if (ShadowRoot* shadowRoot = ShadowRoot::FromNode(node)) {
+        node = shadowRoot->GetHost();
       }
 
-      if (aNode->IsContent()) {
-        nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
+      if (node->IsContent()) {
+        nsIFrame* frame = node->AsContent()->GetPrimaryFrame();
         if (!frame) {
-          if (aNode->IsElement() && aNode->AsElement()->IsDisplayContents()) {
-            return true;
+          if (node->IsElement() && node->AsElement()->IsDisplayContents()) {
+            return false;
           }
-          if (aNode->IsText()) {
+          if (node->IsText()) {
             // We have already checked that our parent is visible.
             //
             // FIXME(emilio): Text not assigned to a <slot> in Shadow DOM should
             // probably return false...
-            return true;
+            return false;
           }
-          if (aNode->IsHTMLElement(nsGkAtoms::rp)) {
+          if (node->IsHTMLElement(nsGkAtoms::rp)) {
             // Ruby parentheses are part of ruby structure, hence
             // shouldn't be stripped out even if it is not displayed.
-            return true;
+            return false;
           }
-          return false;
+          return true;
         }
         bool isVisible = frame->StyleVisibility()->IsVisible();
-        if (!isVisible && aNode->IsText()) return false;
+        if (!isVisible && node->IsText()) {
+          return true;
+        }
       }
     }
-    return true;
+    return false;
   }
 
   virtual bool IncludeInContext(nsINode* aNode);
 
   void Clear();
 
   class MOZ_STACK_CLASS AutoReleaseDocumentIfNeeded final {
    public:
@@ -344,17 +345,17 @@ nsresult nsDocumentEncoder::SerializeNod
     } else if (aOriginalNode.IsText()) {
       const nsCOMPtr<nsINode> parent = aOriginalNode.GetParent();
       if (parent && parent->IsElement()) {
         mSerializer->ScanElementForPreformat(parent->AsElement());
       }
     }
   }
 
-  if (!IsVisibleNode(&aOriginalNode)) {
+  if (IsInvisibleNodeAndShouldBeSkipped(aOriginalNode)) {
     return NS_OK;
   }
 
   FixupNodeDeterminer fixupNodeDeterminer{mNodeFixup, aFixupNode,
                                           aOriginalNode};
   nsINode* node = &fixupNodeDeterminer.GetFixupNodeFallBackToOriginalNode();
 
   if (node->IsElement()) {
@@ -406,17 +407,17 @@ nsresult nsDocumentEncoder::SerializeNod
     } else if (aNode.IsText()) {
       const nsCOMPtr<nsINode> parent = aNode.GetParent();
       if (parent && parent->IsElement()) {
         mSerializer->ForgetElementForPreformat(parent->AsElement());
       }
     }
   }
 
-  if (!IsVisibleNode(&aNode)) {
+  if (IsInvisibleNodeAndShouldBeSkipped(aNode)) {
     return NS_OK;
   }
 
   if (aNode.IsElement()) {
     mSerializer->AppendElementEnd(aNode.AsElement(), aStr);
   }
   return NS_OK;
 }
@@ -424,36 +425,39 @@ nsresult nsDocumentEncoder::SerializeNod
 nsresult nsDocumentEncoder::SerializeToStringRecursive(nsINode* aNode,
                                                        nsAString& aStr,
                                                        bool aDontSerializeRoot,
                                                        uint32_t aMaxLength) {
   if (aMaxLength > 0 && aStr.Length() >= aMaxLength) {
     return NS_OK;
   }
 
-  if (!IsVisibleNode(aNode)) return NS_OK;
+  NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER);
 
-  nsresult rv = NS_OK;
+  if (IsInvisibleNodeAndShouldBeSkipped(*aNode)) {
+    return NS_OK;
+  }
 
-  MOZ_ASSERT(aNode, "aNode shouldn't be nullptr.");
   FixupNodeDeterminer fixupNodeDeterminer{mNodeFixup, nullptr, *aNode};
   nsINode* maybeFixedNode =
       &fixupNodeDeterminer.GetFixupNodeFallBackToOriginalNode();
 
   if ((mFlags & SkipInvisibleContent) &&
       !(mFlags & OutputNonTextContentAsPlaceholder)) {
     if (aNode->IsContent()) {
       if (nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame()) {
         if (!frame->IsSelectable(nullptr)) {
           aDontSerializeRoot = true;
         }
       }
     }
   }
 
+  nsresult rv = NS_OK;
+
   if (!aDontSerializeRoot) {
     int32_t endOffset = -1;
     if (aMaxLength > 0) {
       MOZ_ASSERT(aMaxLength >= aStr.Length());
       endOffset = aMaxLength - aStr.Length();
     }
     rv = SerializeNodeStart(*aNode, 0, endOffset, aStr, maybeFixedNode);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -580,17 +584,19 @@ static bool IsTextNode(nsINode* aNode) {
 
 nsresult nsDocumentEncoder::SerializeRangeNodes(nsRange* const aRange,
                                                 nsINode* const aNode,
                                                 nsAString& aString,
                                                 const int32_t aDepth) {
   nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
   NS_ENSURE_TRUE(content, NS_ERROR_FAILURE);
 
-  if (!IsVisibleNode(aNode)) return NS_OK;
+  if (IsInvisibleNodeAndShouldBeSkipped(*aNode)) {
+    return NS_OK;
+  }
 
   nsresult rv = NS_OK;
 
   // get start and end nodes for this recursion level
   nsCOMPtr<nsIContent> startNode, endNode;
   {
     int32_t start = mStartRootIndex - aDepth;
     if (start >= 0 && (uint32_t)start <= mStartNodes.Length())
@@ -783,21 +789,23 @@ nsresult nsDocumentEncoder::SerializeRan
   nsresult rv = NS_OK;
 
   rv = SerializeRangeContextStart(mCommonAncestors, aOutputString);
   NS_ENSURE_SUCCESS(rv, rv);
 
   if (startContainer == endContainer && IsTextNode(startContainer)) {
     if (mFlags & SkipInvisibleContent) {
       // Check that the parent is visible if we don't a frame.
-      // IsVisibleNode() will do it when there's a frame.
+      // IsInvisibleNodeAndShouldBeSkipped() will do it when there's a frame.
       nsCOMPtr<nsIContent> content = do_QueryInterface(startContainer);
       if (content && !content->GetPrimaryFrame()) {
         nsIContent* parent = content->GetParent();
-        if (!parent || !IsVisibleNode(parent)) return NS_OK;
+        if (!parent || IsInvisibleNodeAndShouldBeSkipped(*parent)) {
+          return NS_OK;
+        }
       }
     }
     rv = SerializeNodeStart(*startContainer, startOffset, endOffset,
                             aOutputString);
     NS_ENSURE_SUCCESS(rv, rv);
     rv = SerializeNodeEnd(*startContainer, aOutputString);
     NS_ENSURE_SUCCESS(rv, rv);
   } else {
@@ -842,19 +850,19 @@ nsDocumentEncoder::EncodeToStringWithMax
 
   if (!mDocument) return NS_ERROR_NOT_INITIALIZED;
 
   AutoReleaseDocumentIfNeeded autoReleaseDocument(this);
 
   aOutputString.Truncate();
 
   nsString output;
-  static const size_t bufferSize = 2048;
+  static const size_t kStringBufferSizeInBytes = 2048;
   if (!mCachedBuffer) {
-    mCachedBuffer = nsStringBuffer::Alloc(bufferSize).take();
+    mCachedBuffer = nsStringBuffer::Alloc(kStringBufferSizeInBytes).take();
     if (NS_WARN_IF(!mCachedBuffer)) {
       return NS_ERROR_OUT_OF_MEMORY;
     }
   }
   NS_ASSERTION(
       !mCachedBuffer->IsReadonly(),
       "nsIDocumentEncoder shouldn't keep reference to non-readonly buffer!");
   static_cast<char16_t*>(mCachedBuffer->Data())[0] = char16_t(0);
@@ -970,17 +978,17 @@ nsDocumentEncoder::EncodeToStringWithMax
   rv = mSerializer->Flush(output);
 
   mCachedBuffer = nsStringBuffer::FromString(output);
   // We have to be careful how we set aOutputString, because we don't
   // want it to end up sharing mCachedBuffer if we plan to reuse it.
   bool setOutput = false;
   // Try to cache the buffer.
   if (mCachedBuffer) {
-    if (mCachedBuffer->StorageSize() == bufferSize &&
+    if ((mCachedBuffer->StorageSize() == kStringBufferSizeInBytes) &&
         !mCachedBuffer->IsReadonly()) {
       mCachedBuffer->AddRef();
     } else {
       if (NS_SUCCEEDED(rv)) {
         mCachedBuffer->ToString(output.Length(), aOutputString);
         setOutput = true;
       }
       mCachedBuffer = nullptr;
--- a/dom/base/nsINode.h
+++ b/dom/base/nsINode.h
@@ -461,17 +461,17 @@ class nsINode : public mozilla::dom::Eve
    * like event handler compilation.  Returning null means to use the
    * global object as the scope chain parent.
    */
   virtual nsINode* GetScopeChainParent() const;
 
   MOZ_CAN_RUN_SCRIPT mozilla::dom::Element* GetParentFlexElement();
 
   /**
-   * Return whether the node is an Element node
+   * Return whether the node is an Element node. Faster than using `NodeType()`.
    */
   bool IsElement() const { return GetBoolFlag(NodeIsElement); }
 
   /**
    * Return this node as an Element.  Should only be used for nodes
    * for which IsElement() is true.  This is defined inline in Element.h.
    */
   inline mozilla::dom::Element* AsElement();
--- a/dom/chrome-webidl/JSWindowActor.webidl
+++ b/dom/chrome-webidl/JSWindowActor.webidl
@@ -30,16 +30,19 @@ interface JSWindowActorChild {
   readonly attribute WindowGlobalChild manager;
 
   [Throws]
   readonly attribute Document? document;
 
   [Throws]
   readonly attribute BrowsingContext? browsingContext;
 
+  [Throws]
+  readonly attribute nsIDocShell? docShell;
+
   // NOTE: As this returns a window proxy, it may not be currently referencing
   // the document associated with this JSWindowActor. Generally prefer using
   // `document`.
   [Throws]
   readonly attribute WindowProxy? contentWindow;
 };
 JSWindowActorChild implements JSWindowActor;
 
--- a/dom/events/test/mochitest.ini
+++ b/dom/events/test/mochitest.ini
@@ -71,17 +71,17 @@ skip-if = toolkit == 'android' #CRASH_DU
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_bug547996-2.xhtml]
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_bug556493.html]
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_bug563329.html]
 skip-if = true # Disabled due to timeouts.
 [test_bug574663.html]
-skip-if = (toolkit == 'android') || (os == 'win' && bits == 32 && !debug) || (os == 'linux' && !debug) #CRASH_DUMP, RANDOM, Bug 1523853
+skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_bug591815.html]
 [test_bug593959.html]
 [test_bug603008.html]
 skip-if = toolkit == 'android'
 [test_bug605242.html]
 skip-if = toolkit == 'android' #CRASH_DUMP, RANDOM
 [test_bug607464.html]
 skip-if = toolkit == 'android' || e10s #CRASH_DUMP, RANDOM, bug 1400586
--- a/dom/events/test/test_bug574663.html
+++ b/dom/events/test/test_bug574663.html
@@ -58,17 +58,16 @@ function forceScrollAndWait(scrollbox, c
     waitForPaint(win, utils, callback);
   }
   SpecialPowers.Services.obs.addObserver(postApzFlush, "apz-repaints-flushed");
   if (!utils.flushApzRepaints()) {
     postApzFlush();
   }
 }
 
-var kExtraEvents = 5;
 var kDelta = 3;
 
 function sendTouchpadScrollMotion(scrollbox, direction, ctrl, momentum, callback) {
   var win = scrollbox.ownerDocument.defaultView;
   let event = {
     deltaMode: WheelEvent.DOM_DELTA_PIXEL,
     deltaY: direction * kDelta,
     lineOrPageDeltaY: direction,
@@ -86,21 +85,19 @@ function sendTouchpadScrollMotion(scroll
       setTimeout(function() {
         forceScrollAndWait(scrollbox, callback);
       }, 0);
     }
   };
   scrollbox.addEventListener("wheel", onwheel);
 
   synthesizeWheel(scrollbox, 10, 10, event, win);
-  // then 5 additional pixel scrolls
+  // then additional pixel scroll
   event.lineOrPageDeltaY = 0;
-  for (let i = 1; i <= kExtraEvents; ++i) {
-    synthesizeWheel(scrollbox, 10, 10, event, win);
-  }
+  synthesizeWheel(scrollbox, 10, 10, event, win);
 }
 
 function runTest() {
   var win = open('bug574663.html', '_blank', 'width=300,height=300');
   let winUtils = SpecialPowers.getDOMWindowUtils(win);
 
   let waitUntilPainted = function(callback) {
     // Until the first non-blank paint, the parent will set the opacity of our
@@ -142,25 +139,25 @@ function runTest() {
       let scrollTopBefore = scrollbox.scrollTop;
       let zoomFactorBefore = winUtils.fullZoom;
 
       let check = function() {
         if (!ctrlKey) {
           let postfix = isMomentum ? ", even after releasing the touchpad" : "";
           // Normal scroll: scroll
           is(winUtils.fullZoom, zoomFactorBefore, "Normal scrolling shouldn't change zoom" + postfix);
-          is(scrollbox.scrollTop, scrollTopBefore + kDelta * (kExtraEvents + 1),
+          is(scrollbox.scrollTop, scrollTopBefore + kDelta * 2,
              "Normal scrolling should scroll" + postfix);
         } else {
           if (!isMomentum) {
             isnot(winUtils.fullZoom, zoomFactorBefore, "Ctrl-scrolling should zoom while the user is touching the touchpad");
             is(scrollbox.scrollTop, scrollTopBefore, "Ctrl-scrolling shouldn't scroll while the user is touching the touchpad");
           } else {
             is(winUtils.fullZoom, zoomFactorBefore, "Momentum scrolling shouldn't zoom, even when pressing Ctrl");
-            is(scrollbox.scrollTop, scrollTopBefore + kDelta * (kExtraEvents + 1),
+            is(scrollbox.scrollTop, scrollTopBefore + kDelta * 2,
                "Momentum scrolling should scroll, even when pressing Ctrl");
           }
         }
 
         if (!outstandingTests.length) {
           winUtils.restoreNormalRefresh();
           win.close();
           SimpleTest.finish();
--- a/dom/html/nsGenericHTMLElement.cpp
+++ b/dom/html/nsGenericHTMLElement.cpp
@@ -2648,24 +2648,16 @@ nsresult nsGenericHTMLElement::NewURIFro
     // and waiting for the subsequent load to fail.
     NS_RELEASE(*aURI);
     return NS_ERROR_DOM_INVALID_STATE_ERR;
   }
 
   return NS_OK;
 }
 
-// https://html.spec.whatwg.org/#being-rendered
-//
-// With a gotcha for display contents:
-//   https://github.com/whatwg/html/issues/3947
-static bool IsRendered(const Element& aElement) {
-  return aElement.GetPrimaryFrame() || aElement.IsDisplayContents();
-}
-
 void nsGenericHTMLElement::GetInnerText(mozilla::dom::DOMString& aValue,
                                         mozilla::ErrorResult& aError) {
   // innerText depends on layout. For example, white space processing is
   // something that happens during reflow and which must be reflected by
   // innerText.  So for:
   //
   //   <div style="white-space:normal"> A     B C </div>
   //
@@ -2710,17 +2702,17 @@ void nsGenericHTMLElement::GetInnerText(
     frame = frame->GetInFlowParent();
   }
 
   // Flush layout if we determined a reflow is required.
   if (dirty && doc) {
     doc->FlushPendingNotifications(FlushType::Layout);
   }
 
-  if (!IsRendered(*this)) {
+  if (!IsRendered()) {
     GetTextContentInternal(aValue, aError);
   } else {
     nsRange::GetInnerTextNoFlush(aValue, aError, this);
   }
 }
 
 void nsGenericHTMLElement::SetInnerText(const nsAString& aValue) {
   // Batch possible DOMSubtreeModified events.
--- a/dom/ipc/JSWindowActorChild.cpp
+++ b/dom/ipc/JSWindowActorChild.cpp
@@ -101,16 +101,24 @@ BrowsingContext* JSWindowActorChild::Get
   if (!mManager) {
     aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
     return nullptr;
   }
 
   return mManager->BrowsingContext();
 }
 
+nsIDocShell* JSWindowActorChild::GetDocShell(ErrorResult& aRv) {
+  if (BrowsingContext* bc = GetBrowsingContext(aRv)) {
+    return bc->GetDocShell();
+  }
+
+  return nullptr;
+}
+
 Nullable<WindowProxyHolder> JSWindowActorChild::GetContentWindow(
     ErrorResult& aRv) {
   if (BrowsingContext* bc = GetBrowsingContext(aRv)) {
     return WindowProxyHolder(bc);
   }
   return nullptr;
 }
 
--- a/dom/ipc/JSWindowActorChild.h
+++ b/dom/ipc/JSWindowActorChild.h
@@ -46,16 +46,17 @@ class JSWindowActorChild final : public 
   }
 
   WindowGlobalChild* Manager() const;
   void Init(const nsAString& aName, WindowGlobalChild* aManager);
   void StartDestroy();
   void AfterDestroy();
   Document* GetDocument(ErrorResult& aRv);
   BrowsingContext* GetBrowsingContext(ErrorResult& aRv);
+  nsIDocShell* GetDocShell(ErrorResult& aRv);
   Nullable<WindowProxyHolder> GetContentWindow(ErrorResult& aRv);
 
  protected:
   void SendRawMessage(const JSWindowActorMessageMeta& aMeta,
                       ipc::StructuredCloneData&& aData,
                       ErrorResult& aRv) override;
 
  private:
--- a/dom/ipc/tests/browser.ini
+++ b/dom/ipc/tests/browser.ini
@@ -2,12 +2,10 @@
 support-files =
   file_disableScript.html
   file_domainPolicy_base.html
   file_cancel_content_js.html
 
 [browser_domainPolicy.js]
 [browser_memory_distribution_telemetry.js]
 skip-if = !e10 # This is an e10s only probe.
-[browser_remote_navigation_delay_telemetry.js]
+[browser_cancel_content_js.js]
 skip-if = !e10s # This is an e10s only probe.
-[browser_cancel_content_js.js]
-skip-if = !e10s # This is an e10s only probe.
\ No newline at end of file
deleted file mode 100644
--- a/dom/ipc/tests/browser_remote_navigation_delay_telemetry.js
+++ /dev/null
@@ -1,48 +0,0 @@
-"use strict";
-
-var session = ChromeUtils.import("resource://gre/modules/TelemetrySession.jsm", null);
-
-add_task(async function test_memory_distribution() {
-  if (Services.prefs.getIntPref("dom.ipc.processCount", 1) < 2) {
-    ok(true, "Skip this test if e10s-multi is disabled.");
-    return;
-  }
-
-  let canRecordExtended = Services.telemetry.canRecordExtended;
-  Services.telemetry.canRecordExtended = true;
-  registerCleanupFunction(() => Services.telemetry.canRecordExtended = canRecordExtended);
-
-  Services.telemetry.getSnapshotForKeyedHistograms("main", true /* clear */);
-
-  // Open a remote page in a new tab to trigger the WebNavigation:LoadURI.
-  let tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com");
-  ok(tab1.linkedBrowser.isRemoteBrowser, "|tab1| should have a remote browser.");
-
-  // Open a new tab with about:robots, so it ends up in the parent process with a non-remote browser.
-  let tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, "about:robots");
-  ok(!tab2.linkedBrowser.isRemoteBrowser, "|tab2| should have a non-remote browser.");
-  // Navigate the tab, so it will change remotness and it triggers the SessionStore:restoreTabContent case.
-  await BrowserTestUtils.loadURI(tab2.linkedBrowser, "http://example.com");
-  ok(tab2.linkedBrowser.isRemoteBrowser, "|tab2| should have a remote browser by now.");
-
-  // There is no good way to make sure that the parent received the histogram entries from the child processes.
-  // Let's stick to the ugly, spinning the event loop until we have a good approach (Bug 1357509).
-  await BrowserTestUtils.waitForCondition(() => {
-    let s = Services.telemetry.getSnapshotForKeyedHistograms("main", false).content.FX_TAB_REMOTE_NAVIGATION_DELAY_MS;
-    return s && "WebNavigation:LoadURI" in s && "SessionStore:restoreTabContent" in s;
-  });
-
-  let s = Services.telemetry.getSnapshotForKeyedHistograms("main", false).content.FX_TAB_REMOTE_NAVIGATION_DELAY_MS;
-  let restoreTabSnapshot = s["SessionStore:restoreTabContent"];
-  ok(restoreTabSnapshot.sum > 0, "Zero delay for the restoreTabContent case is unlikely.");
-  ok(restoreTabSnapshot.sum < 10000, "More than 10 seconds delay for the restoreTabContent case is unlikely.");
-
-  let loadURISnapshot = s["WebNavigation:LoadURI"];
-  ok(loadURISnapshot.sum > 0, "Zero delay for the LoadURI case is unlikely.");
-  ok(loadURISnapshot.sum < 10000, "More than 10 seconds delay for the LoadURI case is unlikely.");
-
-  Services.telemetry.getSnapshotForKeyedHistograms("main", true /* clear */);
-
-  BrowserTestUtils.removeTab(tab2);
-  BrowserTestUtils.removeTab(tab1);
-});
--- a/dom/media/gtest/MockCubeb.h
+++ b/dom/media/gtest/MockCubeb.h
@@ -419,24 +419,26 @@ int cubeb_mock_stream_stop(cubeb_stream*
 void cubeb_mock_stream_destroy(cubeb_stream* stream) {
   MockCubeb::MockCubebStream* mockStream =
       reinterpret_cast<MockCubeb::MockCubebStream*>(stream);
   MockCubeb* mock = reinterpret_cast<MockCubeb*>(mockStream->context);
   return mock->StreamDestroy(stream);
 }
 
 static char const* cubeb_mock_get_backend_id(cubeb* context) {
-#if defined(XP_LINUX)
-  return "pulse";
-#elif defined(XP_MACOSX)
+#if defined(XP_MACOSX)
   return "audiounit";
 #elif defined(XP_WIN)
   return "wasapi";
 #elif defined(ANDROID)
   return "opensl";
+#elif defined(__OpenBSD__)
+  return "sndio";
+#else
+  return "pulse";
 #endif
 }
 
 static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume) {
   return CUBEB_OK;
 }
 
 int cubeb_mock_get_min_latency(cubeb* context, cubeb_stream_params params,
--- a/dom/smil/SMILCSSValueType.cpp
+++ b/dom/smil/SMILCSSValueType.cpp
@@ -511,18 +511,18 @@ bool SMILCSSValueType::SetPropertyValues
              "Unexpected SMIL value type");
   const ValueWrapper* wrapper = ExtractValueWrapper(aValue);
   if (!wrapper) {
     return false;
   }
 
   bool changed = false;
   for (const auto& value : wrapper->mServoValues) {
-    changed |=
-        Servo_DeclarationBlock_SetPropertyToAnimationValue(aDecl.Raw(), value);
+    changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(aDecl.Raw(),
+                                                                  value, {});
   }
 
   return changed;
 }
 
 // static
 nsCSSPropertyID SMILCSSValueType::PropertyFromValue(const SMILValue& aValue) {
   if (aValue.mType != &SMILCSSValueType::sSingleton) {
--- a/dom/webidl/Animation.webidl
+++ b/dom/webidl/Animation.webidl
@@ -7,48 +7,58 @@
  * 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" };
 
-[Constructor (optional AnimationEffect? effect = null,
-              optional AnimationTimeline? timeline)]
+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;
   [BinaryName="startTimeAsDouble"]
   attribute double? startTime;
   [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;
-  void cancel ();
+  [Pref="dom.animations-api.autoremove.enabled"]
+           attribute EventHandler       onremove;
+  void cancel();
   [Throws]
-  void finish ();
+  void finish();
   [Throws, BinaryName="playFromJS"]
-  void play ();
+  void play();
   [Throws, BinaryName="pauseFromJS"]
-  void pause ();
+  void pause();
   void updatePlaybackRate (double playbackRate);
   [Throws]
-  void reverse ();
+  void reverse();
+  [Pref="dom.animations-api.autoremove.enabled"]
+  void persist();
+  [Pref="dom.animations-api.autoremove.enabled", Throws]
+  void commitStyles();
 };
 
 // Non-standard extensions
 partial interface Animation {
   [ChromeOnly] readonly attribute boolean isRunningOnCompositor;
 };
--- a/dom/webidl/DocumentTimeline.webidl
+++ b/dom/webidl/DocumentTimeline.webidl
@@ -10,11 +10,11 @@
  * liability, trademark and document use rules apply.
  */
 
 dictionary DocumentTimelineOptions {
   DOMHighResTimeStamp originTime = 0;
 };
 
 [Func="Document::AreWebAnimationsTimelinesEnabled",
- Constructor (optional DocumentTimelineOptions options)]
+ Constructor(optional DocumentTimelineOptions options)]
 interface DocumentTimeline : AnimationTimeline {
 };
--- a/dom/webidl/KeyframeEffect.webidl
+++ b/dom/webidl/KeyframeEffect.webidl
@@ -19,28 +19,28 @@ dictionary KeyframeEffectOptions : Effec
   IterationCompositeOperation iterationComposite = "replace";
   CompositeOperation          composite = "replace";
 };
 
 // KeyframeEffect should run in the caller's compartment to do custom
 // processing on the `keyframes` object.
 [Func="Document::IsWebAnimationsEnabled",
  RunConstructorInCallerCompartment,
- Constructor ((Element or CSSPseudoElement)? target,
-              object? keyframes,
-              optional (unrestricted double or KeyframeEffectOptions) options),
- Constructor (KeyframeEffect source)]
+ Constructor((Element or CSSPseudoElement)? target,
+             object? keyframes,
+             optional (unrestricted double or KeyframeEffectOptions) options),
+ Constructor(KeyframeEffect source)]
 interface KeyframeEffect : AnimationEffect {
   attribute (Element or CSSPseudoElement)?  target;
   [Pref="dom.animations-api.compositing.enabled"]
   attribute IterationCompositeOperation     iterationComposite;
   [Pref="dom.animations-api.compositing.enabled"]
   attribute CompositeOperation              composite;
-  [Throws] sequence<object> getKeyframes ();
-  [Throws] void             setKeyframes (object? keyframes);
+  [Throws] sequence<object> getKeyframes();
+  [Throws] void             setKeyframes(object? keyframes);
 };
 
 // Non-standard extensions
 dictionary AnimationPropertyValueDetails {
   required double             offset;
            DOMString          value;
            DOMString          easing;
   required CompositeOperation composite;
--- a/js/public/Promise.h
+++ b/js/public/Promise.h
@@ -255,17 +255,17 @@ class MOZ_RAII JS_PUBLIC_API AutoDebugge
   MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER;
   JSContext* cx;
   js::UniquePtr<JobQueue::SavedJobQueue> saved;
 };
 
 enum class PromiseRejectionHandlingState { Unhandled, Handled };
 
 typedef void (*PromiseRejectionTrackerCallback)(
-    JSContext* cx, JS::HandleObject promise,
+    JSContext* cx, bool mutedErrors, JS::HandleObject promise,
     JS::PromiseRejectionHandlingState state, void* data);
 
 /**
  * Sets the callback that's invoked whenever a Promise is rejected without
  * a rejection handler, and when a Promise that was previously rejected
  * without a handler gets a handler attached.
  */
 extern JS_PUBLIC_API void SetPromiseRejectionTrackerCallback(
--- a/js/src/shell/js.cpp
+++ b/js/src/shell/js.cpp
@@ -1132,17 +1132,17 @@ static bool TrackUnhandledRejections(JSC
       // add the promise in the first place, due to OOM.
       break;
   }
 
   return true;
 }
 
 static void ForwardingPromiseRejectionTrackerCallback(
-    JSContext* cx, JS::HandleObject promise,
+    JSContext* cx, bool mutedErrors, JS::HandleObject promise,
     JS::PromiseRejectionHandlingState state, void* data) {
   AutoReportException are(cx);
 
   if (!TrackUnhandledRejections(cx, promise, state)) {
     return;
   }
 
   RootedValue callback(cx,
--- a/js/src/vm/Runtime.cpp
+++ b/js/src/vm/Runtime.cpp
@@ -632,31 +632,43 @@ bool JSRuntime::enqueuePromiseJob(JSCont
 
 void JSRuntime::addUnhandledRejectedPromise(JSContext* cx,
                                             js::HandleObject promise) {
   MOZ_ASSERT(promise->is<PromiseObject>());
   if (!cx->promiseRejectionTrackerCallback) {
     return;
   }
 
+  bool mutedErrors = false;
+  if (JSScript* script = cx->currentScript()) {
+    mutedErrors = script->mutedErrors();
+  }
+
   void* data = cx->promiseRejectionTrackerCallbackData;
   cx->promiseRejectionTrackerCallback(
-      cx, promise, JS::PromiseRejectionHandlingState::Unhandled, data);
+      cx, mutedErrors, promise, JS::PromiseRejectionHandlingState::Unhandled,
+      data);
 }
 
 void JSRuntime::removeUnhandledRejectedPromise(JSContext* cx,
                                                js::HandleObject promise) {
   MOZ_ASSERT(promise->is<PromiseObject>());
   if (!cx->promiseRejectionTrackerCallback) {
     return;
   }
 
+  bool mutedErrors = false;
+  if (JSScript* script = cx->currentScript()) {
+    mutedErrors = script->mutedErrors();
+  }
+
   void* data = cx->promiseRejectionTrackerCallbackData;
   cx->promiseRejectionTrackerCallback(
-      cx, promise, JS::PromiseRejectionHandlingState::Handled, data);
+      cx, mutedErrors, promise, JS::PromiseRejectionHandlingState::Handled,
+      data);
 }
 
 mozilla::non_crypto::XorShift128PlusRNG& JSRuntime::randomKeyGenerator() {
   MOZ_ASSERT(CurrentThreadCanAccessRuntime(this));
   if (randomKeyGenerator_.isNothing()) {
     mozilla::Array<uint64_t, 2> seed;
     GenerateXorShift128PlusSeed(seed);
     randomKeyGenerator_.emplace(seed[0], seed[1]);
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -220,16 +220,20 @@ static ContentMap& GetContentMap() {
     sContentMap = new ContentMap();
   }
   return *sContentMap;
 }
 
 template <typename TestType>
 static bool HasMatchingAnimations(EffectSet& aEffects, TestType&& aTest) {
   for (KeyframeEffect* effect : aEffects) {
+    if (!effect->GetAnimation() || !effect->GetAnimation()->IsRelevant()) {
+      continue;
+    }
+
     if (aTest(*effect, aEffects)) {
       return true;
     }
   }
 
   return false;
 }
 
@@ -258,18 +262,17 @@ static bool HasMatchingAnimations(const 
 }
 
 /* static */
 bool nsLayoutUtils::HasAnimationOfPropertySet(
     const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet) {
   return HasMatchingAnimations(
       aFrame, aPropertySet,
       [&aPropertySet](KeyframeEffect& aEffect, const EffectSet&) {
-        return (aEffect.IsInEffect() || aEffect.IsCurrent()) &&
-               aEffect.HasAnimationOfPropertySet(aPropertySet);
+        return aEffect.HasAnimationOfPropertySet(aPropertySet);
       });
 }
 
 /* static */
 bool nsLayoutUtils::HasAnimationOfPropertySet(
     const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet,
     EffectSet* aEffectSet) {
   MOZ_ASSERT(
@@ -289,29 +292,27 @@ bool nsLayoutUtils::HasAnimationOfProper
   if (aPropertySet.IsSubsetOf(nsCSSPropertyIDSet::OpacityProperties()) &&
       !aEffectSet->MayHaveOpacityAnimation()) {
     return false;
   }
 
   return HasMatchingAnimations(
       *aEffectSet,
       [&aPropertySet](KeyframeEffect& aEffect, const EffectSet& aEffectSet) {
-        return (aEffect.IsInEffect() || aEffect.IsCurrent()) &&
-               aEffect.HasAnimationOfPropertySet(aPropertySet);
+        return aEffect.HasAnimationOfPropertySet(aPropertySet);
       });
 }
 
 /* static */
 bool nsLayoutUtils::HasEffectiveAnimation(
     const nsIFrame* aFrame, const nsCSSPropertyIDSet& aPropertySet) {
   return HasMatchingAnimations(
       aFrame, aPropertySet,
       [&aPropertySet](KeyframeEffect& aEffect, const EffectSet& aEffectSet) {
-        return (aEffect.IsInEffect() || aEffect.IsCurrent()) &&
-               aEffect.HasEffectiveAnimationOfPropertySet(aPropertySet,
+        return aEffect.HasEffectiveAnimationOfPropertySet(aPropertySet,
                                                           aEffectSet);
       });
 }
 
 /* static */
 nsCSSPropertyIDSet nsLayoutUtils::GetAnimationPropertiesForCompositor(
     const nsIFrame* aStyleFrame) {
   nsCSSPropertyIDSet properties;
--- 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;
@@ -1892,16 +1903,38 @@ void nsRefreshDriver::Tick(VsyncId aId, 
       obs->WillRefresh(aNowTime);
 
       if (!mPresContext || !mPresContext->GetPresShell()) {
         StopTimer();
         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.
+    //
+    // 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;
+    }
+
     if (i == 1) {
       // This is the FlushType::Style case.
 
       DispatchScrollEvents();
       DispatchVisualViewportScrollEvents();
       DispatchAnimationEvents();
       RunFullscreenSteps();
       RunFrameRequestCallbacks(aNowTime);
--- a/layout/generic/nsFrameSelection.cpp
+++ b/layout/generic/nsFrameSelection.cpp
@@ -2742,33 +2742,34 @@ nsresult nsFrameSelection::UpdateSelecti
     Selection* aSel) {
   PresShell* presShell = aSel->GetPresShell();
   if (!presShell) {
     return NS_OK;
   }
   nsCOMPtr<Document> aDoc = presShell->GetDocument();
 
   if (aDoc && aSel && !aSel->IsCollapsed()) {
-    return nsCopySupport::HTMLCopy(aSel, aDoc, nsIClipboard::kSelectionCache,
-                                   false);
+    return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
+        aSel, aDoc, nsIClipboard::kSelectionCache, false);
   }
 
   return NS_OK;
 }
 
 // mozilla::AutoCopyListener
 
 int16_t AutoCopyListener::sClipboardID = -1;
 
 /*
  * What we do now:
  * On every selection change, we copy to the clipboard anew, creating a
  * HTML buffer, a transferable, an nsISupportsString and
- * a huge mess every time.  This is basically what nsCopySupport::HTMLCopy()
- * does to move the selection into the clipboard for Edit->Copy.
+ * a huge mess every time.  This is basically what
+ * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
+ * selection into the clipboard for Edit->Copy.
  *
  * What we should do, to make our end of the deal faster:
  * Create a singleton transferable with our own magic converter.  When selection
  * changes (use a quick cache to detect ``real'' changes), we put the new
  * Selection in the transferable.  Our magic converter will take care of
  * transferable->whatever-other-format when the time comes to actually
  * hand over the clipboard contents.
  *
@@ -2825,13 +2826,15 @@ void AutoCopyListener::OnSelectionChange
     // If on macOS, clear the current selection transferable cached
     // on the parent process (nsClipboard) when the selection is empty.
     DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
     NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                          "nsCopySupport::ClearSelectionCache() failed");
     return;
   }
 
-  // Call the copy code.
   DebugOnly<nsresult> rv =
-      nsCopySupport::HTMLCopy(&aSelection, aDocument, sClipboardID, false);
-  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "nsCopySupport::HTMLCopy() failed");
+      nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
+          &aSelection, aDocument, sClipboardID, false);
+  NS_WARNING_ASSERTION(
+      NS_SUCCEEDED(rv),
+      "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");
 }
--- a/layout/style/ServoBindingTypes.h
+++ b/layout/style/ServoBindingTypes.h
@@ -168,13 +168,11 @@ SERVO_ARC_TYPE(ComputedStyle, mozilla::C
     void operator()(type_* aPtr) const { Servo_##name_##_Drop(aPtr); } \
   };                                                                   \
   }
 #include "mozilla/ServoBoxedTypeList.h"
 #undef SERVO_BOXED_TYPE
 
 // Other special cases.
 
-// TODO(heycam): Handle these elsewhere.
 struct RawServoAnimationValueTable;
-struct RawServoAnimationValueMap;
 
 #endif  // mozilla_ServoBindingTypes_h
--- a/layout/style/ServoBoxedTypeList.h
+++ b/layout/style/ServoBoxedTypeList.h
@@ -19,13 +19,14 @@
 //
 // If you add an entry to this file, you should also add an implementation of
 // HasBoxFFI in Rust.
 //
 // TODO(emilio): We should remove the opaque type now, cbindgen should be able
 // to just generate the forward declaration.
 
 SERVO_BOXED_TYPE(StyleSet, RawServoStyleSet)
+SERVO_BOXED_TYPE(AnimationValueMap, RawServoAnimationValueMap)
 SERVO_BOXED_TYPE(AuthorStyles, RawServoAuthorStyles)
 SERVO_BOXED_TYPE(SelectorList, RawServoSelectorList)
 SERVO_BOXED_TYPE(SharedMemoryBuilder, RawServoSharedMemoryBuilder)
 SERVO_BOXED_TYPE(SourceSizeList, RawServoSourceSizeList)
 SERVO_BOXED_TYPE(UseCounters, StyleUseCounters)
--- a/modules/libpref/init/StaticPrefList.h
+++ b/modules/libpref/init/StaticPrefList.h
@@ -122,16 +122,29 @@ VARCACHE_PREF(
   bool, PREF_VALUE
 )
 #undef PREF_VALUE
 
 //---------------------------------------------------------------------------
 // DOM prefs
 //---------------------------------------------------------------------------
 
+// Is support for automatically removing replaced filling animations enabled?
+#ifdef RELEASE_OR_BETA
+# define PREF_VALUE false
+#else
+# define PREF_VALUE true
+#endif
+VARCACHE_PREF(
+  "dom.animations-api.autoremove.enabled",
+   dom_animations_api_autoremove_enabled,
+  bool, PREF_VALUE
+)
+#undef PREF_VALUE
+
 // 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/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -1617,16 +1617,19 @@ pref("network.disable.ipc.security", tru
 pref("network.protocol-handler.external-default", true);      // OK to load
 pref("network.protocol-handler.warn-external-default", true); // warn before load
 
 // Prevent using external protocol handlers for these schemes
 pref("network.protocol-handler.external.hcp", false);
 pref("network.protocol-handler.external.vbscript", false);
 pref("network.protocol-handler.external.javascript", false);
 pref("network.protocol-handler.external.data", false);
+pref("network.protocol-handler.external.ie.http", false);
+pref("network.protocol-handler.external.iehistory", false);
+pref("network.protocol-handler.external.ierss", false);
 pref("network.protocol-handler.external.ms-help", false);
 pref("network.protocol-handler.external.res", false);
 pref("network.protocol-handler.external.shell", false);
 pref("network.protocol-handler.external.vnd.ms.radio", false);
 #ifdef XP_MACOSX
 pref("network.protocol-handler.external.help", false);
 #endif
 pref("network.protocol-handler.external.disk", false);
--- a/security/manager/ssl/PKCS11ModuleDB.cpp
+++ b/security/manager/ssl/PKCS11ModuleDB.cpp
@@ -2,18 +2,16 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* 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/. */
 
 #include "PKCS11ModuleDB.h"
 
 #include "ScopedNSSTypes.h"
-#include "mozilla/Telemetry.h"
-#include "nsCRTGlue.h"
 #include "nsIMutableArray.h"
 #include "nsNSSCertHelper.h"
 #include "nsNSSComponent.h"
 #include "nsNativeCharsetUtils.h"
 #include "nsPKCS11Slot.h"
 #include "nsServiceManagerUtils.h"
 
 namespace mozilla {
@@ -58,42 +56,16 @@ PKCS11ModuleDB::DeleteModule(const nsASt
   SECStatus srv = SECMOD_DeleteModule(moduleNameNormalized.get(), &modType);
   if (srv != SECSuccess) {
     return NS_ERROR_FAILURE;
   }
 
   return NS_OK;
 }
 
-// Given a PKCS#11 module, determines an appropriate name to identify it for the
-// purposes of gathering telemetry. For 3rd party PKCS#11 modules, this should
-// be the name of the dynamic library that implements the module. For built-in
-// NSS modules, it will be the common name of the module.
-// Because the result will be used as a telemetry scalar key (which must be less
-// than 70 characters), this function will also truncate the result if it
-// exceeds this limit. (Note that unfortunately telemetry doesn't expose a way
-// to programmatically query the scalar key length limit, so we have to
-// hard-code the value here.)
-void GetModuleNameForTelemetry(/*in*/ const SECMODModule* module,
-                               /*out*/ nsString& result) {
-  result.Truncate();
-  if (module->dllName) {
-    result.AssignASCII(module->dllName);
-    int32_t separatorIndex = result.RFind(FILE_PATH_SEPARATOR);
-    if (separatorIndex != kNotFound) {
-      result = Substring(result, separatorIndex + 1);
-    }
-  } else {
-    result.AssignASCII(module->commonName);
-  }
-  if (result.Length() >= 70) {
-    result.Truncate(69);
-  }
-}
-
 // Add a new PKCS11 module to the user's profile.
 NS_IMETHODIMP
 PKCS11ModuleDB::AddModule(const nsAString& aModuleName,
                           const nsAString& aLibraryFullPath,
                           int32_t aCryptoMechanismFlags, int32_t aCipherFlags) {
   if (aModuleName.IsEmpty()) {
     return NS_ERROR_INVALID_ARG;
   }
@@ -126,33 +98,16 @@ PKCS11ModuleDB::AddModule(const nsAStrin
   NS_CopyUnicodeToNative(aLibraryFullPath, fullPath);
   uint32_t mechFlags = SECMOD_PubMechFlagstoInternal(aCryptoMechanismFlags);
   uint32_t cipherFlags = SECMOD_PubCipherFlagstoInternal(aCipherFlags);
   SECStatus srv = SECMOD_AddNewModule(moduleNameNormalized.get(),
                                       fullPath.get(), mechFlags, cipherFlags);
   if (srv != SECSuccess) {
     return NS_ERROR_FAILURE;
   }
-
-  UniqueSECMODModule module(SECMOD_FindModule(moduleNameNormalized.get()));
-  if (!module) {
-    return NS_ERROR_FAILURE;
-  }
-
-  nsAutoString scalarKey;
-  GetModuleNameForTelemetry(module.get(), scalarKey);
-  // Scalar keys must be between 0 and 70 characters (exclusive).
-  // GetModuleNameForTelemetry takes care of keys that are too long.
-  // If for some reason it couldn't come up with an appropriate name and
-  // returned an empty result, however, we need to not attempt to record this
-  // (it wouldn't give us anything useful anyway).
-  if (scalarKey.Length() > 0) {
-    Telemetry::ScalarSet(Telemetry::ScalarID::SECURITY_PKCS11_MODULES_LOADED,
-                         scalarKey, true);
-  }
   return NS_OK;
 }
 
 NS_IMETHODIMP
 PKCS11ModuleDB::ListModules(nsISimpleEnumerator** _retval) {
   NS_ENSURE_ARG_POINTER(_retval);
 
   nsresult rv = BlockUntilLoadableRootsLoaded();
--- a/security/manager/ssl/PKCS11ModuleDB.h
+++ b/security/manager/ssl/PKCS11ModuleDB.h
@@ -3,18 +3,16 @@
  * 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 PKCS11ModuleDB_h
 #define PKCS11ModuleDB_h
 
 #include "nsIPKCS11ModuleDB.h"
 
-#include "nsString.h"
-
 namespace mozilla {
 namespace psm {
 
 #define NS_PKCS11MODULEDB_CID                        \
   {                                                  \
     0xff9fbcd7, 0x9517, 0x4334, {                    \
       0xb9, 0x7a, 0xce, 0xed, 0x78, 0x90, 0x99, 0x74 \
     }                                                \
@@ -26,15 +24,12 @@ class PKCS11ModuleDB : public nsIPKCS11M
 
   NS_DECL_ISUPPORTS
   NS_DECL_NSIPKCS11MODULEDB
 
  protected:
   virtual ~PKCS11ModuleDB() {}
 };
 
-void GetModuleNameForTelemetry(/*in*/ const SECMODModule* module,
-                               /*out*/ nsString& result);
-
 }  // namespace psm
 }  // namespace mozilla
 
 #endif  // PKCS11ModuleDB_h
--- a/security/manager/ssl/nsNSSComponent.cpp
+++ b/security/manager/ssl/nsNSSComponent.cpp
@@ -4,17 +4,16 @@
  * 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/. */
 
 #include "nsNSSComponent.h"
 
 #include "EnterpriseRoots.h"
 #include "ExtendedValidation.h"
 #include "NSSCertDBTrustDomain.h"
-#include "PKCS11ModuleDB.h"
 #include "ScopedNSSTypes.h"
 #include "SharedSSLState.h"
 #include "cert.h"
 #include "certdb.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Casting.h"
 #include "mozilla/Preferences.h"
@@ -1788,38 +1787,16 @@ nsresult nsNSSComponent::InitializeNSS()
   }
 
   mozilla::pkix::RegisterErrorTable();
 
   if (PK11_IsFIPS()) {
     Telemetry::Accumulate(Telemetry::FIPS_ENABLED, true);
   }
 
-  // Gather telemetry on any PKCS#11 modules we have loaded. Note that because
-  // we load the built-in root module asynchronously after this, the telemetry
-  // will not include it.
-  {  // Introduce scope for the AutoSECMODListReadLock.
-    AutoSECMODListReadLock lock;
-    for (SECMODModuleList* list = SECMOD_GetDefaultModuleList(); list;
-         list = list->next) {
-      nsAutoString scalarKey;
-      GetModuleNameForTelemetry(list->module, scalarKey);
-      // Scalar keys must be between 0 and 70 characters (exclusive).
-      // GetModuleNameForTelemetry takes care of keys that are too long. If for
-      // some reason it couldn't come up with an appropriate name and returned
-      // an empty result, however, we need to not attempt to record this (it
-      // wouldn't give us anything useful anyway).
-      if (scalarKey.Length() > 0) {
-        Telemetry::ScalarSet(
-            Telemetry::ScalarID::SECURITY_PKCS11_MODULES_LOADED, scalarKey,
-            true);
-      }
-    }
-  }
-
   MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("NSS Initialization done\n"));
 
   {
     MutexAutoLock lock(mMutex);
 
     // ensure we have initial values for various root hashes
 #ifdef DEBUG
     mTestBuiltInRootHash.Truncate();
--- a/security/manager/ssl/tests/unit/test_pkcs11_module.js
+++ b/security/manager/ssl/tests/unit/test_pkcs11_module.js
@@ -45,50 +45,23 @@ function checkTestModuleExists() {
   notEqual(testModule, null, "Test module should have been found");
   notEqual(testModule.libName, null, "Test module lib name should not be null");
   ok(testModule.libName.includes(ctypes.libraryName("pkcs11testmodule")),
      "Test module lib name should include lib name of 'pkcs11testmodule'");
 
   return testModule;
 }
 
-function checkModuleTelemetry(additionalExpectedModule = undefined) {
-  let expectedModules = [
-    "NSS Internal PKCS #11 Module",
-  ];
-  if (additionalExpectedModule) {
-    expectedModules.push(additionalExpectedModule);
-  }
-  expectedModules.sort();
-  let telemetry = Services.telemetry.getSnapshotForKeyedScalars("main", false).parent;
-  let moduleTelemetry = telemetry["security.pkcs11_modules_loaded"];
-  let actualModules = [];
-  Object.keys(moduleTelemetry).forEach((key) => {
-    ok(moduleTelemetry[key], "each keyed scalar should be true");
-    actualModules.push(key);
-  });
-  actualModules.sort();
-  equal(actualModules.length, expectedModules.length,
-        "the number of actual and expected loaded modules should be the same");
-  for (let i in actualModules) {
-    equal(actualModules[i], expectedModules[i],
-          "actual and expected module names should match");
-  }
-}
-
 function run_test() {
   // Check that if we have never added the test module, that we don't find it
   // in the module list.
   checkTestModuleNotPresent();
-  checkModuleTelemetry();
 
   // Check that adding the test module makes it appear in the module list.
   loadPKCS11TestModule(true);
-  checkModuleTelemetry(
-    `${AppConstants.DLL_PREFIX}pkcs11testmodule${AppConstants.DLL_SUFFIX}`);
   let testModule = checkTestModuleExists();
 
   // Check that listing the slots for the test module works.
   let testModuleSlotNames = Array.from(testModule.listSlots(),
                                        slot => slot.name);
   testModuleSlotNames.sort();
   const expectedSlotNames = ["Empty PKCS11 Slot", "Test PKCS11 Slot", "Test PKCS11 Slot 二"];
   deepEqual(testModuleSlotNames, expectedSlotNames,
--- a/servo/components/style/gecko/boxed_types.rs
+++ b/servo/components/style/gecko/boxed_types.rs
@@ -1,21 +1,31 @@
 /* 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/. */
 
 //! FFI implementations for types listed in ServoBoxedTypeList.h.
 
 use crate::gecko_bindings::sugar::ownership::{HasBoxFFI, HasFFI, HasSimpleFFI};
+use crate::properties::animated_properties::AnimationValueMap;
 use to_shmem::SharedMemoryBuilder;
 
 // TODO(heycam): The FFI impls for most of the types in ServoBoxedTypeList.h are spread across
 // various files at the moment, but should probably all move here, and use macros to define
 // them more succinctly, like we do in arc_types.rs.
 
 #[cfg(feature = "gecko")]
+unsafe impl HasFFI for AnimationValueMap {
+    type FFIType = crate::gecko_bindings::bindings::RawServoAnimationValueMap;
+}
+#[cfg(feature = "gecko")]
+unsafe impl HasSimpleFFI for AnimationValueMap {}
+#[cfg(feature = "gecko")]
+unsafe impl HasBoxFFI for AnimationValueMap {}
+
+#[cfg(feature = "gecko")]
 unsafe impl HasFFI for SharedMemoryBuilder {
     type FFIType = crate::gecko_bindings::bindings::RawServoSharedMemoryBuilder;
 }
 #[cfg(feature = "gecko")]
 unsafe impl HasSimpleFFI for SharedMemoryBuilder {}
 #[cfg(feature = "gecko")]
 unsafe impl HasBoxFFI for SharedMemoryBuilder {}
--- a/servo/components/style/properties/helpers/animated_properties.mako.rs
+++ b/servo/components/style/properties/helpers/animated_properties.mako.rs
@@ -4,19 +4,17 @@
 
 <%namespace name="helpers" file="/helpers.mako.rs" />
 
 <%
     from data import to_idl_name, SYSTEM_FONT_LONGHANDS, to_camel_case
     from itertools import groupby
 %>
 
-#[cfg(feature = "gecko")] use crate::gecko_bindings::structs::RawServoAnimationValueMap;
 #[cfg(feature = "gecko")] use crate::gecko_bindings::structs::nsCSSPropertyID;
-#[cfg(feature = "gecko")] use crate::gecko_bindings::sugar::ownership::{HasFFI, HasSimpleFFI};
 use itertools::{EitherOrBoth, Itertools};
 use crate::properties::{CSSWideKeyword, PropertyDeclaration};
 use crate::properties::longhands;
 use crate::properties::longhands::visibility::computed_value::T as Visibility;
 use crate::properties::LonghandId;
 use servo_arc::Arc;
 use smallvec::SmallVec;
 use std::ptr;
@@ -185,23 +183,16 @@ impl AnimatedProperty {
     }
 }
 
 /// A collection of AnimationValue that were composed on an element.
 /// This HashMap stores the values that are the last AnimationValue to be
 /// composed for each TransitionProperty.
 pub type AnimationValueMap = FxHashMap<LonghandId, AnimationValue>;
 
-#[cfg(feature = "gecko")]
-unsafe impl HasFFI for AnimationValueMap {
-    type FFIType = RawServoAnimationValueMap;
-}
-#[cfg(feature = "gecko")]
-unsafe impl HasSimpleFFI for AnimationValueMap {}
-
 /// An enum to represent a single computed value belonging to an animated
 /// property in order to be interpolated with another one. When interpolating,
 /// both values need to belong to the same property.
 ///
 /// This is different to AnimatedProperty in the sense that AnimatedProperty
 /// also knows the final value to be used during the animation.
 ///
 /// This is to be used in Gecko integration code.
--- a/servo/components/style/properties/properties.mako.rs
+++ b/servo/components/style/properties/properties.mako.rs
@@ -2332,16 +2332,24 @@ impl SourcePropertyDeclaration {
     #[inline]
     pub fn new() -> Self {
         SourcePropertyDeclaration {
             declarations: ::arrayvec::ArrayVec::new(),
             all_shorthand: AllShorthand::NotSet,
         }
     }
 
+    /// Create one with a single PropertyDeclaration.
+    #[inline]
+    pub fn with_one(decl: PropertyDeclaration) -> Self {
+        let mut result = Self::new();
+        result.declarations.push(decl);
+        result
+    }
+
     /// Similar to Vec::drain: leaves this empty when the return value is dropped.
     pub fn drain(&mut self) -> SourcePropertyDeclarationDrain {
         SourcePropertyDeclarationDrain {
             declarations: self.declarations.drain(..),
             all_shorthand: mem::replace(&mut self.all_shorthand, AllShorthand::NotSet),
         }
     }
 
--- a/servo/ports/geckolib/glue.rs
+++ b/servo/ports/geckolib/glue.rs
@@ -96,17 +96,17 @@ use style::gecko_bindings::structs::Styl
 use style::gecko_bindings::structs::URLExtraData;
 use style::gecko_bindings::sugar::ownership::{FFIArcHelpers, HasArcFFI, HasFFI};
 use style::gecko_bindings::sugar::ownership::{HasSimpleFFI, HasBoxFFI, Strong, Owned, OwnedOrNull};
 use style::gecko_bindings::sugar::refptr::RefPtr;
 use style::global_style_data::{GlobalStyleData, GLOBAL_STYLE_DATA, STYLE_THREAD_POOL};
 use style::invalidation::element::restyle_hints::RestyleHint;
 use style::media_queries::MediaList;
 use style::parser::{self, Parse, ParserContext};
-use style::properties::animated_properties::AnimationValue;
+use style::properties::animated_properties::{AnimationValue, AnimationValueMap};
 use style::properties::{parse_one_declaration_into, parse_style_attribute};
 use style::properties::{ComputedValues, Importance, NonCustomPropertyId};
 use style::properties::{LonghandId, LonghandIdSet, PropertyDeclarationBlock, PropertyId};
 use style::properties::{PropertyDeclarationId, ShorthandId};
 use style::properties::{SourcePropertyDeclaration, StyleBuilder, UnparsedValue};
 use style::rule_cache::RuleCacheConditions;
 use style::rule_tree::{CascadeLevel, StrongRuleNode};
 use style::selector_parser::PseudoElementCascadeType;
@@ -909,16 +909,46 @@ fn resolve_rules_for_element_with_contex
         PseudoElementResolution::IfApplicable,
     );
     resolver
         .cascade_style_and_visited_with_default_parents(inputs)
         .0
 }
 
 #[no_mangle]
+pub extern "C" fn Servo_AnimationValueMap_Create() -> Owned<structs::RawServoAnimationValueMap> {
+    Box::<AnimationValueMap>::default().into_ffi()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn Servo_AnimationValueMap_Drop(value_map: *mut structs::RawServoAnimationValueMap) {
+    AnimationValueMap::drop_ffi(value_map)
+}
+
+#[no_mangle]
+pub extern "C" fn Servo_AnimationValueMap_GetValue(
+    raw_value_map: &mut structs::RawServoAnimationValueMap,
+    property_id: nsCSSPropertyID,
+) -> Strong<RawServoAnimationValue> {
+    use style::properties::animated_properties::AnimationValueMap;
+
+    let property = match LonghandId::from_nscsspropertyid(property_id) {
+        Ok(longhand) => longhand,
+        Err(()) => return Strong::null(),
+    };
+    let value_map = AnimationValueMap::from_ffi_mut(raw_value_map);
+
+    value_map
+        .get(&property)
+        .map_or(Strong::null(), |value| {
+            Arc::new(value.clone()).into_strong()
+        })
+}
+
+#[no_mangle]
 pub extern "C" fn Servo_StyleSet_GetBaseComputedValuesForElement(
     raw_style_set: &RawServoStyleSet,
     element: &RawGeckoElement,
     computed_values: &ComputedValues,
     snapshots: *const ServoElementSnapshotTable,
 ) -> Strong<ComputedValues> {
     debug_assert!(!snapshots.is_null());
     let computed_values = unsafe { ArcBorrow::from_ref(computed_values) };
@@ -4086,16 +4116,38 @@ pub unsafe extern "C" fn Servo_Declarati
     property: *const nsACString,
 ) -> bool {
     let property_id = get_property_id_from_property!(property, false);
     read_locked_arc(declarations, |decls: &PropertyDeclarationBlock| {
         decls.property_priority(&property_id).important()
     })
 }
 
+#[inline(always)]
+fn set_property_to_declarations(
+    block: &RawServoDeclarationBlock,
+    parsed_declarations: &mut SourcePropertyDeclaration,
+    before_change_closure: DeclarationBlockMutationClosure,
+    importance: Importance,
+) -> bool {
+    let mut updates = Default::default();
+    let will_change = read_locked_arc(block, |decls: &PropertyDeclarationBlock| {
+        decls.prepare_for_update(&parsed_declarations, importance, &mut updates)
+    });
+    if !will_change {
+        return false;
+    }
+
+    before_change_closure.invoke();
+    write_locked_arc(block, |decls: &mut PropertyDeclarationBlock| {
+        decls.update(parsed_declarations.drain(), importance, &mut updates)
+    });
+    true
+}
+
 fn set_property(
     declarations: &RawServoDeclarationBlock,
     property_id: PropertyId,
     value: *const nsACString,
     is_important: bool,
     data: *mut URLExtraData,
     parsing_mode: structs::ParsingMode,
     quirks_mode: QuirksMode,
@@ -4118,29 +4170,23 @@ fn set_property(
         return false;
     }
 
     let importance = if is_important {
         Importance::Important
     } else {
         Importance::Normal
     };
-    let mut updates = Default::default();
-    let will_change = read_locked_arc(declarations, |decls: &PropertyDeclarationBlock| {
-        decls.prepare_for_update(&source_declarations, importance, &mut updates)
-    });
-    if !will_change {
-        return false;
-    }
-
-    before_change_closure.invoke();
-    write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.update(source_declarations.drain(), importance, &mut updates)
-    });
-    true
+
+    set_property_to_declarations(
+        declarations,
+        &mut source_declarations,
+        before_change_closure,
+        importance,
+    )
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetProperty(
     declarations: &RawServoDeclarationBlock,
     property: *const nsACString,
     value: *const nsACString,
     is_important: bool,
@@ -4162,23 +4208,27 @@ pub unsafe extern "C" fn Servo_Declarati
         before_change_closure,
     )
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetPropertyToAnimationValue(
     declarations: &RawServoDeclarationBlock,
     animation_value: &RawServoAnimationValue,
+    before_change_closure: DeclarationBlockMutationClosure,
 ) -> bool {
-    write_locked_arc(declarations, |decls: &mut PropertyDeclarationBlock| {
-        decls.push(
-            AnimationValue::as_arc(&animation_value).uncompute(),
-            Importance::Normal,
-        )
-    })
+    let mut source_declarations =
+        SourcePropertyDeclaration::with_one(AnimationValue::as_arc(&animation_value).uncompute());
+
+    set_property_to_declarations(
+        declarations,
+        &mut source_declarations,
+        before_change_closure,
+        Importance::Normal,
+    )
 }
 
 #[no_mangle]
 pub unsafe extern "C" fn Servo_DeclarationBlock_SetPropertyById(
     declarations: &RawServoDeclarationBlock,
     property: nsCSSPropertyID,
     value: *const nsACString,
     is_important: bool,
--- a/taskcluster/ci/beetmover-checksums/kind.yml
+++ b/taskcluster/ci/beetmover-checksums/kind.yml
@@ -16,21 +16,17 @@ only-for-attributes:
     - nightly
     - shippable
 
 job-template:
     shipping-phase: promote
     attributes:
         artifact_prefix: public
         artifact_map:
-            by-project:
+            by-release-type:
+                beta|release.*|esr.*:
+                    by-platform:
+                        android.*: taskcluster/taskgraph/manifests/fennec_candidates_checksums.yml
+                        default: taskcluster/taskgraph/manifests/firefox_candidates_checksums.yml
                 default:
                     by-platform:
                         android.*: taskcluster/taskgraph/manifests/fennec_nightly_checksums.yml
                         default: taskcluster/taskgraph/manifests/firefox_nightly_checksums.yml
-                mozilla-beta:
-                    by-platform:
-                        android.*: taskcluster/taskgraph/manifests/fennec_candidates_checksums.yml
-                        default: taskcluster/taskgraph/manifests/firefox_candidates_checksums.yml
-                mozilla-release:
-                    by-platform:
-                        android.*: taskcluster/taskgraph/manifests/fennec_candidates_checksums.yml
-                        default: taskcluster/taskgraph/manifests/firefox_candidates_checksums.yml
--- a/taskcluster/ci/beetmover-repackage/kind.yml
+++ b/taskcluster/ci/beetmover-repackage/kind.yml
@@ -52,12 +52,11 @@ only-for-build-platforms:
     - win64-aarch64-devedition-nightly/opt
     - linux64-asan-reporter-nightly/opt
     - win64-asan-reporter-nightly/opt
 
 job-template:
     shipping-phase: promote
     attributes:
         artifact_map:
-            by-project:
+            by-release-type:
+                beta|release.*|esr.*: taskcluster/taskgraph/manifests/firefox_candidates.yml
                 default: taskcluster/taskgraph/manifests/firefox_nightly.yml
-                mozilla-beta: taskcluster/taskgraph/manifests/firefox_candidates.yml
-                mozilla-release: taskcluster/taskgraph/manifests/firefox_candidates.yml
--- a/taskcluster/ci/beetmover/kind.yml
+++ b/taskcluster/ci/beetmover/kind.yml
@@ -36,12 +36,11 @@ not-for-build-platforms:
     - win64-aarch64-devedition-nightly/opt
     - linux64-asan-reporter-nightly/opt
     - win64-asan-reporter-nightly/opt
 
 job-template:
     shipping-phase: promote
     attributes:
         artifact_map:
-            by-project:
-                mozilla-release: taskcluster/taskgraph/manifests/fennec_candidates.yml
-                mozilla-beta: taskcluster/taskgraph/manifests/fennec_candidates.yml
+            by-release-type:
+                beta|release.*|esr.*: taskcluster/taskgraph/manifests/fennec_candidates.yml
                 default: taskcluster/taskgraph/manifests/fennec_nightly.yml
--- a/taskcluster/ci/build/windows.yml
+++ b/taskcluster/ci/build/windows.yml
@@ -264,17 +264,17 @@ win32-shippable/opt:
         type: shippable
     attributes:
         shippable: true
         enable-full-crashsymbols: true
     stub-installer:
         by-release-type:
             nightly: true
             beta: true
-            release: true
+            release.*: true
             esr.*: false
             default:
                 by-project:
                     # browser/confvars.sh looks for nightly-try
                     try: true
                     default: false
     shipping-phase: build
     shipping-product: firefox
@@ -827,17 +827,17 @@ win32-devedition-nightly/opt:
         type: nightly
     attributes:
         nightly: true
         enable-full-crashsymbols: true
     stub-installer:
         by-release-type:
             nightly: true
             beta: true
-            release: true
+            release.*: true
             default:
                 by-project:
                     # browser/confvars.sh looks for nightly-try
                     try: true
                     default: false
     shipping-phase: build
     shipping-product: devedition
     treeherder:
--- a/taskcluster/ci/cron-bouncer-check/kind.yml
+++ b/taskcluster/ci/cron-bouncer-check/kind.yml
@@ -37,25 +37,26 @@ jobs:
             job-name: firefox-bouncer-check
         run:
             config:
                 by-release-type:
                     beta:
                         - releases/bouncer_firefox_beta.py
                     release:
                         - releases/bouncer_firefox_release.py
-                    esr60:
+                    esr.*:
                         - releases/bouncer_firefox_esr.py
                     default:
                         - releases/bouncer_firefox_beta.py
             product-field:
                 by-project:
                     mozilla-beta: LATEST_FIREFOX_RELEASED_DEVEL_VERSION
                     mozilla-release: LATEST_FIREFOX_VERSION
                     mozilla-esr60: FIREFOX_ESR
+                    mozilla-esr68: FIREFOX_ESR_NEXT
                     default: LATEST_FIREFOX_DEVEL_VERSION
             products-url: https://product-details.mozilla.org/1.0/firefox_versions.json
         treeherder:
             platform: firefox-release/opt
 
     devedition:
         shipping-product: devedition
         run-on-projects: [mozilla-beta]
--- a/taskcluster/ci/release-balrog-scheduling/kind.yml
+++ b/taskcluster/ci/release-balrog-scheduling/kind.yml
@@ -29,36 +29,41 @@ jobs:
             product: firefox
             publish-rules:
                 by-release-level:
                     production:
                         by-release-type:
                             beta: [32]
                             release: [145]
                             esr60: [806]
+                            esr68: [882]
                             default: []
                     staging:
                         by-release-type:
                             beta: [32]
                             release: [145]
                             esr60: [806]
+                            esr68: [875]
                             default: []
         treeherder:
             platform: firefox-release/opt
             symbol: Rel(BSFx)
             tier: 1
             kind: build
     firefox-bz2:
         description: Schedule Firefox publishing in balrog (bz2)
         name: release-firefox_schedule_publishing_in_balrog-bz2
         shipping-product: firefox
-        run-on-releases: [esr60]
+        run-on-releases: [esr60, esr68]
         worker:
             product: firefox
-            publish-rules: [521]
+            publish-rules:
+                by-release-type:
+                    esr60: [521]
+                    default: []
             blob-suffix: -bz2
         treeherder:
             platform: firefox-release/opt
             symbol: Rel(BSFx-bz2)
             tier: 1
             kind: build
     devedition:
         description: Schedule Devedition publishing in balrog
--- a/taskcluster/ci/release-balrog-submit-toplevel/kind.yml
+++ b/taskcluster/ci/release-balrog-submit-toplevel/kind.yml
@@ -34,40 +34,44 @@ jobs:
         description: Submit toplevel Firefox release to balrog
         shipping-product: firefox
         worker:
             product: firefox
             channel-names:
                 by-release-type:
                     beta: ["beta", "beta-localtest", "beta-cdntest"]
                     release(-rc)?: ["release", "release-localtest", "release-cdntest"]
-                    esr60: ["esr", "esr-localtest", "esr-cdntest"]
+                    esr.*: ["esr", "esr-localtest", "esr-cdntest", "esr-localtest-next", "esr-cdntest-next"]
                     default: []
             rules-to-update:
                 by-release-type:
                     beta: ["firefox-beta-cdntest", "firefox-beta-localtest"]
                     release(-rc)?: ["firefox-release-cdntest", "firefox-release-localtest"]
+                    esr68: ["firefox-esr68-cdntest", "firefox-esr68-localtest"]
                     esr60: ["firefox-esr60-cdntest", "firefox-esr60-localtest"]
                     default: []
             platforms: ["linux", "linux64", "macosx64", "win32", "win64", "win64-aarch64"]
         treeherder:
             platform: firefox-release/opt
             symbol: Rel(BPFx)
             tier: 1
             kind: build
 
     firefox-bz2:
         name: submit-toplevel-firefox-release-to-balrog-bz2
         description: Submit toplevel Firefox release to balrog
         shipping-product: firefox
-        run-on-releases: [esr60]
+        run-on-releases: [esr60, esr68]
         worker:
             product: firefox
-            channel-names: ["esr", "esr-localtest", "esr-cdntest"]
-            rules-to-update: ["esr52-cdntest", "esr52-localtest"]
+            channel-names: ["esr", "esr-localtest", "esr-cdntest", "esr-localtest-next", "esr-cdntest-next"]
+            rules-to-update:
+                by-release-type:
+                    esr68: ["esr52-cdntest-next", "esr52-localtest-next"]
+                    esr60: ["esr52-cdntest", "esr52-localtest"]
             platforms: ["linux", "linux64", "macosx64", "win32", "win64"]
             blob-suffix: -bz2
             complete-mar-filename-pattern: '%s-%s.bz2.complete.mar'
             complete-mar-bouncer-product-pattern: '%s-%s-complete-bz2'
         treeherder:
             platform: firefox-release/opt
             symbol: Rel(BPFx-bz2)
             tier: 1
--- a/taskcluster/ci/release-bouncer-aliases/kind.yml
+++ b/taskcluster/ci/release-bouncer-aliases/kind.yml
@@ -74,16 +74,20 @@ jobs:
                     firefox-latest-ssl: installer-ssl
                     firefox-latest: installer
                     firefox-stub: stub-installer
                     firefox-msi-latest-ssl: msi
                 mozilla-esr60:
                     firefox-esr-latest-ssl: installer-ssl
                     firefox-esr-latest: installer
                     firefox-esr-msi-latest-ssl: msi
+                mozilla-esr68:
+                    firefox-esr-next-latest-ssl: installer-ssl
+                    firefox-esr-next-latest: installer
+                    firefox-esr-next-msi-latest-ssl: msi
                 birch:
                     firefox-latest-ssl: installer-ssl
                     firefox-latest: installer
                     firefox-stub: stub-installer
                 jamun:
                     firefox-esr-latest-ssl: installer-ssl
                     firefox-esr-latest: installer
                 maple:
--- a/taskcluster/ci/release-bouncer-check/kind.yml
+++ b/taskcluster/ci/release-bouncer-check/kind.yml
@@ -52,17 +52,17 @@ jobs:
             job-name: firefox-release-bouncer-check
         run:
             config:
                 by-release-type:
                     beta:
                         - releases/bouncer_firefox_beta.py
                     release:
                         - releases/bouncer_firefox_release.py
-                    esr60:
+                    esr.*:
                         - releases/bouncer_firefox_esr.py
                     default:
                         - releases/bouncer_firefox_beta.py
         treeherder:
             platform: firefox-release/opt
 
     devedition:
         shipping-product: devedition
--- a/taskcluster/ci/release-bouncer-sub/kind.yml
+++ b/taskcluster/ci/release-bouncer-sub/kind.yml
@@ -47,21 +47,21 @@ jobs:
         shipping-product: fennec
         locales-file: mobile/locales/l10n-changesets.json
         treeherder:
             platform: fennec-release/opt
 
     firefox:
         bouncer-platforms: ['linux', 'linux64', 'osx', 'win', 'win64', 'win64-aarch64']
         bouncer-products:
-            by-project:
+            by-release-type:
                 default: ['complete-mar', 'installer', 'installer-ssl', 'partial-mar', 'stub-installer', 'msi']
+                esr68: ['complete-mar', 'complete-mar-bz2', 'installer', 'installer-ssl', 'partial-mar', 'msi']
                 # No stub installer in esr60
-                mozilla-esr60: ['complete-mar', 'complete-mar-bz2', 'installer', 'installer-ssl', 'partial-mar']
-                jamun: ['complete-mar', 'complete-mar-bz2', 'installer', 'installer-ssl', 'partial-mar']
+                esr60: ['complete-mar', 'complete-mar-bz2', 'installer', 'installer-ssl', 'partial-mar']
         shipping-product: firefox
         treeherder:
             platform: firefox-release/opt
 
     firefox-rc:
         bouncer-platforms: ['linux', 'linux64', 'osx', 'win', 'win64', 'win64-aarch64']
         bouncer-products: ['complete-mar-candidates', 'partial-mar-candidates']
         shipping-product: firefox
--- a/taskcluster/ci/release-snap-repackage/kind.yml
+++ b/taskcluster/ci/release-snap-repackage/kind.yml
@@ -54,12 +54,12 @@ job-defaults:
             LANG: C.UTF-8
             L10N_CHANGESETS: "{config_params[head_repository]}/raw-file/{config_params[head_rev]}/browser/locales/l10n-changesets.json"
         chain-of-trust: true
 
 jobs:
     firefox:
         shipping-product: firefox
         attributes:
-            build_platform: linux64-snap-shippable
+            build_platform: linux64-shippable
             build_type: opt
         treeherder:
             symbol: Snap(r)
new file mode 100644
--- /dev/null
+++ b/taskcluster/ci/release-update-verify-config-next/kind.yml
@@ -0,0 +1,98 @@
+# 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/.
+---
+loader: taskgraph.loader.transform:loader
+
+transforms:
+    - taskgraph.transforms.release:run_on_releases
+    - taskgraph.transforms.update_verify_config:transforms
+    - taskgraph.transforms.job:transforms
+    - taskgraph.transforms.task:transforms
+
+job-defaults:
+    name: update-verify-config-next
+    run-on-projects: []  # to make sure this never runs as part of CI
+    run-on-releases: [esr68]
+    shipping-phase: promote
+    worker-type: b-linux
+    worker:
+        docker-image:
+            in-tree: "update-verify"
+        max-run-time: 3600
+        artifacts:
+            - name: public/build/update-verify.cfg
+              path: /builds/worker/checkouts/gecko/update-verify.cfg
+              type: file
+    run:
+        sparse-profile: mozharness
+    treeherder:
+        symbol: UVCnext
+        kind: test
+        tier: 1
+    extra:
+        app-name: browser
+        branch-prefix: mozilla
+        archive-prefix:
+            by-release-level:
+                staging: "http://ftp.stage.mozaws.net/pub"
+                production: "https://archive.mozilla.org/pub"
+        previous-archive-prefix:
+            by-release-level:
+                staging: "https://archive.mozilla.org/pub"
+                production: null
+        aus-server:
+            by-release-level:
+                staging: "https://stage.balrog.nonprod.cloudops.mozgcp.net"
+                production: "https://aus5.mozilla.org"
+        override-certs:
+            by-release-level:
+                staging: dep
+                production: null
+        updater-platform: linux-x86_64
+        product: firefox
+        channel: "esr-localtest-next"
+        include-version: esr68-next
+        last-watershed: "52.0esr"
+
+jobs:
+    firefox-next-linux:
+        shipping-product: firefox
+        treeherder:
+            platform: linux32-shippable/opt
+        attributes:
+            build_platform: linux-shippable
+        extra:
+            platform: linux-i686
+    firefox-next-linux64:
+        shipping-product: firefox
+        treeherder:
+            platform: linux64-shippable/opt
+        attributes:
+            build_platform: linux64-shippable
+        extra:
+            platform: linux-x86_64
+    firefox-next-macosx64:
+        shipping-product: firefox
+        treeherder:
+            platform: osx-shippable/opt
+        attributes:
+            build_platform: macosx64-shippable
+        extra:
+            platform: mac
+    firefox-next-win32:
+        shipping-product: firefox
+        treeherder:
+            platform: windows2012-32-shippable/opt
+        attributes:
+            build_platform: win32-shippable
+        extra:
+            platform: win32
+    firefox-next-win64:
+        shipping-product: firefox
+        treeherder:
+            platform: windows2012-64-shippable/opt
+        attributes:
+            build_platform: win64-shippable
+        extra:
+            platform: win64
--- a/taskcluster/ci/release-update-verify-config/kind.yml
+++ b/taskcluster/ci/release-update-verify-config/kind.yml
@@ -65,16 +65,17 @@ job-defaults:
                         linux-.*: "57.0"
                         linux64-.*: "57.0"
                         macosx64-.*: "57.0"
                         win32-.*: "56.0"
                         win64(?!-aarch64)-.*: "56.0"
                         win64-aarch64.*: "67.0"
                         default: null
                 esr60: "52.0esr"
+                esr68: "68.0esr"
                 default: "default"
 
 jobs:
     firefox-linux:
         shipping-product: firefox
         treeherder:
             symbol: UVC
             platform: linux32-shippable/opt
new file mode 100644
--- /dev/null
+++ b/taskcluster/ci/release-update-verify-next/kind.yml
@@ -0,0 +1,73 @@
+# 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/.
+---
+loader: taskgraph.loader.transform:loader
+
+kind-dependencies:
+    - post-balrog-dummy
+    - post-beetmover-dummy
+    - release-balrog-submit-toplevel
+    - release-update-verify-config-next
+
+transforms:
+    - taskgraph.transforms.release:run_on_releases
+    - taskgraph.transforms.release_deps:transforms
+    - taskgraph.transforms.update_verify:transforms
+    - taskgraph.transforms.job:transforms
+    - taskgraph.transforms.task:transforms
+
+job-defaults:
+    name: update-verify-next
+    run-on-projects: []  # to make sure this never runs as part of CI
+    run-on-releases: [esr68]
+    shipping-phase: promote
+    worker-type: b-linux
+    worker:
+        artifacts:
+            - name: 'public/build/diff-summary.log'
+              path: '/builds/worker/tools/release/updates/diff-summary.log'
+              type: file
+        docker-image:
+            in-tree: "update-verify"
+        max-run-time: 7200
+        retry-exit-status:
+            - 255
+        env:
+            CHANNEL: "esr-localtest-next"
+    treeherder:
+        symbol: UV(UVnext)
+        kind: test
+    extra:
+        chunks: 12
+
+jobs:
+    firefox-next-linux64:
+        description: linux64 esr-next update verify
+        shipping-product: firefox
+        attributes:
+            build_platform: linux64-shippable
+
+    firefox-next-linux:
+        description: linux esr-next update verify
+        shipping-product: firefox
+        attributes:
+            build_platform: linux-shippable
+
+    firefox-next-win64:
+        description: win64 esr-next update verify
+        shipping-product: firefox
+        attributes:
+            build_platform: win64-shippable
+
+    firefox-next-win32:
+        description: win32 esr-next update verify
+        shipping-product: firefox
+        attributes:
+            build_platform: win32-shippable
+
+    firefox-next-macosx64:
+        description: macosx64 esr-next update verify
+        shipping-product: firefox
+        attributes:
+            build_platform: macosx64-shippable
--- a/taskcluster/ci/repackage-l10n/kind.yml
+++ b/taskcluster/ci/repackage-l10n/kind.yml
@@ -58,17 +58,17 @@ job-template:
                     - repackage/win32_sfx_stub.py
                     - repackage/win64_signed.py
                 win64-aarch64\b.*:
                     - repackage/base.py
                     - repackage/win64-aarch64_sfx_stub.py
                     - repackage/win64_signed.py
     package-formats:
         by-release-type:
-            esr60:
+            esr(60|68):
                 by-build-platform:
                     linux.*: [mar, mar-bz2]
                     linux4\b.*: [mar, mar-bz2]
                     macosx64\b.*: [mar, mar-bz2, dmg]
                     win32\b.*: [mar, mar-bz2, installer]
                     win64\b.*: [mar, mar-bz2, installer]
             default:
                 by-build-platform:
--- a/taskcluster/ci/repackage/kind.yml
+++ b/taskcluster/ci/repackage/kind.yml
@@ -68,17 +68,17 @@ job-template:
                     - repackage/win32_sfx_stub.py
                     - repackage/win64_signed.py
                 win64-aarch64\b.*:
                     - repackage/base.py
                     - repackage/win64-aarch64_sfx_stub.py
                     - repackage/win64_signed.py
     package-formats:
         by-release-type:
-            esr60:
+            esr(60|68):
                 by-build-platform:
                     linux.*: [mar, mar-bz2]
                     linux4\b.*: [mar, mar-bz2]
                     macosx64\b.*: [mar, mar-bz2, dmg]
                     win32\b.*: [mar, mar-bz2, installer]
                     win64\b.*: [mar, mar-bz2, installer]
             default:
                 by-build-platform:
--- a/taskcluster/docker/funsize-update-generator/Pipfile.lock
+++ b/taskcluster/docker/funsize-update-generator/Pipfile.lock
@@ -41,20 +41,20 @@
                 "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6",
                 "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889"
             ],
             "index": "pypi",
             "version": "==3.5.4"
         },
         "arrow": {
             "hashes": [
-                "sha256:3397e5448952e18e1295bf047014659effa5ae8da6a5371d37ff0ddc46fa6872",
-                "sha256:6f54d9f016c0b7811fac9fb8c2c7fa7421d80c54dbdd75ffb12913c55db60b8a"
+                "sha256:002f2315cf4c8404de737c42860441732d339bbc57fee584e2027520e055ecc1",
+                "sha256:82dd5e13b733787d4eb0fef42d1ee1a99136dc1d65178f70373b3678b3181bfc"
             ],
-            "version": "==0.13.1"
+            "version": "==0.13.2"
         },
         "asn1crypto": {
             "hashes": [
                 "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87",
                 "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49"
             ],
             "version": "==0.24.0"
         },
@@ -69,34 +69,34 @@
             "hashes": [
                 "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79",
                 "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399"
             ],
             "version": "==19.1.0"
         },
         "awscli": {
             "hashes": [
-                "sha256:b7a6e758a7d2e7230e4e21acab9f80db2fd31248333ca8575b4538a5c43ebd2c",
-                "sha256:fae8839c4ddf6e7fb49543beec8d9659afd60e2fa23481ee723390f0a3a7d0f7"
+                "sha256:34e7ee2bd912e6613ac064099c13e2114722d508fc35e01fd0dfc3be41ddd92c",
+                "sha256:f73c11e6726a5ca25df3399762fae7f6882c71e097dc622d0e4743c9f8e84526"
             ],
             "index": "pypi",
-            "version": "==1.16.156"
+            "version": "==1.16.161"
         },
         "backports.lzma": {
             "hashes": [
                 "sha256:50829db66f0445442f6c796bba0ca62d1f87f54760c4682b6d1489e729a43744"
             ],
             "version": "==0.0.13"
         },
         "botocore": {
             "hashes": [
-                "sha256:1517c52eaa3056d0e81f9a81b580d7f28440e7e1523d10a8acc8160c56be7113",
-                "sha256:19d9d56fcf4f16ffea8a929bbf3c72db3458b6c1f306c04031f3166759cd62ac"
+                "sha256:5e4774c106bb02f8e4639818c2f8157b8ec114a76e481e17cd3fe6955206e088",
+                "sha256:cfc667e7888aad09ead8f7e32129ea90aa5c7f602531094954bf6305db74aac4"
             ],
-            "version": "==1.12.146"
+            "version": "==1.12.151"
         },
         "certifi": {
             "hashes": [
                 "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5",
                 "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae"
             ],
             "version": "==2019.3.9"
         },
@@ -365,21 +365,21 @@
                 "sha256:36784bf8ae766e14f9db0e377ccfa02835d648321d2007b6ae0bf4fd612c0f94",
                 "sha256:71161cb0e928d824092a5f16203939bbc0867ce4c4685db263cf22c3ae7634a8"
             ],
             "index": "pypi",
             "version": "==2.0.3"
         },
         "requests": {
             "hashes": [
-                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
-                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
+                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
+                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
             ],
             "index": "pypi",
-            "version": "==2.21.0"
+            "version": "==2.22.0"
         },
         "rsa": {
             "hashes": [
                 "sha256:25df4e10c263fb88b5ace923dd84bf9aa7f5019687b5e55382ffcdb8bede9db5",
                 "sha256:43f682fea81c452c98d09fc316aae12de6d30c4b5c84226642cf8f8fd1c93abd"
             ],
             "version": "==3.4.2"
         },
@@ -387,21 +387,21 @@
             "hashes": [
                 "sha256:7b9ad3213bff7d357f888e0fab5101b56fa1a0548ee77d121c3a3dbfbef4cb2e",
                 "sha256:f23d5cb7d862b104401d9021fc82e5fa0e0cf57b7660a1331425aab0c691d021"
             ],
             "version": "==0.2.0"
         },
         "scriptworker": {
             "hashes": [
-                "sha256:44b19ef0ddfe14309ddb035e4f6e82da8d9eb3d7c3de8ed82ee74a75beefb767",
-                "sha256:4b7bd567c8b511f1a87c68ac541c94b730d6f307ad86bb0af279ac30ef5867e9"
+                "sha256:836181e36befcd74bb6b9457fd9336d8efa1350e77c285f0dc32bdb0ef6e4270",
+                "sha256:d858c4e0dae3305dec3683458d2e879752b0a3645e9f95278b717d53d45c2809"
             ],
             "index": "pypi",
-            "version": "==23.0.4"
+            "version": "==23.0.5"
         },
         "sh": {
             "hashes": [
                 "sha256:ae3258c5249493cebe73cb4e18253a41ed69262484bad36fdb3efcb8ad8870bb",
                 "sha256:b52bf5833ed01c7b5c5fb73a7f71b3d98d48e9b9b8764236237bdc7ecae850fc"
             ],
             "index": "pypi",
             "version": "==1.12.14"
--- a/taskcluster/docs/kinds.rst
+++ b/taskcluster/docs/kinds.rst
@@ -362,29 +362,36 @@ Publishes signed langpacks to archive.mo
 
 release-beetmover-signed-langpacks-checksums
 --------------------------------------------
 Publishes signed langpacks to archive.mozilla.org
 
 release-update-verify
 ---------------------
 Verifies the contents and package of release update MARs.
-
 release-secondary-update-verify
 -------------------------------
 Verifies the contents and package of release update MARs.
 
+release-update-verify-next
+--------------------------
+Verifies the contents and package of release and updare MARs from the previous ESR release.
+
 release-update-verify-config
 ----------------------------
 Creates configs for release-update-verify tasks
 
 release-secondary-update-verify-config
 --------------------------------------
 Creates configs for release-secondary-update-verify tasks
 
+release-update-verify-config-next
+---------------------------------
+Creates configs for release-update-verify-next tasks
+
 release-updates-builder
 -----------------------
 Top level Balrog blob submission & patcher/update verify config updates.
 
 release-version-bump
 --------------------
 Bumps to the next version.
 
--- a/taskcluster/taskgraph/manifests/firefox_candidates.yml
+++ b/taskcluster/taskgraph/manifests/firefox_candidates.yml
@@ -327,25 +327,37 @@ mapping:
             - repackage-signing-msi
         only_for_platforms:
             - win64-shippable
             - win32-shippable
         pretty_name: Firefox Setup ${version}.msi
         checksums_path: ${path_platform}/${locale}/Firefox Setup ${version}.msi
     target.complete.mar:
         <<: *default
-        description: "The main installer we ship our mobile products baked within"
+        description: "Complete MAR to serve as updates"
         all_locales: true
         from:
             - mar-signing
         pretty_name: firefox-${version}.complete.mar
         checksums_path: update/${path_platform}/${locale}/firefox-${version}.complete.mar
         update_balrog_manifest: true
         destinations:
             - ${version}-candidates/build${build_number}/update/${path_platform}
+    target.bz2.complete.mar:
+        <<: *default
+        description: "Complete MAR with bz2 compression and SHA1 signing to serve as updates"
+        all_locales: true
+        from:
+            - mar-signing
+        pretty_name: firefox-${version}.bz2.complete.mar
+        checksums_path: update/${path_platform}/${locale}/firefox-${version}.bz2.complete.mar
+        update_balrog_manifest: true
+        balrog_format: bz2
+        destinations:
+            - ${version}-candidates/build${build_number}/update/${path_platform}
     ${partial}:
         <<: *default
         description: "Partials MAR files to serve as updates"
         all_locales: true
         from:
             - partials-signing
         partials_only: true
         pretty_name: firefox-${previous_version}-${version}.partial.mar
--- a/taskcluster/taskgraph/transforms/beetmover.py
+++ b/taskcluster/taskgraph/transforms/beetmover.py
@@ -310,31 +310,31 @@ def make_task_worker(config, jobs):
             if 'signing' in dependency:
                 signing_task = dependency
             else:
                 build_task = dependency
 
         signing_task_ref = "<" + str(signing_task) + ">"
         build_task_ref = "<" + str(build_task) + ">"
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform, locale
             )
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 job, signing_task_ref, build_task_ref, platform, locale
             )
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
             'upstream-artifacts': upstream_artifacts,
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform, locale=locale)
 
         if locale:
             worker["locale"] = locale
         job["worker"] = worker
 
         yield job
--- a/taskcluster/taskgraph/transforms/beetmover_checksums.py
+++ b/taskcluster/taskgraph/transforms/beetmover_checksums.py
@@ -140,17 +140,17 @@ def make_beetmover_checksums_worker(conf
             "signing": "<checksums-signing>",
         }
 
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform, locale
             )
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform, locale=locale)
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 refs, platform, locale
--- a/taskcluster/taskgraph/transforms/beetmover_langpack_checksums.py
+++ b/taskcluster/taskgraph/transforms/beetmover_langpack_checksums.py
@@ -133,17 +133,17 @@ def make_beetmover_checksums_worker(conf
             raise NotImplementedError(
                 "Beetmover checksums must have a beetmover dependency!")
 
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform, locales
             )
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform, locale=locales)
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 refs, platform, locales
--- a/taskcluster/taskgraph/transforms/beetmover_repackage.py
+++ b/taskcluster/taskgraph/transforms/beetmover_repackage.py
@@ -353,32 +353,32 @@ least 2 regular expressions. First match
 
 
 @transforms.add
 def make_task_worker(config, jobs):
     for job in jobs:
         locale = job["attributes"].get("locale")
         platform = job["attributes"]["build_platform"]
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform, locale)
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 config, job, job['dependencies'], platform, locale,
                 project=config.params['project']
             )
 
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
             'upstream-artifacts': upstream_artifacts,
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform, locale=locale)
 
         if locale:
             worker["locale"] = locale
         job["worker"] = worker
 
         yield job
@@ -407,17 +407,17 @@ def make_partials_artifacts(config, jobs
 
         job['worker']['upstream-artifacts'].extend(upstream_artifacts)
 
         extra = list()
 
         partials_info = get_partials_info_from_params(
             config.params.get('release_history'), balrog_platform, locale)
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             job['worker']['artifact-map'].extend(
                 generate_beetmover_partials_artifact_map(
                     config, job, partials_info, platform=platform, locale=locale))
 
         for artifact in partials_info:
             artifact_extra = {
                 'locale': locale,
                 'artifact_name': artifact,
--- a/taskcluster/taskgraph/transforms/beetmover_source_checksums.py
+++ b/taskcluster/taskgraph/transforms/beetmover_source_checksums.py
@@ -132,29 +132,29 @@ def make_beetmover_checksums_worker(conf
             if dependency.startswith("beetmover"):
                 refs['beetmover'] = "<{}>".format(dependency)
             else:
                 refs['signing'] = "<{}>".format(dependency)
         if None in refs.values():
             raise NotImplementedError(
                 "Beetmover checksums must have a beetmover and signing dependency!")
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(config,
                                                                        job, platform, locale)
         else:
             upstream_artifacts = generate_upstream_artifacts(refs, platform, locale)
 
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
             'upstream-artifacts': upstream_artifacts,
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform)
 
         if locale:
             worker["locale"] = locale
         job["worker"] = worker
 
         yield job
--- a/taskcluster/taskgraph/transforms/bouncer_submission.py
+++ b/taskcluster/taskgraph/transforms/bouncer_submission.py
@@ -126,17 +126,18 @@ def make_task_worker(config, jobs):
             job, 'worker-type', item_name=job['name'],
             **{'release-level': config.params.release_level()}
         )
         resolve_keyed_by(
             job, 'scopes', item_name=job['name'],
             **{'release-level': config.params.release_level()}
         )
         resolve_keyed_by(
-            job, 'bouncer-products', item_name=job['name'], project=config.params['project']
+            job, 'bouncer-products', item_name=job['name'],
+            **{'release-type': config.params['release_type']}
         )
 
         # No need to filter out ja-JP-mac, we need to upload both; but we do
         # need to filter out the platforms they come with
         all_locales = sorted([
             locale
             for locale in parse_locales_file(job['locales-file']).keys()
             if locale not in ('linux', 'win32', 'osx')
--- a/taskcluster/taskgraph/transforms/release_beetmover_signed_addons.py
+++ b/taskcluster/taskgraph/transforms/release_beetmover_signed_addons.py
@@ -120,31 +120,31 @@ def make_task_description(config, jobs):
 def make_task_worker(config, jobs):
     for job in jobs:
         signing_task_ref = get_upstream_task_ref(
             job, expected_kinds=('release-sign-and-push-langpacks',)
         )
 
         platform = job["attributes"]["build_platform"]
         locale = job["attributes"]["chunk_locales"]
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform, locale,
             )
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 signing_task_ref, job['attributes']['chunk_locales']
             )
         job['worker'] = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
             'upstream-artifacts': upstream_artifacts,
         }
 
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             job['worker']['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform, locale=locale)
 
         yield job
 
 
 def generate_upstream_artifacts(upstream_task_ref, locales):
     return [{
@@ -227,17 +227,17 @@ def _change_platform_data(config, platfo
     platform_job['label'] = platform_job['label'].replace(orig_platform, platform)
     platform_job['description'] = platform_job['description'].replace(orig_platform, platform)
     platform_job['treeherder']['platform'] = platform_job['treeherder']['platform'].replace(
         orig_platform, platform
     )
     platform_job['worker']['release-properties']['platform'] = platform
 
     # amend artifactMap entries as well
-    if should_use_artifact_map(backup_platform, config.params['project']):
+    if should_use_artifact_map(backup_platform):
         platform_mapping = {
             'linux64': 'linux-x86_64',
             'linux': 'linux-i686',
             'macosx64': 'mac',
             'win32': 'win32',
             'win64': 'win64',
         }
         orig_platform = platform_mapping.get(orig_platform, orig_platform)
--- a/taskcluster/taskgraph/transforms/release_generate_checksums_beetmover.py
+++ b/taskcluster/taskgraph/transforms/release_generate_checksums_beetmover.py
@@ -151,27 +151,27 @@ def make_task_worker(config, jobs):
 
         worker = {
             'implementation': 'beetmover',
             'release-properties': craft_release_properties(config, job),
         }
 
         platform = job["attributes"]["build_platform"]
         # Works with Firefox/Devedition. Commented for migration.
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             upstream_artifacts = generate_beetmover_upstream_artifacts(
                 config, job, platform=None, locale=None
             )
         else:
             upstream_artifacts = generate_upstream_artifacts(
                 job, signing_task_ref, build_task_ref
             )
 
         worker['upstream-artifacts'] = upstream_artifacts
 
         # Works with Firefox/Devedition. Commented for migration.
-        if should_use_artifact_map(platform, config.params['project']):
+        if should_use_artifact_map(platform):
             worker['artifact-map'] = generate_beetmover_artifact_map(
                 config, job, platform=platform)
 
         job["worker"] = worker
 
         yield job
--- a/taskcluster/taskgraph/transforms/update_verify.py
+++ b/taskcluster/taskgraph/transforms/update_verify.py
@@ -14,17 +14,17 @@ from taskgraph.util.treeherder import ad
 
 transforms = TransformSequence()
 
 
 @transforms.add
 def add_command(config, tasks):
     config_tasks = {}
     for dep in config.kind_dependencies_tasks:
-        if 'update-verify-config' in dep.kind:
+        if 'update-verify-config' in dep.kind or 'update-verify-next-config' in dep.kind:
             config_tasks[dep.name] = dep
 
     for task in tasks:
         config_task = config_tasks[task['name']]
         total_chunks = task["extra"]["chunks"]
         task['worker'].setdefault('env', {}).update(
             CHANNEL=config_task.task['extra']['channel'],
         )
--- a/taskcluster/taskgraph/transforms/update_verify_config.py
+++ b/taskcluster/taskgraph/transforms/update_verify_config.py
@@ -28,16 +28,18 @@ transforms = TransformSequence()
 INCLUDE_VERSION_REGEXES = {
     "beta": r"'^(\d+\.\d+(b\d+)?)$'",
     "nonbeta": r"'^\d+\.\d+(\.\d+)?$'",
     # Same as beta, except excludes 58.0b1 due to issues with it not being able
     # to update to latest
     "devedition_hack": r"'^((?!58\.0b1$)\d+\.\d+(b\d+)?)$'",
     # Same as nonbeta, except for the esr suffix
     "esr": r"'^\d+\.\d+(\.\d+)?esr$'",
+    # Previous esr versions, for update testing before we update users to esr68
+    "esr68-next": r"'^(52|60)+\.\d+(\.\d+)?esr$'",
 }
 
 MAR_CHANNEL_ID_OVERRIDE_REGEXES = {
     "beta": r"'^\d+\.\d+(\.\d+)?$$,firefox-mozilla-beta,firefox-mozilla-release'",
 }
 
 
 @transforms.add
--- a/taskcluster/taskgraph/util/scriptworker.py
+++ b/taskcluster/taskgraph/util/scriptworker.py
@@ -422,18 +422,20 @@ def generate_beetmover_upstream_artifact
 
     Returns:
         list: A list of dictionaries conforming to the upstream_artifacts spec.
     """
     base_artifact_prefix = get_artifact_prefix(job)
     resolve_keyed_by(
         job, 'attributes.artifact_map',
         'artifact map',
-        project=config.params['project'],
-        platform=platform
+        **{
+            'release-type': config.params['release_type'],
+            'platform': platform,
+        }
     )
     map_config = deepcopy(cached_load_yaml(job['attributes']['artifact_map']))
     upstream_artifacts = list()
 
     if not locale:
         locales = map_config['default_locales']
     elif isinstance(locale, list):
         locales = locale
@@ -464,16 +466,21 @@ def generate_beetmover_upstream_artifact
             resolve_keyed_by(file_config, "source_path_modifier",
                              'source path modifier', locale=locale)
             paths.append(os.path.join(
                 base_artifact_prefix,
                 jsone.render(file_config['source_path_modifier'], {'locale': locale}),
                 filename,
             ))
 
+        if getattr(job['dependencies'][dep], 'release_artifacts', None):
+            paths = [
+                path for path in paths
+                if path in job['dependencies'][dep].release_artifacts]
+
         if not paths:
             continue
 
         upstream_artifacts.append({
             "taskId": {
                 "task-reference": "<{}>".format(dep)
             },
             "taskType": map_config['tasktype_map'].get(dep),
@@ -551,18 +558,20 @@ def generate_beetmover_artifact_map(conf
     Returns:
         list: A list of dictionaries containing source->destination
             maps for beetmover.
     """
     platform = kwargs.get('platform', '')
     resolve_keyed_by(
         job, 'attributes.artifact_map',
         'artifact map',
-        project=config.params['project'],
-        platform=platform
+        **{
+            'release-type': config.params['release_type'],
+            'platform': platform,
+        }
     )
     map_config = deepcopy(cached_load_yaml(job['attributes']['artifact_map']))
     base_artifact_prefix = map_config.get('base_artifact_prefix', get_artifact_prefix(job))
 
     artifacts = list()
 
     dependencies = job['dependencies'].keys()
 
@@ -692,18 +701,20 @@ def generate_beetmover_partials_artifact
     Returns:
         list: A list of dictionaries containing source->destination
             maps for beetmover.
     """
     platform = kwargs.get('platform', '')
     resolve_keyed_by(
         job, 'attributes.artifact_map',
         'artifact map',
-        project=config.params['project'],
-        platform=platform
+        **{
+            'release-type': config.params['release_type'],
+            'platform': platform,
+        }
     )
     map_config = deepcopy(cached_load_yaml(job['attributes']['artifact_map']))
     base_artifact_prefix = map_config.get('base_artifact_prefix', get_artifact_prefix(job))
 
     artifacts = list()
     dependencies = job['dependencies'].keys()
 
     if kwargs.get('locale'):
@@ -806,53 +817,15 @@ def generate_beetmover_partials_artifact
             'taskId': {'task-reference': "<{}>".format(dep)},
             'locale': locale,
             'paths': paths,
         })
 
     return artifacts
 
 
-# should_use_artifact_map {{{
-def should_use_artifact_map(platform, project):
+def should_use_artifact_map(platform):
     """Return True if this task uses the beetmover artifact map.
 
     This function exists solely for the beetmover artifact map
     migration.
     """
-    if 'linux64-snap-shippable' in platform:
-        # Snap has never been implemented outside of declarative artifacts. We need to use
-        # declarative artifacts no matter the branch we're on
-        return True
-
-    # FIXME: once we're ready to switch fully to declarative artifacts on other
-    # branches, we can expand this; for now, Fennec is rolled-out to all
-    # release branches, while Firefox only to mozilla-central
-    platforms = [
-        'android',
-        'fennec'
-    ]
-    projects = ['mozilla-central', 'mozilla-beta', 'mozilla-release']
-    if any([pl in platform for pl in platforms]) and any([pj == project for pj in projects]):
-        return True
-
-    platforms = [
-        'linux',    # needed for beetmover-langpacks-checksums
-        'linux64',  # which inherit amended platform from their beetmover counterpart
-        'win32',
-        'win64',
-        'macosx64',
-        'linux-shippable',
-        'linux64-shippable',
-        'macosx64-shippable',
-        'win32-shippable',
-        'win64-shippable',
-        'win64-aarch64-shippable',
-        'win64-asan-reporter-nightly',
-        'linux64-asan-reporter-nightly',
-        'firefox-source',
-        'firefox-release',
-    ]
-    projects = ['mozilla-central', 'mozilla-beta', 'mozilla-release']
-    if any([pl == platform for pl in platforms]) and any([pj == project for pj in projects]):
-        return True
-
-    return False
+    return 'devedition' not in platform
--- a/testing/marionette/harness/marionette_harness/runtests.py
+++ b/testing/marionette/harness/marionette_harness/runtests.py
@@ -86,16 +86,16 @@ def cli(runner_class=MarionetteTestRunne
     """
     logger = mozlog.commandline.setup_logging('Marionette test runner', {})
     try:
         harness_instance = harness_class(runner_class, parser_class, testcase_class,
                                          args=args)
         failed = harness_instance.run()
         if failed > 0:
             sys.exit(10)
-    except Exception:
-        logger.error('Failure during harness execution', exc_info=True)
+    except Exception as e:
+        logger.error(e.message, exc_info=True)
         sys.exit(1)
     sys.exit(0)
 
 
 if __name__ == "__main__":
     cli()
--- a/testing/mozharness/configs/releases/bouncer_firefox_esr.py
+++ b/testing/mozharness/configs/releases/bouncer_firefox_esr.py
@@ -39,16 +39,28 @@ config = {
             "platforms": [
                 "linux",
                 "linux64",
                 "osx",
                 "win",
                 "win64",
             ],
         },
+        "complete-mar-bz2": {
+            "product-name": "Firefox-%(version)s-Complete-bz2",
+            "check_uptake": True,
+            "platforms": [
+                "linux",
+                "linux64",
+                "osx",
+                "win",
+                "win64",
+                "win64-aarch64",
+            ],
+        },
     },
     "partials": {
         "releases-dir": {
             "product-name": "Firefox-%(version)s-Partial-%(prev_version)s",
             "check_uptake": True,
             "platforms": [
                 "linux",
                 "linux64",
deleted file mode 100644
--- a/testing/web-platform/meta/html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/disallow-crossorigin.html.ini
+++ /dev/null
@@ -1,5 +0,0 @@
-[disallow-crossorigin.html]
-  expected: ERROR
-  [Promise rejection event should be muted for cross-origin non-CORS script]
-    expected: FAIL
-
--- a/testing/web-platform/meta/web-animations/__dir__.ini
+++ b/testing/web-platform/meta/web-animations/__dir__.ini
@@ -1,1 +1,1 @@
-prefs: [dom.animations-api.compositing.enabled:true, dom.animations-api.core.enabled:true, dom.animations-api.getAnimations.enabled:true, dom.animations-api.implicit-keyframes.enabled:true, dom.animations-api.timelines.enabled:true, layout.css.step-position-jump.enabled:true]
+prefs: [dom.animations-api.autoremove.enabled:true, dom.animations-api.compositing.enabled:true, dom.animations-api.core.enabled:true, dom.animations-api.getAnimations.enabled:true, dom.animations-api.implicit-keyframes.enabled:true, dom.animations-api.timelines.enabled:true, layout.css.step-position-jump.enabled:true]
--- a/testing/web-platform/tests/html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/support/promise-rejection-events.js
+++ b/testing/web-platform/tests/html/webappapis/scripting/processing-model-2/unhandled-promise-rejections/support/promise-rejection-events.js
@@ -157,16 +157,34 @@ async_test(function(t) {
   var e = new Error();
   var p;
 
   onUnhandledSucceed(t, e, function() { return p; });
 
   p = Promise.all([Promise.reject(e)]);
 }, 'unhandledrejection: from Promise.reject, indirected through Promise.all');
 
+async_test(function(t) {
+  var p;
+
+  var unhandled = function(ev) {
+    if (ev.promise === p) {
+      t.step(function() {
+        assert_equals(ev.reason.name, 'InvalidStateError');
+        assert_equals(ev.promise, p);
+      });
+      t.done();
+    }
+  };
+  addEventListener('unhandledrejection', unhandled);
+  ensureCleanup(t, unhandled);
+
+  p = createImageBitmap(new Blob());
+}, 'unhandledrejection: from createImageBitmap which is UA triggered');
+
 //
 // Negative unhandledrejection/rejectionhandled tests with immediate attachment
 //
 
 async_test(function(t) {
   var e = new Error();
   var p;
 
@@ -265,16 +283,26 @@ async_test(function(t) {
     p = Promise.resolve().then(function() {
       return Promise.reject(e);
     })
     .catch(function() {});
   });
 }, 'no unhandledrejection/rejectionhandled: all inside a queued task, a rejection handler attached synchronously to ' +
    'a promise created from returning a Promise.reject-created promise in a fulfillment handler');
 
+async_test(function(t) {
+  var p;
+
+  onUnhandledFail(t, function() { return p; });
+
+  var unreached = t.unreached_func('promise should not be fulfilled');
+  p = createImageBitmap(new Blob()).then(unreached, function() {});
+}, 'no unhandledrejection/rejectionhandled: rejection handler attached synchronously to a promise created from ' +
+   'createImageBitmap');
+
 //
 // Negative unhandledrejection/rejectionhandled tests with microtask-delayed attachment
 //
 
 async_test(function(t) {
   var e = new Error();
   var p;
 
@@ -654,16 +682,53 @@ async_test(function(t) {
     var unreached = t.unreached_func('promise should not be fulfilled');
     p.then(unreached, function(reason) {
       assert_equals(reason, e);
       setTimeout(function() { t.done(); }, 10);
     });
   }, 10);
 }, 'delayed handling: delaying handling by setTimeout(,10) will cause both events to fire');
 
+async_test(function(t) {
+  var unhandledPromises = [];
+  var unhandledReasons = [];
+  var p;
+
+  var unhandled = function(ev) {
+    if (ev.promise === p) {
+      t.step(function() {
+        unhandledPromises.push(ev.promise);
+        unhandledReasons.push(ev.reason.name);
+      });
+    }
+  };
+  var handled = function(ev) {
+    if (ev.promise === p) {
+      t.step(function() {
+        assert_array_equals(unhandledPromises, [p]);
+        assert_array_equals(unhandledReasons, ['InvalidStateError']);
+        assert_equals(ev.promise, p);
+        assert_equals(ev.reason.name, 'InvalidStateError');
+      });
+    }
+  };
+  addEventListener('unhandledrejection', unhandled);
+  addEventListener('rejectionhandled', handled);
+  ensureCleanup(t, unhandled, handled);
+
+  p = createImageBitmap(new Blob());
+  setTimeout(function() {
+    var unreached = t.unreached_func('promise should not be fulfilled');
+    p.then(unreached, function(reason) {
+      assert_equals(reason.name, 'InvalidStateError');
+      setTimeout(function() { t.done(); }, 10);
+    });
+  }, 10);
+}, 'delayed handling: delaying handling rejected promise created from createImageBitmap will cause both events to fire');
+
 //
 // Miscellaneous tests about integration with the rest of the platform
 //
 
 async_test(function(t) {
   var e = new Error();
   var l = function(ev) {
     var order = [];
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/keyframe-effects/effect-value-replaced-animations.html
@@ -0,0 +1,161 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>The effect value of a keyframe effect: Overlapping keyframes</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#the-effect-value-of-a-keyframe-animation-effect">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_opacity_value(opacity, expected, description) {
+  return assert_approx_equals(
+    parseFloat(opacity),
+    expected,
+    0.0001,
+    description
+  );
+}
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3 },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  // Sanity check
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+
+  // animA is now removed so if we cancel animB, we should go back to the
+  // underlying value
+  animB.cancel();
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.1,
+    'Opacity should be the un-animated value'
+  );
+}, 'Removed animations do not contribute to animated style');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  // Sanity check
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+
+  // animA has been removed so the final result should be 0.1 + 0.3 = 0.4.
+  // (If animA were not removed it would be 0.2 + 0.3 = 0.5.)
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.4,
+    'Opacity value should not include the removed animation'
+  );
+}, 'Removed animations do not contribute to the effect stack');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  animA.persist();
+
+  animB.cancel();
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.2,
+    "Opacity should be the persisted animation's value"
+  );
+}, 'Persisted animations contribute to animated style');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.4,
+    'Opacity value should NOT include the contribution of the removed animation'
+  );
+
+  animA.persist();
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.5,
+    'Opacity value should include the contribution of the persisted animation'
+  );
+}, 'Persisted animations contribute to the effect stack');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  // Persist the animation before it finishes (and before it would otherwise be
+  // removed).
+  animA.persist();
+
+  const animB = div.animate(
+    { opacity: 0.3, composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  assert_opacity_value(
+    getComputedStyle(div).opacity,
+    0.5,
+    'Opacity value should include the contribution of the persisted animation'
+  );
+}, 'Animations persisted before they would be removed contribute to the'
+   + ' effect stack');
+
+</script>
+</body>
--- a/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
+++ b/testing/web-platform/tests/web-animations/interfaces/Animatable/getAnimations.html
@@ -206,16 +206,38 @@ test(t => {
   assert_array_equals(div.getAnimations(), [],
                       'Animation should not be returned after seeking to the'
                       + ' clipped end of the active interval');
 }, 'Returns animations based on dynamic changes to individual'
    + ' animations\' current time');
 
 promise_test(async t => {
   const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_array_equals(div.getAnimations(), [animB]);
+}, 'Does not return an animation that has been removed');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  animA.persist();
+
+  assert_array_equals(div.getAnimations(), [animA, animB]);
+}, 'Returns an animation that has been persisted');
+
+promise_test(async t => {
+  const div = createDiv(t);
   const watcher = EventWatcher(t, div, 'transitionrun');
 
   // Create a covering animation to prevent transitions from firing after
   // calling getAnimations().
   const coveringAnimation = new Animation(
     new KeyframeEffect(div, { opacity: [0, 1] }, 100 * MS_PER_SEC)
   );
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/commitStyles.html
@@ -0,0 +1,389 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation.commitStyles</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-commitstyles">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+function assert_numeric_style_equals(opacity, expected, description) {
+  return assert_approx_equals(
+    parseFloat(opacity),
+    expected,
+    0.0001,
+    description
+  );
+}
+
+test(t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animation = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+
+  animation.commitStyles();
+
+  // Cancel the animation so we can inspect the underlying style
+  animation.cancel();
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2);
+}, 'Commits styles');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: 0.3 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  animB.cancel();
+
+  animA.commitStyles();
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.2);
+}, 'Commits styles for an animation that has been removed');
+
+test(t => {
+  const div = createDiv(t);
+  div.style.margin = '10px';
+
+  const animation = div.animate(
+    { margin: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+
+  animation.commitStyles();
+
+  animation.cancel();
+
+  assert_equals(div.style.marginLeft, '20px');
+}, 'Commits shorthand styles');
+
+test(t => {
+  const div = createDiv(t);
+  div.style.marginLeft = '10px';
+
+  const animation = div.animate(
+    { marginInlineStart: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+
+  animation.commitStyles();
+
+  animation.cancel();
+
+  assert_equals(div.style.marginLeft, '20px');
+}, 'Commits logical properties');
+
+test(t => {
+  const div = createDiv(t);
+  div.style.marginLeft = '10px';
+
+  const animation = div.animate({ opacity: [0.2, 0.7] }, 1000);
+  animation.currentTime = 500;
+  animation.commitStyles();
+  animation.cancel();
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.45);
+}, 'Commits values calculated mid-interval');
+
+test(t => {
+  const div = createDiv(t);
+  div.style.setProperty('--target', '0.5');
+
+  const animation = div.animate(
+    { opacity: 'var(--target)' },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+  animation.commitStyles();
+  animation.cancel();
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5);
+
+  // Changes to the variable should have no effect
+  div.style.setProperty('--target', '1');
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.5);
+}, 'Commits variables as their computed values');
+
+test(t => {
+  const div = createDiv(t);
+  div.style.fontSize = '10px';
+
+  const animation = div.animate(
+    { width: '10em' },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+  animation.commitStyles();
+  animation.cancel();
+
+  assert_numeric_style_equals(getComputedStyle(div).width, 100);
+
+  // Changes to the font-size should have no effect
+  div.style.fontSize = '20px';
+
+  assert_numeric_style_equals(getComputedStyle(div).width, 100);
+}, 'Commits em units as pixel values');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  const animA = div.animate(
+    { opacity: '0.2' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { opacity: '0.2', composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animC = div.animate(
+    { opacity: '0.3', composite: 'add' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  animA.persist();
+  animB.persist();
+
+  await animB.finished;
+
+  // The values above have been chosen such that various error conditions
+  // produce results that all differ from the desired result:
+  //
+  //  Expected result:
+  //
+  //    animA + animB = 0.4
+  //
+  //  Likely error results:
+  //
+  //    <underlying> = 0.1
+  //    (Commit didn't work at all)
+  //
+  //    animB = 0.2
+  //    (Didn't add at all when resolving)
+  //
+  //    <underlying> + animB = 0.3
+  //    (Added to the underlying value instead of lower-priority animations when
+  //    resolving)
+  //
+  //    <underlying> + animA + animB = 0.5
+  //    (Didn't respect the composite mode of lower-priority animations)
+  //
+  //    animA + animB + animC = 0.7
+  //    (Resolved the whole stack, not just up to the target effect)
+  //
+
+  animB.commitStyles();
+
+  animA.cancel();
+  animB.cancel();
+  animC.cancel();
+
+  assert_numeric_style_equals(getComputedStyle(div).opacity, 0.4);
+}, 'Commits the intermediate value of an animation in the middle of stack');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.1';
+
+  // Setup animation
+  const animation = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+
+  // Setup observer
+  const mutationRecords = [];
+  const observer = new MutationObserver(mutations => {
+    mutationRecords.push(...mutations);
+  });
+  observer.observe(div, { attributes: true, attributeOldValue: true });
+
+  animation.commitStyles();
+
+  // Wait for mutation records to be dispatched
+  await Promise.resolve();
+
+  assert_equals(mutationRecords.length, 1, 'Should have one mutation record');
+
+  const mutation = mutationRecords[0];
+  assert_equals(mutation.type, 'attributes');
+  assert_equals(mutation.oldValue, 'opacity: 0.1;');
+
+  observer.disconnect();
+}, 'Triggers mutation observers when updating style');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.opacity = '0.2';
+
+  // Setup animation
+  const animation = div.animate(
+    { opacity: 0.2 },
+    { duration: 1, fill: 'forwards' }
+  );
+  animation.finish();
+
+  // Setup observer
+  const mutationRecords = [];
+  const observer = new MutationObserver(mutations => {
+    mutationRecords.push(...mutations);
+  });
+  observer.observe(div, { attributes: true });
+
+  animation.commitStyles();
+
+  // Wait for mutation records to be dispatched
+  await Promise.resolve();
+
+  assert_equals(mutationRecords.length, 0, 'Should have no mutation records');
+
+  observer.disconnect();
+}, 'Does NOT trigger mutation observers when the change to style is redundant');
+
+test(t => {
+  const pseudo = getPseudoElement(t, 'before');
+  const animation = pseudo.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  assert_throws('NoModificationAllowedError', () => {
+    animation.commitStyles();
+  });
+}, 'Throws if the target element is a pseudo element');
+
+test(t => {
+  const animation = createDiv(t).animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  const nonStyleElement
+    = document.createElementNS('http://example.org/test', 'test');
+  document.body.appendChild(nonStyleElement);
+  animation.effect.target = nonStyleElement;
+
+  assert_throws('NoModificationAllowedError', () => {
+    animation.commitStyles();
+  });
+
+  nonStyleElement.remove();
+}, 'Throws if the target element is not something with a style attribute');
+
+test(t => {
+  const div = createDiv(t);
+  const animation = div.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  div.style.display = 'none';
+
+  assert_throws('InvalidStateError', () => {
+    animation.commitStyles();
+  });
+}, 'Throws if the target effect is display:none');
+
+test(t => {
+  const container = createDiv(t);
+  const div = createDiv(t);
+  container.append(div);
+
+  const animation = div.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  container.style.display = 'none';
+
+  assert_throws('InvalidStateError', () => {
+    animation.commitStyles();
+  });
+}, "Throws if the target effect's ancestor is display:none");
+
+test(t => {
+  const container = createDiv(t);
+  const div = createDiv(t);
+  container.append(div);
+
+  const animation = div.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  container.style.display = 'contents';
+
+  // Should NOT throw
+  animation.commitStyles();
+}, 'Treats display:contents as rendered');
+
+test(t => {
+  const container = createDiv(t);
+  const div = createDiv(t);
+  container.append(div);
+
+  const animation = div.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  div.style.display = 'contents';
+  container.style.display = 'none';
+
+  assert_throws('InvalidStateError', () => {
+    animation.commitStyles();
+  });
+}, 'Treats display:contents in a display:none subtree as not rendered');
+
+test(t => {
+  const div = createDiv(t);
+  const animation = div.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  div.remove();
+
+  assert_throws('InvalidStateError', () => {
+    animation.commitStyles();
+  });
+}, 'Throws if the target effect is disconnected');
+
+test(t => {
+  const pseudo = getPseudoElement(t, 'before');
+  const animation = pseudo.animate(
+    { opacity: 0 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  pseudo.element.remove();
+
+  assert_throws('NoModificationAllowedError', () => {
+    animation.commitStyles();
+  });
+}, 'Checks the pseudo element condition before the not rendered condition');
+
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/persist.html
@@ -0,0 +1,40 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Animation.persist</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#dom-animation-persist">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<body>
+<div id="log"></div>
+<script>
+'use strict';
+
+async_test(t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  animA.onremove = t.step_func_done(() => {
+    assert_equals(animA.replaceState, 'removed');
+    animA.persist();
+    assert_equals(animA.replaceState, 'persisted');
+  });
+}, 'Allows an animation to be persisted after being removed');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  animA.persist();
+
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'persisted');
+}, 'Allows an animation to be persisted before being removed');
+
+</script>
+</body>
--- a/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
+++ b/testing/web-platform/tests/web-animations/interfaces/Animation/style-change-events.html
@@ -6,36 +6,37 @@
 <script src="/resources/testharness.js"></script>
 <script src="/resources/testharnessreport.js"></script>
 <script src="../../testcommon.js"></script>
 <body>
 <div id="log"></div>
 <script>
 'use strict';
 
-// Test that each property defined in the Animation interface does not produce
-// style change events.
+// Test that each property defined in the Animation interface behaves as
+// expected with regards to whether or not it produces style change events.
 //
 // There are two types of tests:
 //
 //   PlayAnimationTest
 //
 //     For properties that are able to cause the Animation to start affecting
 //     the target CSS property.
 //
 //     This function takes either:
 //
 //     (a) A function that simply "plays" that passed-in Animation (i.e. makes
 //         it start affecting the target CSS property.
 //
 //     (b) An object with the following format:
 //
 //         {
-//            setup: elem => { /* return Animation */ }
-//            test: animation => { /* play |animation| */ }
+//            setup: elem => { /* return Animation */ },
+//            test: animation => { /* play |animation| */ },
+//            shouldFlush: boolean /* optional, defaults to false */
 //         }
 //
 //     If the latter form is used, the setup function should return an Animation
 //     that does NOT (yet) have an in-effect AnimationEffect that affects the
 //     'opacity' property. Otherwise, the transition we use to detect if a style
 //     change event has occurred will never have a chance to be triggered (since
 //     the animated style will clobber both before-change and after-change
 //     style).
@@ -51,53 +52,56 @@
 //    target CSS property.
 //
 //    The shape of the parameter to the UsePropertyTest is identical to the
 //    PlayAnimationTest. The only difference is that the function (or 'test'
 //    function of the object format is used) does not need to play the
 //    animation, but simply needs to get/set the property under test.
 
 const PlayAnimationTest = testFuncOrObj => {
-  let test, setup;
+  let test, setup, shouldFlush;
 
   if (typeof testFuncOrObj === 'function') {
     test = testFuncOrObj;
+    shouldFlush = false;
   } else {
     test = testFuncOrObj.test;
     if (typeof testFuncOrObj.setup === 'function') {
       setup = testFuncOrObj.setup;
     }
+    shouldFlush = !!testFuncOrObj.shouldFlush;
   }
 
   if (!setup) {
     setup = elem =>
       new Animation(
         new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
       );
   }
 
-  return { test, setup };
+  return { test, setup, shouldFlush };
 };
 
 const UsePropertyTest = testFuncOrObj => {
-  const { setup, test } = PlayAnimationTest(testFuncOrObj);
+  const { setup, test, shouldFlush } = PlayAnimationTest(testFuncOrObj);
 
   let coveringAnimation;
   return {
     setup: elem => {
       coveringAnimation = new Animation(
         new KeyframeEffect(elem, { opacity: [0, 1] }, 100 * MS_PER_SEC)
       );
 
       return setup(elem);
     },
     test: animation => {
       test(animation);
       coveringAnimation.play();
     },
+    shouldFlush,
   };
 };
 
 const tests = {
   id: UsePropertyTest(animation => (animation.id = 'yer')),
   get effect() {
     let effect;
     return PlayAnimationTest({
@@ -155,28 +159,36 @@ const tests = {
     animation.currentTime = 0;
   }),
   playbackRate: UsePropertyTest(animation => {
     // Get and set the playbackRate
     animation.playbackRate = animation.playbackRate * 1.1;
   }),
   playState: UsePropertyTest(animation => animation.playState),
   pending: UsePropertyTest(animation => animation.pending),
+  replaceState: UsePropertyTest(animation => animation.replaceState),
   ready: UsePropertyTest(animation => animation.ready),
   finished: UsePropertyTest(animation => {
     // Get the finished Promise
     animation.finished;
   }),
   onfinish: UsePropertyTest(animation => {
     // Get the onfinish member
     animation.onfinish;
 
     // Set the onfinish menber
     animation.onfinish = () => {};
   }),
+  onremove: UsePropertyTest(animation => {
+    // Get the onremove member
+    animation.onremove;
+
+    // Set the onremove menber
+    animation.onremove = () => {};
+  }),
   oncancel: UsePropertyTest(animation => {
     // Get the oncancel member
     animation.oncancel;
 
     // Set the oncancel menber
     animation.oncancel = () => {};
   }),
   cancel: UsePropertyTest({
@@ -220,16 +232,61 @@ const tests = {
       const animation = elem.animate({ opacity: [0.5, 1] }, 100 * MS_PER_SEC);
       animation.finish();
       return animation;
     },
     test: animation => {
       animation.reverse();
     },
   }),
+  persist: PlayAnimationTest({
+    setup: async elem => {
+      // Create an animation whose replaceState is 'removed'.
+      const animA = elem.animate(
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      const animB = elem.animate(
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      await animA.finished;
+      animB.cancel();
+
+      return animA;
+    },
+    test: animation => {
+      animation.persist();
+    },
+  }),
+  commitStyles: PlayAnimationTest({
+    setup: async elem => {
+      // Create an animation whose replaceState is 'removed'.
+      const animA = elem.animate(
+        // It's important to use opacity of '1' here otherwise we'll create a
+        // transition due to updating the specified style whereas the transition
+        // we want to detect is the one from flushing due to calling
+        // commitStyles.
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      const animB = elem.animate(
+        { opacity: 1 },
+        { duration: 1, fill: 'forwards' }
+      );
+      await animA.finished;
+      animB.cancel();
+
+      return animA;
+    },
+    test: animation => {
+      animation.commitStyles();
+    },
+    shouldFlush: true,
+  }),
   get ['Animation constructor']() {
     let originalElem;
     return UsePropertyTest({
       setup: elem => {
         originalElem = elem;
         // Return a dummy animation so the caller has something to wait on
         return elem.animate(null);
       },
@@ -261,47 +318,54 @@ test(() => {
         ' Animation'
     );
   }
 }, 'All property keys are recognized');
 
 for (const key of properties) {
   promise_test(async t => {
     assert_own_property(tests, key, `Should have a test for '${key}' property`);
-    const { setup, test } = tests[key];
+    const { setup, test, shouldFlush } = tests[key];
 
     // Setup target element
     const div = createDiv(t);
     let gotTransition = false;
     div.addEventListener('transitionrun', () => {
       gotTransition = true;
     });
 
     // Setup animation
-    const animation = setup(div);
+    const animation = await setup(div);
 
     // Setup transition start point
     div.style.transition = 'opacity 100s';
     getComputedStyle(div).opacity;
 
     // Update specified style but don't flush
     div.style.opacity = '0.5';
 
     // Trigger the property
     test(animation);
 
     // If the test function produced a style change event it will have triggered
     // a transition.
 
-    // Wait for the animation to start and then for at least one animation
-    // frame to give the transitionrun event a chance to be dispatched.
+    // Wait for the animation to start and then for at least two animation
+    // frames to give the transitionrun event a chance to be dispatched.
     assert_true(
       typeof animation.ready !== 'undefined',
       'Should have a valid animation to wait on'
     );
     await animation.ready;
-    await waitForAnimationFrames(1);
+    await waitForAnimationFrames(2);
 
-    assert_false(gotTransition, 'A transition should NOT have been triggered');
-  }, `Animation.${key} does NOT trigger a style change event`);
+    if (shouldFlush) {
+      assert_true(gotTransition, 'A transition should have been triggered');
+    } else {
+      assert_false(
+        gotTransition,
+        'A transition should NOT have been triggered'
+      );
+    }
+  }, `Animation.${key} produces expected style change events`);
 }
 </script>
 </body>
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events-replacement.html
@@ -0,0 +1,1019 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Update animations and send events (replacement)</title>
+<link rel="help" href="https://drafts.csswg.org/web-animations/#update-animations-and-send-events">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="../../testcommon.js"></script>
+<style>
+@keyframes opacity-animation {
+  to { opacity: 1 }
+}
+</style>
+<div id="log"></div>
+<script>
+'use strict';
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another covers the same properties');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after another animation finishes');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1, width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  const animB = div.animate(
+    { width: '200px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  const animC = div.animate(
+    { opacity: 0.5 },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animC.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+  assert_equals(animC.replaceState, 'active');
+}, 'Removes an animation after multiple other animations finish');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animB.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  // Seek animA to just before it finishes since we want to test the behavior
+  // when the animation finishes by the ticking of the timeline, not by seeking
+  // (that is covered in a separate test).
+
+  animA.currentTime = 99.99 * MS_PER_SEC;
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after it finishes');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.finish();
+
+  // Replacement should not happen until the next time the "update animations
+  // and send events" procedure runs.
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking another animation');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.finish();
+
+  // Replacement should not happen until the next time the "update animations
+  // and send events" procedure runs.
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after seeking it');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, 1);
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect.updateTiming({ fill: 'forwards' });
+
+  // Replacement should not happen until the next time the "update animations
+  // and send events" procedure runs.
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating the fill mode of another animation');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, 1);
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect.updateTiming({ fill: 'forwards' });
+
+  // Replacement should not happen until the next time the "update animations
+  // and send events" procedure runs.
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its fill mode');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, 1);
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect = new KeyframeEffect(
+    div,
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different timing");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, 1);
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect = new KeyframeEffect(
+    div,
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different timing');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+
+  await animA.finished;
+
+  // Set up a timeline that makes animB finished
+  animB.timeline = new DocumentTimeline({
+    originTime:
+      document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+  });
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's timeline");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  await animB.finished;
+
+  // Set up a timeline that makes animA finished
+  animA.timeline = new DocumentTimeline({
+    originTime:
+      document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+  });
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its timeline');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect.setKeyframes({ width: '100px', opacity: 1 });
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's properties");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1, width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { width: '200px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect.setKeyframes({ width: '100px' });
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's properties");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect = new KeyframeEffect(
+    div,
+    { width: '100px', opacity: 1 },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with different properties");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1, width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { width: '200px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect = new KeyframeEffect(
+    div,
+    { width: '100px' },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with different properties');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { marginLeft: '10px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { margin: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation when another animation uses a shorthand');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { margin: '10px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    {
+      marginLeft: '10px',
+      marginTop: '20px',
+      marginRight: '30px',
+      marginBottom: '40px',
+    },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation that uses a shorthand');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { marginLeft: '10px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { marginInlineStart: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { marginInlineStart: '10px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { marginLeft: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation using logical properties');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { marginTop: '10px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animB = div.animate(
+    { marginInlineStart: '20px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  div.style.writingMode = 'vertical-rl';
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation by another animation using logical properties after updating the context');
+
+promise_test(async t => {
+  const divA = createDiv(t);
+  const divB = createDiv(t);
+
+  const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect.target = divA;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect's target");
+
+promise_test(async t => {
+  const divA = createDiv(t);
+  const divB = createDiv(t);
+
+  const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect.target = divB;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating its effect's target");
+
+promise_test(async t => {
+  const divA = createDiv(t);
+  const divB = createDiv(t);
+
+  const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animB.effect = new KeyframeEffect(
+    divA,
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, "Removes an animation after updating another animation's effect to one with a different target");
+
+promise_test(async t => {
+  const divA = createDiv(t);
+  const divB = createDiv(t);
+
+  const animA = divA.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = divB.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  animA.effect = new KeyframeEffect(
+    divB,
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+
+  assert_equals(animA.replaceState, 'active');
+  assert_equals(animB.replaceState, 'active');
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Removes an animation after updating its effect to one with a different target');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.animation = 'opacity-animation 1ms forwards';
+  const cssAnimation = div.getAnimations()[0];
+
+  const scriptAnimation = div.animate(
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  await scriptAnimation.finished;
+
+  assert_equals(cssAnimation.replaceState, 'active');
+  assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS animation tied to markup');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  div.style.animation = 'opacity-animation 1ms forwards';
+  const cssAnimation = div.getAnimations()[0];
+
+  // Break tie to markup
+  div.style.animationName = 'none';
+  assert_equals(cssAnimation.playState, 'idle');
+
+  // Restart animation
+  cssAnimation.play();
+
+  const scriptAnimation = div.animate(
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  await scriptAnimation.finished;
+
+  assert_equals(cssAnimation.replaceState, 'removed');
+  assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS animation no longer tied to markup');
+
+promise_test(async t => {
+  // Setup transition
+  const div = createDiv(t);
+  div.style.opacity = '0';
+  div.style.transition = 'opacity 1ms';
+  getComputedStyle(div).opacity;
+  div.style.opacity = '1';
+  const cssTransition = div.getAnimations()[0];
+  cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+  const scriptAnimation = div.animate(
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  await scriptAnimation.finished;
+
+  assert_equals(cssTransition.replaceState, 'active');
+  assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Does NOT remove a CSS transition tied to markup');
+
+promise_test(async t => {
+  // Setup transition
+  const div = createDiv(t);
+  div.style.opacity = '0';
+  div.style.transition = 'opacity 1ms';
+  getComputedStyle(div).opacity;
+  div.style.opacity = '1';
+  const cssTransition = div.getAnimations()[0];
+  cssTransition.effect.updateTiming({ fill: 'forwards' });
+
+  // Break tie to markup
+  div.style.transitionProperty = 'none';
+  assert_equals(cssTransition.playState, 'idle');
+
+  // Restart transition
+  cssTransition.play();
+
+  const scriptAnimation = div.animate(
+    { opacity: 1 },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  await scriptAnimation.finished;
+
+  assert_equals(cssTransition.replaceState, 'removed');
+  assert_equals(scriptAnimation.replaceState, 'active');
+}, 'Removes a CSS transition no longer tied to markup');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+  const event = await eventWatcher.wait_for('remove');
+
+  assert_equals(event.timelineTime, document.timeline.currentTime);
+  assert_equals(event.currentTime, 1);
+}, 'Dispatches an event when removing');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+  await eventWatcher.wait_for('remove');
+
+  // Check we don't get another event
+  animA.addEventListener(
+    'remove',
+    t.step_func(() => {
+      assert_unreached('remove event should not be fired a second time');
+    })
+  );
+
+  // Restart animation
+  animA.play();
+
+  await waitForNextFrame();
+
+  // Finish animation
+  animA.finish();
+
+  await waitForNextFrame();
+}, 'Does NOT dispatch a remove event twice');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  animB.finish();
+  animB.currentTime = 0;
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's current time");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  animA.finish();
+  animA.currentTime = 0;
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its current time');
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  // Set up a timeline that makes animB finished but then restore it
+  animB.timeline = new DocumentTimeline({
+    originTime:
+      document.timeline.currentTime - 100 * MS_PER_SEC - animB.startTime,
+  });
+  animB.timeline = document.timeline;
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's timeline");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate(
+    { opacity: 1 },
+    { duration: 100 * MS_PER_SEC, fill: 'forwards' }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animB.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  // Set up a timeline that makes animA finished but then restore it
+  animA.timeline = new DocumentTimeline({
+    originTime:
+      document.timeline.currentTime - 100 * MS_PER_SEC - animA.startTime,
+  });
+  animA.timeline = document.timeline;
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, 'Does NOT remove an animation after making a redundant change to its timeline');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { marginLeft: '100px' },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  // Redundant change
+  animB.effect.setKeyframes({ marginLeft: '100px', opacity: 1 });
+  animB.effect.setKeyframes({ marginLeft: '100px' });
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to another animation's effect's properties");
+
+promise_test(async t => {
+  const div = createDiv(t);
+  const animA = div.animate(
+    { marginLeft: '100px' },
+    {
+      duration: 1,
+      fill: 'forwards',
+    }
+  );
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'active');
+
+  // Redundant change
+  animA.effect.setKeyframes({ opacity: 1 });
+  animA.effect.setKeyframes({ marginLeft: '100px' });
+
+  await waitForNextFrame();
+
+  assert_equals(animA.replaceState, 'active');
+}, "Does NOT remove an animation after making a redundant change to its effect's properties");
+
+promise_test(async t => {
+  const div = createDiv(t);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  animB.timeline = new DocumentTimeline();
+
+  await animA.finished;
+
+  // If, for example, we only update the timeline for animA before checking
+  // replacement state, then animB will not be finished and animA will not be
+  // replaced.
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Updates ALL timelines before checking for replacement');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  const events = [];
+  const logEvent = (targetName, eventType) => {
+    events.push(`${targetName}:${eventType}`);
+  };
+
+  animA.addEventListener('finish', () => logEvent('animA', 'finish'));
+  animA.addEventListener('remove', () => logEvent('animA', 'remove'));
+  animB.addEventListener('finish', () => logEvent('animB', 'finish'));
+  animB.addEventListener('remove', () => logEvent('animB', 'remove'));
+
+  await animA.finished;
+
+  // Allow all events to be dispatched
+
+  await waitForNextFrame();
+
+  assert_array_equals(events, [
+    'animA:finish',
+    'animB:finish',
+    'animA:remove',
+  ]);
+}, 'Dispatches remove events after finish events');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  const eventWatcher = new EventWatcher(t, animA, 'remove');
+
+  await animA.finished;
+
+  let rAFReceived = false;
+  requestAnimationFrame(() => (rAFReceived = true));
+
+  await eventWatcher.wait_for('remove');
+
+  assert_false(
+    rAFReceived,
+    'remove event should be fired before requestAnimationFrame'
+  );
+}, 'Fires remove event before requestAnimationFrame');
+
+promise_test(async t => {
+  const div = createDiv(t);
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate(
+    { width: '100px' },
+    { duration: 1, fill: 'forwards' }
+  );
+  const animC = div.animate(
+    { opacity: 0.5, width: '200px' },
+    { duration: 1, fill: 'forwards' }
+  );
+
+  // In the event handler for animA (which should be fired before that of animB)
+  // we make a change to animC so that it no longer covers animB.
+  //
+  // If the remove event for animB is not already queued by this point, it will
+  // fail to fire.
+  animA.addEventListener('remove', () => {
+    animC.effect.setKeyframes({
+      opacity: 0.5,
+    });
+  });
+
+  const eventWatcher = new EventWatcher(t, animB, 'remove');
+  await eventWatcher.wait_for('remove');
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'removed');
+  assert_equals(animC.replaceState, 'active');
+}, 'Queues all remove events before running them');
+
+promise_test(async t => {
+  const outerIframe = createElement(t, 'iframe');
+  outerIframe.width = 10;
+  outerIframe.height = 10;
+
+  await new Promise(resolve => outerIframe.addEventListener('load', resolve));
+
+  const innerIframe = createElement(t, 'iframe', outerIframe.contentDocument);
+  innerIframe.width = 10;
+  innerIframe.height = 10;
+
+  await new Promise(resolve => innerIframe.addEventListener('load', resolve));
+
+  const div = createDiv(t, innerIframe.contentDocument);
+
+  const animA = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+  const animB = div.animate({ opacity: 1 }, { duration: 1, fill: 'forwards' });
+
+  // Sanity check: The timeline for these animations should be the default
+  // document timeline for div.
+  assert_equals(animA.timeline, innerIframe.contentDocument.timeline);
+  assert_equals(animB.timeline, innerIframe.contentDocument.timeline);
+
+  await animA.finished;
+
+  assert_equals(animA.replaceState, 'removed');
+  assert_equals(animB.replaceState, 'active');
+}, 'Performs removal in deeply nested iframes');
+
+</script>
--- a/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
+++ b/testing/web-platform/tests/web-animations/timing-model/timelines/update-and-send-events.html
@@ -218,9 +218,40 @@ promise_test(async t => {
 
   assert_array_equals(receivedEvents.map(event => event.type),
     [ 'finish', 'cancel' ],
     'Calling finish() synchronously queues a finish event when updating the ' +
     'finish state so it should appear before the cancel event');
 }, 'Playback events with the same timeline retain the order in which they are' +
    'queued');
 
+promise_test(async t => {
+  const div = createDiv(t);
+
+  // Create two animations with separate timelines
+
+  const timelineA = document.timeline;
+  const animA = div.animate(null, 100 * MS_PER_SEC);
+
+  const timelineB = new DocumentTimeline();
+  const animB = new Animation(
+    new KeyframeEffect(div, null, 100 * MS_PER_SEC),
+    timelineB
+  );
+  animB.play();
+
+  animA.currentTime = 99.9 * MS_PER_SEC;
+  animB.currentTime = 99.9 * MS_PER_SEC;
+
+  // When the next tick happens both animations should be updated, and we will
+  // notice that they are now finished. As a result their finished promise
+  // callbacks should be queued. All of that should happen before we run the
+  // next microtask checkpoint and actually run the promise callbacks and
+  // hence the calls to cancel should not stop the existing callbacks from
+  // being run.
+
+  animA.finished.then(() => { animB.cancel() });
+  animB.finished.then(() => { animA.cancel() });
+
+  await Promise.all([animA.finished, animB.finished]);
+}, 'All timelines are updated before running microtasks');
+
 </script>
--- a/toolkit/actors/WebNavigationChild.jsm
+++ b/toolkit/actors/WebNavigationChild.jsm
@@ -31,22 +31,17 @@ class WebNavigationChild extends ActorCh
         break;
       case "WebNavigation:GoForward":
         this.goForward(message.data);
         break;
       case "WebNavigation:GotoIndex":
         this.gotoIndex(message.data);
         break;
       case "WebNavigation:LoadURI":
-        let histogram = Services.telemetry.getKeyedHistogramById("FX_TAB_REMOTE_NAVIGATION_DELAY_MS");
-        histogram.add("WebNavigation:LoadURI",
-                      Services.telemetry.msSystemNow() - message.data.requestTime);
-
         this.loadURI(message.data);
-
         break;
       case "WebNavigation:SetOriginAttributes":
         this.setOriginAttributes(message.data.originAttributes);
         break;
       case "WebNavigation:Reload":
         this.reload(message.data.flags);
         break;
       case "WebNavigation:Stop":
--- a/toolkit/components/prompts/src/SharedPromptUtils.jsm
+++ b/toolkit/components/prompts/src/SharedPromptUtils.jsm
@@ -59,32 +59,53 @@ var EnableDelayHelper = function({enable
     this.enableDialog = makeSafe(enableDialog);
     this.disableDialog = makeSafe(disableDialog);
     this.focusTarget = focusTarget;
 
     this.disableDialog();
 
     this.focusTarget.addEventListener("blur", this);
     this.focusTarget.addEventListener("focus", this);
+    // While the user key-repeats, we want to renew the timer until keyup:
+    this.focusTarget.addEventListener("keyup", this, true);
+    this.focusTarget.addEventListener("keydown", this, true);
     this.focusTarget.document.addEventListener("unload", this);
 
     this.startOnFocusDelay();
 };
 
 this.EnableDelayHelper.prototype = {
     get delayTime() {
         return Services.prefs.getIntPref("security.dialog_enable_delay");
     },
 
     handleEvent(event) {
-        if (event.target != this.focusTarget &&
+        if (!event.type.startsWith("key") &&
+            event.target != this.focusTarget &&
             event.target != this.focusTarget.document)
             return;
 
         switch (event.type) {
+            case "keyup":
+                // As soon as any key goes up, we can stop treating keypresses
+                // as indicative of key-repeating that should prolong the timer.
+                this.focusTarget.removeEventListener("keyup", this, true);
+                this.focusTarget.removeEventListener("keydown", this, true);
+                break;
+
+            case "keydown":
+                // Renew timer for repeating keydowns:
+                if (this._focusTimer) {
+                    this._focusTimer.cancel();
+                    this._focusTimer = null;
+                    this.startOnFocusDelay();
+                    event.preventDefault();
+                }
+                break;
+
             case "blur":
                 this.onBlur();
                 break;
 
             case "focus":
                 this.onFocus();
                 break;
 
@@ -106,16 +127,18 @@ this.EnableDelayHelper.prototype = {
 
     onFocus() {
         this.startOnFocusDelay();
     },
 
     onUnload() {
         this.focusTarget.removeEventListener("blur", this);
         this.focusTarget.removeEventListener("focus", this);
+        this.focusTarget.removeEventListener("keyup", this, true);
+        this.focusTarget.removeEventListener("keydown", this, true);
         this.focusTarget.document.removeEventListener("unload", this);
 
         if (this._focusTimer) {
             this._focusTimer.cancel();
             this._focusTimer = null;
         }
 
         this.focusTarget = this.enableDialog = this.disableDialog = null;
--- a/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
+++ b/toolkit/components/remotebrowserutils/RemoteWebNavigation.jsm
@@ -111,17 +111,16 @@ RemoteWebNavigation.prototype = {
       flags: aLoadURIOptions.loadFlags,
       referrerInfo: E10SUtils.serializeReferrerInfo(aLoadURIOptions.referrerInfo),
       postData: aLoadURIOptions.postData ? Utils.serializeInputStream(aLoadURIOptions.postData) : null,
       headers: aLoadURIOptions.headers ? Utils.serializeInputStream(aLoadURIOptions.headers) : null,
       baseURI: aLoadURIOptions.baseURI ? aLoadURIOptions.baseURI.spec : null,
       triggeringPrincipal: E10SUtils.serializePrincipal(
                            aLoadURIOptions.triggeringPrincipal || Services.scriptSecurityManager.createNullPrincipal({})),
       csp: aLoadURIOptions.csp ? E10SUtils.serializeCSP(aLoadURIOptions.csp) : null,
-      requestTime: Services.telemetry.msSystemNow(),
       cancelContentJSEpoch,
     });
   },
   setOriginAttributesBeforeLoading(aOriginAttributes) {
     this._sendMessage("WebNavigation:SetOriginAttributes", {
       originAttributes: aOriginAttributes,
     });
   },
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -6277,27 +6277,16 @@
   "FX_TAB_CLICK_MS": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "default",
     "kind": "exponential",
     "high": 1000,
     "n_buckets": 20,
     "description": "Firefox: Time in ms spent on switching tabs in response to a tab click"
   },
-  "FX_TAB_REMOTE_NAVIGATION_DELAY_MS": {
-    "record_in_processes": ["content"],
-    "alert_emails": ["mconley@mozilla.com"],
-    "bug_numbers": [1352961, 1501295],
-    "expires_in_version": "69",
-    "kind": "exponential",
-    "high": 4000,
-    "n_buckets": 100,
-    "keyed": true,
-    "description": "Time taken (in ms) from the point of the parent sending the naviagion triggering message to the content and the content receiving it. This message can be either SessionStore:restoreTabContent or WebNavigation:LoadURI and these names are used as keys for this histogram. This is e10s only and recorded in the content process."
-  },
   "FX_BOOKMARKS_TOOLBAR_INIT_MS": {
     "record_in_processes": ["main", "content"],
     "expires_in_version": "never",
     "kind": "exponential",
     "low": 50,
     "high": 5000,
     "n_buckets": 10,
     "description": "Firefox: Time to initialize the bookmarks toolbar view (ms)"
--- a/toolkit/components/telemetry/Scalars.yaml
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -444,32 +444,16 @@ identity.fxaccounts:
     keyed: false
     notification_emails:
       - fxa-staff@mozilla.com
     release_channel_collection: opt-out
     record_in_processes:
       - main
 
 security:
-  pkcs11_modules_loaded:
-    bug_numbers:
-      - 1369911
-      - 1445961
-    description: >
-      A keyed boolean indicating the library names of the PKCS#11 modules that
-      have been loaded by the browser.
-    expires: "69"
-    kind: boolean
-    keyed: true
-    notification_emails:
-      - seceng-telemetry@mozilla.com
-      - dkeeler@mozilla.com
-    release_channel_collection: opt-out
-    record_in_processes:
-      - main
   webauthn_used:
     bug_numbers:
       - 1265472
     description: >
       Counts of how often Web Authentication was used in this session, keyed
       by authenticator protocol, method and result. Currently: U2FRegisterFinish,
       U2FRegisterAbort, U2FSignFinish, U2FSignAbort.
     expires: "70"
@@ -1444,16 +1428,31 @@ devtools.inspector:
     kind: uint
     notification_emails:
       - dev-developer-tools@lists.mozilla.org
       - mbalfanz@mozilla.com
     release_channel_collection: opt-out
     record_in_processes:
       - 'main'
 
+  node_selection_count:
+    bug_numbers:
+      - 1550794
+    description: >
+      Number of times a different node is marked as selected in the Inspector regardless
+      of the cause: context menu, manual selection in markup view, etc.
+    expires: "never"
+    kind: uint
+    notification_emails:
+      - dev-developer-tools@lists.mozilla.org
+      - mbalfanz@mozilla.com
+    release_channel_collection: opt-out
+    record_in_processes:
+      - 'main'
+
 devtools.shadowdom:
   shadow_root_displayed:
     bug_numbers:
       - 1470128
     description: >
       Whether the markup view displayed any #shadow-root element in the UI.
     expires: "66"
     kind: boolean
--- a/toolkit/mozapps/handling/content/dialog.js
+++ b/toolkit/mozapps/handling/content/dialog.js
@@ -99,17 +99,17 @@ var dialog = {
       }
       this._windowCtxt.QueryInterface(Ci.nsIInterfaceRequestor);
     }
 
     this.isPrivate = usePrivateBrowsing ||
                      (window.opener && PrivateBrowsingUtils.isWindowPrivate(window.opener));
 
     this._itemChoose  = document.getElementById("item-choose");
-    this._okButton    = document.documentElement.getButton("accept");
+    this._okButton    = document.documentElement.getButton("extra1");
 
     var description = {
       image: document.getElementById("description-image"),
       text:  document.getElementById("description-text"),
     };
     var options = document.getElementById("item-action-text");
     var checkbox = {
       desc: document.getElementById("remember"),
@@ -126,17 +126,19 @@ var dialog = {
     checkbox.text.textContent    = window.arguments[6];
 
     // Hide stuff that needs to be hidden
     if (!checkbox.desc.label)
       checkbox.desc.hidden = true;
 
     // UI is ready, lets populate our list
     this.populateList();
-    document.addEventListener("dialogaccept", () => { this.onAccept(); });
+    // Explicitly not an 'accept' button to avoid having `enter` accept the dialog.
+    document.addEventListener("dialogextra1", () => { this.onOK(); });
+    document.addEventListener("dialogaccept", e => { e.preventDefault(); });
 
     this._delayHelper = new EnableDelayHelper({
       disableDialog: () => {
         this._buttonDisabled = true;
         this.updateOKButton();
       },
       enableDialog: () => {
         this._buttonDisabled = false;
@@ -284,17 +286,20 @@ var dialog = {
         parent.ensureSelectedElementIsVisible();
       }
     });
   },
 
  /**
   * Function called when the OK button is pressed.
   */
-  onAccept: function onAccept() {
+  onOK: function onOK() {
+    if (this._buttonDisabled) {
+      return;
+    }
     var checkbox = document.getElementById("remember");
     if (!checkbox.hidden) {
       // We need to make sure that the default is properly set now
       if (this.selectedItem.obj) {
         // default OS handler doesn't have this property
         this._handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
         this._handlerInfo.preferredApplicationHandler = this.selectedItem.obj;
       } else {
@@ -303,16 +308,17 @@ var dialog = {
     }
     this._handlerInfo.alwaysAskBeforeHandling = !checkbox.checked;
 
     var hs = Cc["@mozilla.org/uriloader/handler-service;1"].
              getService(Ci.nsIHandlerService);
     hs.store(this._handlerInfo);
 
     this._handlerInfo.launchWithURI(this._URI, this._windowCtxt);
+    window.close();
   },
 
  /**
   * Determines if the OK button should be disabled or not
   */
   updateOKButton: function updateOKButton() {
     this._okButton.disabled = this._itemChoose.selected ||
                               this._buttonDisabled;
@@ -330,17 +336,17 @@ var dialog = {
 
   /**
    * Function called when the user double clicks on an item of the list
    */
   onDblClick: function onDblClick() {
     if (this.selectedItem == this._itemChoose)
       this.chooseApplication();
     else
-      document.documentElement.acceptDialog();
+      this.onOK();
   },
 
   // Getters / Setters
 
  /**
   * Returns/sets the selected element in the richlistbox
   */
   get selectedItem() {
--- a/toolkit/mozapps/handling/content/dialog.xul
+++ b/toolkit/mozapps/handling/content/dialog.xul
@@ -8,16 +8,17 @@
 
 <!DOCTYPE dialog SYSTEM "chrome://mozapps/locale/handling/handling.dtd">
 
 <dialog id="handling"
         onload="dialog.initialize();"
         style="min-width: &window.emWidth;; min-height: &window.emHeight;;"
         persist="width height screenX screenY"
         aria-describedby="description-text"
+        buttons="cancel,extra1"
         xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
 
   <script src="chrome://mozapps/content/handling/dialog.js" type="application/javascript"/>
 
   <stringbundleset id="strings">
     <stringbundle id="base-strings"
                   src="chrome://mozapps/locale/handling/handling.properties"/>
   </stringbundleset>
@@ -40,12 +41,12 @@
     </richlistbox>
   </vbox>
 
   <checkbox id="remember" aria-describedby="remember-text" oncommand="dialog.onCheck();"/>
   <description id="remember-text"/>
 
   <hbox class="dialog-button-box" pack="end">
     <button dlgtype="cancel" class="dialog-button"/>
-    <button dlgtype="accept" label="&accept;" class="dialog-button"/>
+    <button dlgtype="extra1" label="&accept;" class="dialog-button"/>
   </hbox>
 
 </dialog>
--- a/xpcom/base/CycleCollectedJSContext.cpp
+++ b/xpcom/base/CycleCollectedJSContext.cpp
@@ -327,41 +327,43 @@ CycleCollectedJSContext::saveJobQueue(JS
     return nullptr;
   }
 
   return saved;
 }
 
 /* static */
 void CycleCollectedJSContext::PromiseRejectionTrackerCallback(
-    JSContext* aCx, JS::HandleObject aPromise,
+    JSContext* aCx, bool aMutedErrors, JS::HandleObject aPromise,
     JS::PromiseRejectionHandlingState state, void* aData) {
   CycleCollectedJSContext* self = static_cast<CycleCollectedJSContext*>(aData);
 
   MOZ_ASSERT(aCx == self->Context());
   MOZ_ASSERT(Get() == self);
 
   // TODO: Bug 1549351 - Promise rejection event should not be sent for
   // cross-origin scripts
 
   PromiseArray& aboutToBeNotified = self->mAboutToBeNotifiedRejectedPromises;
   PromiseHashtable& unhandled = self->mPendingUnhandledRejections;
   uint64_t promiseID = JS::GetPromiseID(aPromise);
 
   if (state == JS::PromiseRejectionHandlingState::Unhandled) {
     PromiseDebugging::AddUncaughtRejection(aPromise);
-    if (mozilla::StaticPrefs::dom_promise_rejection_events_enabled()) {
+    if (mozilla::StaticPrefs::dom_promise_rejection_events_enabled() &&
+        !aMutedErrors) {
       RefPtr<Promise> promise =
           Promise::CreateFromExisting(xpc::NativeGlobal(aPromise), aPromise);
       aboutToBeNotified.AppendElement(promise);
       unhandled.Put(promiseID, promise);
     }
   } else {
     PromiseDebugging::AddConsumedRejection(aPromise);
-    if (mozilla::StaticPrefs::dom_promise_rejection_events_enabled()) {
+    if (mozilla::StaticPrefs::dom_promise_rejection_events_enabled() &&
+        !aMutedErrors) {
       for (size_t i = 0; i < aboutToBeNotified.Length(); i++) {
         if (aboutToBeNotified[i] &&
             aboutToBeNotified[i]->PromiseObj() == aPromise) {
           // To avoid large amounts of memmoves, we don't shrink the vector
           // here. Instead, we filter out nullptrs when iterating over the
           // vector later.
           aboutToBeNotified[i] = nullptr;
           DebugOnly<bool> isFound = unhandled.Remove(promiseID);
--- a/xpcom/base/CycleCollectedJSContext.h
+++ b/xpcom/base/CycleCollectedJSContext.h
@@ -113,17 +113,17 @@ class CycleCollectedJSContext
   static JSObject* GetIncumbentGlobalCallback(JSContext* aCx);
   static bool EnqueuePromiseJobCallback(JSContext* aCx,
                                         JS::HandleObject aPromise,
                                         JS::HandleObject aJob,
                                         JS::HandleObject aAllocationSite,
                                         JS::HandleObject aIncumbentGlobal,
                                         void* aData);
   static void PromiseRejectionTrackerCallback(
-      JSContext* aCx, JS::HandleObject aPromise,
+      JSContext* aCx, bool aMutedErrors, JS::HandleObject aPromise,
       JS::PromiseRejectionHandlingState state, void* aData);
 
   void AfterProcessMicrotasks();
 
  public:
   void ProcessStableStateQueue();
 
  private:
--- 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"),