Merge autoland to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 04 Dec 2016 07:02:41 -0800
changeset 325243 ffe250d116edb093404b534883d9daba3792e15c
parent 325222 92d16ccb4ccedcea1da6b650d5952a4eed6b0102 (current diff)
parent 325242 96c519a83bf03d4f54a63113fbd6f7548b9df111 (diff)
child 325250 12637ae351d64ecbf6b74cdbf26d7eb24ac0f659
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersmerge
milestone53.0a1
Merge autoland to m-c, a=merge MozReview-Commit-ID: JfsuEZ8B40u
dom/animation/test/mozilla/file_partial_keyframes.html
dom/animation/test/mozilla/test_partial_keyframes.html
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -579,19 +579,17 @@ pref("mousewheel.with_meta.action", 1); 
 #endif
 pref("mousewheel.with_control.action",3);
 pref("mousewheel.with_win.action", 1);
 
 pref("browser.xul.error_pages.enabled", true);
 pref("browser.xul.error_pages.expert_bad_cert", false);
 
 // Enable captive portal detection.
-#ifdef NIGHTLY_BUILD
 pref("network.captive-portal-service.enabled", true);
-#endif
 
 // If true, network link events will change the value of navigator.onLine
 pref("network.manage-offline-status", true);
 
 // We want to make sure mail URLs are handled externally...
 pref("network.protocol-handler.external.mailto", true); // for mail
 pref("network.protocol-handler.external.news", true);   // for news
 pref("network.protocol-handler.external.snews", true);  // for secure news
--- a/dom/animation/AnimValuesStyleRule.cpp
+++ b/dom/animation/AnimValuesStyleRule.cpp
@@ -78,16 +78,23 @@ AnimValuesStyleRule::AddValue(nsCSSPrope
 {
   MOZ_ASSERT(aProperty != eCSSProperty_UNKNOWN,
              "Unexpected css property");
   mAnimationValues.Put(aProperty, Move(aValue));
   mStyleBits |=
     nsCachedStyleData::GetBitForSID(nsCSSProps::kSIDTable[aProperty]);
 }
 
+bool
+AnimValuesStyleRule::GetValue(nsCSSPropertyID aProperty,
+                              StyleAnimationValue& aValue) const
+{
+  return mAnimationValues.Get(aProperty, &aValue);
+}
+
 #ifdef DEBUG
 void
 AnimValuesStyleRule::List(FILE* out, int32_t aIndent) const
 {
   nsAutoCString str;
   for (int32_t index = aIndent; --index >= 0; ) {
     str.AppendLiteral("  ");
   }
--- a/dom/animation/AnimValuesStyleRule.h
+++ b/dom/animation/AnimValuesStyleRule.h
@@ -40,16 +40,21 @@ public:
   void List(FILE* out = stdout, int32_t aIndent = 0) const override;
 #endif
 
   // For the following functions, it there is already a value for |aProperty| it
   // will be replaced with |aValue|.
   void AddValue(nsCSSPropertyID aProperty, const StyleAnimationValue &aValue);
   void AddValue(nsCSSPropertyID aProperty, StyleAnimationValue&& aValue);
 
+  bool HasValue(nsCSSPropertyID aProperty) const {
+    return mAnimationValues.Contains(aProperty);
+  }
+  bool GetValue(nsCSSPropertyID aProperty, StyleAnimationValue& aValue) const;
+
 private:
   ~AnimValuesStyleRule() {}
 
   nsDataHashtable<nsUint32HashKey, StyleAnimationValue> mAnimationValues;
 
   uint32_t mStyleBits;
 };
 
--- a/dom/animation/Animation.h
+++ b/dom/animation/Animation.h
@@ -269,34 +269,23 @@ public:
   {
     return GetEffect() && GetEffect()->IsCurrent();
   }
   bool IsInEffect() const
   {
     return GetEffect() && GetEffect()->IsInEffect();
   }
 
-  /**
-   * Returns true if this animation's playback state makes it a candidate for
-   * running on the compositor.
-   * We send animations to the compositor when their target effect is 'current'
-   * (a definition that is roughly equivalent to when they are in their before
-   * or active phase). However, we don't send animations to the compositor when
-   * they are paused/pausing (including being effectively paused due to
-   * having a zero playback rate), have a zero-duration active interval, or have
-   * no target effect at all.
-   */
-  bool IsPlayableOnCompositor() const
+  bool IsPlaying() const
   {
-    return HasCurrentEffect() &&
-           mPlaybackRate != 0.0 &&
+    return mPlaybackRate != 0.0 &&
            (PlayState() == AnimationPlayState::Running ||
-            mPendingState == PendingState::PlayPending) &&
-           !GetEffect()->IsActiveDurationZero();
+            mPendingState == PendingState::PlayPending);
   }
+
   bool IsRelevant() const { return mIsRelevant; }
   void UpdateRelevance();
 
   /**
    * Returns true if this Animation has a lower composite order than aOther.
    */
   bool HasLowerCompositeOrderThan(const Animation& aOther) const;
 
--- a/dom/animation/AnimationUtils.cpp
+++ b/dom/animation/AnimationUtils.cpp
@@ -59,23 +59,29 @@ AnimationUtils::IsOffscreenThrottlingEna
     Preferences::AddBoolVarCache(&sOffscreenThrottlingEnabled,
                                  "dom.animations.offscreen-throttling");
   }
 
   return sOffscreenThrottlingEnabled;
 }
 
 /* static */ bool
-AnimationUtils::IsCoreAPIEnabledForCaller()
+AnimationUtils::IsCoreAPIEnabled()
 {
   static bool sCoreAPIEnabled;
   static bool sPrefCached = false;
 
   if (!sPrefCached) {
     sPrefCached = true;
     Preferences::AddBoolVarCache(&sCoreAPIEnabled,
                                  "dom.animations-api.core.enabled");
   }
 
-  return sCoreAPIEnabled || nsContentUtils::IsCallerChrome();
+  return sCoreAPIEnabled;
+}
+
+/* static */ bool
+AnimationUtils::IsCoreAPIEnabledForCaller()
+{
+  return IsCoreAPIEnabled() || nsContentUtils::IsCallerChrome();
 }
 
 } // namespace mozilla
--- a/dom/animation/AnimationUtils.h
+++ b/dom/animation/AnimationUtils.h
@@ -58,17 +58,22 @@ public:
   /**
    * Checks if offscreen animation throttling is enabled.
    */
   static bool
   IsOffscreenThrottlingEnabled();
 
   /**
    * Returns true if the preference to enable the core Web Animations API is
+   * true.
+   */
+  static bool IsCoreAPIEnabled();
+
+  /**
+   * Returns true if the preference to enable the core Web Animations API is
    * true or the caller is chrome.
    */
-  static bool
-  IsCoreAPIEnabledForCaller();
+  static bool IsCoreAPIEnabledForCaller();
 };
 
 } // namespace mozilla
 
 #endif
--- a/dom/animation/EffectCompositor.cpp
+++ b/dom/animation/EffectCompositor.cpp
@@ -12,16 +12,17 @@
 #include "mozilla/AnimationComparator.h"
 #include "mozilla/AnimationPerformanceWarning.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/AnimationUtils.h"
 #include "mozilla/EffectSet.h"
 #include "mozilla/LayerAnimationInfo.h"
 #include "mozilla/RestyleManagerHandle.h"
 #include "mozilla/RestyleManagerHandleInlines.h"
+#include "mozilla/StyleAnimationValue.h"
 #include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetPresShellForContent
 #include "nsCSSPropertyIDSet.h"
 #include "nsCSSProps.h"
 #include "nsIPresShell.h"
 #include "nsLayoutUtils.h"
 #include "nsRuleNode.h" // For nsRuleNode::ComputePropertiesOverridingAnimation
 #include "nsRuleProcessorData.h" // For ElementRuleProcessorData etc.
 #include "nsTArray.h"
@@ -50,16 +51,68 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(
                                cb.Flags());
     }
   }
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(EffectCompositor, AddRef)
 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(EffectCompositor, Release)
 
+namespace {
+enum class MatchForCompositor {
+  // This animation matches and should run on the compositor if possible.
+  Yes,
+  // This (not currently playing) animation matches and can be run on the
+  // compositor if there are other animations for this property that return
+  // 'Yes'.
+  IfNeeded,
+  // This animation does not match or can't be run on the compositor.
+  No,
+  // This animation does not match or can't be run on the compositor and,
+  // furthermore, its presence means we should not run any animations for this
+  // property on the compositor.
+  NoAndBlockThisProperty
+};
+}
+
+static MatchForCompositor
+IsMatchForCompositor(const KeyframeEffectReadOnly& aEffect,
+                     nsCSSPropertyID aProperty,
+                     const nsIFrame* aFrame)
+{
+  const Animation* animation = aEffect.GetAnimation();
+  MOZ_ASSERT(animation);
+
+  if (!animation->IsRelevant()) {
+    return MatchForCompositor::No;
+  }
+
+  bool isPlaying = animation->IsPlaying();
+
+  // If we are finding animations for transform, check if there are other
+  // animations that should block the transform animation. e.g. geometric
+  // properties' animation. This check should be done regardless of whether
+  // the effect has the target property |aProperty| or not.
+  AnimationPerformanceWarning::Type warningType;
+  if (aProperty == eCSSProperty_transform &&
+      isPlaying &&
+      aEffect.ShouldBlockAsyncTransformAnimations(aFrame, warningType)) {
+    EffectCompositor::SetPerformanceWarning(
+      aFrame, aProperty,
+      AnimationPerformanceWarning(warningType));
+    return MatchForCompositor::NoAndBlockThisProperty;
+  }
+
+  if (!aEffect.HasEffectiveAnimationOfProperty(aProperty)) {
+    return MatchForCompositor::No;
+  }
+
+  return isPlaying ? MatchForCompositor::Yes : MatchForCompositor::IfNeeded;
+}
+
 // Helper function to factor out the common logic from
 // GetAnimationsForCompositor and HasAnimationsForCompositor.
 //
 // Takes an optional array to fill with eligible animations.
 //
 // Returns true if there are eligible animations, false otherwise.
 bool
 FindAnimationsForCompositor(const nsIFrame* aFrame,
@@ -126,55 +179,55 @@ FindAnimationsForCompositor(const nsIFra
         aFrame, aProperty,
         AnimationPerformanceWarning(
           AnimationPerformanceWarning::Type::HasRenderingObserver));
       return false;
     }
     content = content->GetParent();
   }
 
-  bool foundSome = false;
+  bool foundRunningAnimations = false;
   for (KeyframeEffectReadOnly* effect : *effects) {
-    MOZ_ASSERT(effect && effect->GetAnimation());
-    Animation* animation = effect->GetAnimation();
+    MatchForCompositor matchResult =
+      IsMatchForCompositor(*effect, aProperty, aFrame);
 
-    if (!animation->IsPlayableOnCompositor()) {
-      continue;
-    }
-
-    AnimationPerformanceWarning::Type warningType;
-    if (aProperty == eCSSProperty_transform &&
-        effect->ShouldBlockAsyncTransformAnimations(aFrame,
-                                                    warningType)) {
+    if (matchResult == MatchForCompositor::NoAndBlockThisProperty) {
       if (aMatches) {
         aMatches->Clear();
       }
-      EffectCompositor::SetPerformanceWarning(
-        aFrame, aProperty,
-        AnimationPerformanceWarning(warningType));
       return false;
     }
 
-    if (!effect->HasEffectiveAnimationOfProperty(aProperty)) {
+    if (matchResult == MatchForCompositor::No) {
       continue;
     }
 
     if (aMatches) {
-      aMatches->AppendElement(animation);
+      aMatches->AppendElement(effect->GetAnimation());
     }
-    foundSome = true;
+
+    if (matchResult == MatchForCompositor::Yes) {
+      foundRunningAnimations = true;
+    }
   }
 
-  MOZ_ASSERT(!foundSome || !aMatches || !aMatches->IsEmpty(),
+  // If all animations we added were not currently playing animations, don't
+  // send them to the compositor.
+  if (aMatches && !foundRunningAnimations) {
+    aMatches->Clear();
+  }
+
+  MOZ_ASSERT(!foundRunningAnimations || !aMatches || !aMatches->IsEmpty(),
              "If return value is true, matches array should be non-empty");
 
-  if (aMatches && foundSome) {
+  if (aMatches && foundRunningAnimations) {
     aMatches->Sort(AnimationPtrComparator<RefPtr<dom::Animation>>());
   }
-  return foundSome;
+
+  return foundRunningAnimations;
 }
 
 void
 EffectCompositor::RequestRestyle(dom::Element* aElement,
                                  CSSPseudoElementType aPseudoType,
                                  RestyleType aRestyleType,
                                  CascadeLevel aCascadeLevel)
 {
@@ -272,16 +325,18 @@ EffectCompositor::UpdateEffectProperties
   if (!effectSet) {
     return;
   }
 
   // Style context change might cause CSS cascade level,
   // e.g removing !important, so we should update the cascading result.
   effectSet->MarkCascadeNeedsUpdate();
 
+  ClearBaseStyles(*aElement, aPseudoType);
+
   for (KeyframeEffectReadOnly* effect : *effectSet) {
     effect->UpdateProperties(aStyleContext);
   }
 }
 
 void
 EffectCompositor::MaybeUpdateAnimationRule(dom::Element* aElement,
                                            CSSPseudoElementType aPseudoType,
@@ -811,16 +866,70 @@ EffectCompositor::SetPerformanceWarning(
     return;
   }
 
   for (KeyframeEffectReadOnly* effect : *effects) {
     effect->SetPerformanceWarning(aProperty, aWarning);
   }
 }
 
+/* static */ StyleAnimationValue
+EffectCompositor::GetBaseStyle(nsCSSPropertyID aProperty,
+                               nsStyleContext* aStyleContext,
+                               dom::Element& aElement,
+                               CSSPseudoElementType aPseudoType)
+{
+  MOZ_ASSERT(aStyleContext, "Need style context to resolve the base value");
+  MOZ_ASSERT(!aStyleContext->StyleSource().IsServoComputedValues(),
+             "Bug 1311257: Servo backend does not support the base value yet");
+
+  StyleAnimationValue result;
+
+  EffectSet* effectSet =
+    EffectSet::GetEffectSet(&aElement, aPseudoType);
+  if (!effectSet) {
+    return result;
+  }
+
+  // Check whether there is a cached style.
+  result = effectSet->GetBaseStyle(aProperty);
+  if (!result.IsNull()) {
+    return result;
+  }
+
+  RefPtr<nsStyleContext> styleContextWithoutAnimation =
+    aStyleContext->PresContext()->StyleSet()->AsGecko()->
+      ResolveStyleWithoutAnimation(&aElement, aStyleContext,
+                                   eRestyle_AllHintsWithAnimations);
+
+  DebugOnly<bool> success =
+    StyleAnimationValue::ExtractComputedValue(aProperty,
+                                              styleContextWithoutAnimation,
+                                              result);
+  MOZ_ASSERT(success, "Should be able to extract computed animation value");
+  MOZ_ASSERT(!result.IsNull(), "Should have a valid StyleAnimationValue");
+
+  effectSet->PutBaseStyle(aProperty, result);
+
+  return result;
+}
+
+/* static */ void
+EffectCompositor::ClearBaseStyles(dom::Element& aElement,
+                                  CSSPseudoElementType aPseudoType)
+{
+  EffectSet* effectSet =
+    EffectSet::GetEffectSet(&aElement, aPseudoType);
+  if (!effectSet) {
+    return;
+  }
+
+  effectSet->ClearBaseStyles();
+}
+
 // ---------------------------------------------------------
 //
 // Nested class: AnimationStyleRuleProcessor
 //
 // ---------------------------------------------------------
 
 NS_IMPL_ISUPPORTS(EffectCompositor::AnimationStyleRuleProcessor,
                   nsIStyleRuleProcessor)
--- a/dom/animation/EffectCompositor.h
+++ b/dom/animation/EffectCompositor.h
@@ -23,17 +23,19 @@ class nsIFrame;
 class nsIStyleRule;
 class nsPresContext;
 class nsStyleContext;
 
 namespace mozilla {
 
 class EffectSet;
 class RestyleTracker;
+class StyleAnimationValue;
 struct AnimationPerformanceWarning;
+struct AnimationProperty;
 struct NonOwningAnimationTarget;
 
 namespace dom {
 class Animation;
 class Element;
 }
 
 class EffectCompositor
@@ -210,16 +212,29 @@ public:
 
   // Associates a performance warning with effects on |aFrame| that animates
   // |aProperty|.
   static void SetPerformanceWarning(
     const nsIFrame* aFrame,
     nsCSSPropertyID aProperty,
     const AnimationPerformanceWarning& aWarning);
 
+  // Returns the base style of (pseudo-)element for |aProperty|.
+  // If there is no cached base style for the property, a new base style value
+  // is resolved with |aStyleContext|. The new resolved base style is cached
+  // until ClearBaseStyles is called.
+  static StyleAnimationValue GetBaseStyle(nsCSSPropertyID aProperty,
+                                          nsStyleContext* aStyleContext,
+                                          dom::Element& aElement,
+                                          CSSPseudoElementType aPseudoType);
+
+  // Clear cached base styles of (pseudo-)element.
+  static void ClearBaseStyles(dom::Element& aElement,
+                              CSSPseudoElementType aPseudoType);
+
 private:
   ~EffectCompositor() = default;
 
   // Rebuilds the animation rule corresponding to |aCascadeLevel| on the
   // EffectSet associated with the specified (pseudo-)element.
   static void ComposeAnimationRule(dom::Element* aElement,
                                    CSSPseudoElementType aPseudoType,
                                    CascadeLevel aCascadeLevel,
--- a/dom/animation/EffectSet.h
+++ b/dom/animation/EffectSet.h
@@ -194,16 +194,34 @@ public:
   {
     return mPropertiesWithImportantRules;
   }
   nsCSSPropertyIDSet& PropertiesForAnimationsLevel()
   {
     return mPropertiesForAnimationsLevel;
   }
 
+  StyleAnimationValue GetBaseStyle(nsCSSPropertyID aProperty) const
+  {
+    StyleAnimationValue result;
+    DebugOnly<bool> hasProperty = mBaseStyleValues.Get(aProperty, &result);
+    MOZ_ASSERT(hasProperty || result.IsNull());
+    return result;
+  }
+
+  void PutBaseStyle(nsCSSPropertyID aProperty,
+                    const StyleAnimationValue& aValue)
+  {
+    return mBaseStyleValues.Put(aProperty, aValue);
+  }
+  void ClearBaseStyles()
+  {
+    return mBaseStyleValues.Clear();
+  }
+
 private:
   static nsIAtom* GetEffectSetPropertyAtom(CSSPseudoElementType aPseudoType);
 
   OwningEffectSet mEffects;
 
   // These style rules contain the style data for currently animating
   // values.  They only match when styling with animation.  When we
   // style without animation, we need to not use them so that we can
@@ -242,16 +260,21 @@ private:
   // Specifies the compositor-animatable properties that are overridden by
   // !important rules.
   nsCSSPropertyIDSet mPropertiesWithImportantRules;
   // Specifies the properties for which the result will be added to the
   // animations level of the cascade and hence should be skipped when we are
   // composing the animation style for the transitions level of the cascede.
   nsCSSPropertyIDSet mPropertiesForAnimationsLevel;
 
+  // The non-animated values for properties animated by effects in this set that
+  // contain at least one animation value that is composited with the underlying
+  // value (i.e. it uses the additive or accumulate composite mode).
+  nsDataHashtable<nsUint32HashKey, StyleAnimationValue> mBaseStyleValues;
+
 #ifdef DEBUG
   // Track how many iterators are referencing this effect set when we are
   // destroyed, we can assert that nothing is still pointing to us.
   uint64_t mActiveIterators;
 
   bool mCalledPropertyDtor;
 #endif
 };
--- a/dom/animation/KeyframeEffectParams.h
+++ b/dom/animation/KeyframeEffectParams.h
@@ -53,16 +53,16 @@ struct KeyframeEffectParams
   static void ParseSpacing(const nsAString& aSpacing,
                            SpacingMode& aSpacingMode,
                            nsCSSPropertyID& aPacedProperty,
                            nsAString& aInvalidPacedProperty,
                            ErrorResult& aRv);
 
   dom::IterationCompositeOperation mIterationComposite =
     dom::IterationCompositeOperation::Replace;
-  // FIXME: Bug 1216844: Add CompositeOperation
+  dom::CompositeOperation mComposite = dom::CompositeOperation::Replace;
   SpacingMode mSpacingMode = SpacingMode::distribute;
   nsCSSPropertyID mPacedProperty = eCSSProperty_UNKNOWN;
 };
 
 } // namespace mozilla
 
 #endif // mozilla_KeyframeEffectParams_h
--- a/dom/animation/KeyframeEffectReadOnly.cpp
+++ b/dom/animation/KeyframeEffectReadOnly.cpp
@@ -96,17 +96,17 @@ IterationCompositeOperation
 KeyframeEffectReadOnly::IterationComposite() const
 {
   return mEffectOptions.mIterationComposite;
 }
 
 CompositeOperation
 KeyframeEffectReadOnly::Composite() const
 {
-  return CompositeOperation::Replace;
+  return mEffectOptions.mComposite;
 }
 
 void
 KeyframeEffectReadOnly::NotifyAnimationTimingUpdated()
 {
   UpdateTargetRegistration();
 
   // If the effect is not relevant it will be removed from the target
@@ -300,31 +300,114 @@ KeyframeEffectReadOnly::UpdateProperties
     CalculateCumulativeChangeHint(aStyleContext);
   }
 
   MarkCascadeNeedsUpdate();
 
   RequestRestyle(EffectCompositor::RestyleType::Layer);
 }
 
+/* static */ StyleAnimationValue
+KeyframeEffectReadOnly::CompositeValue(
+  nsCSSPropertyID aProperty,
+  const StyleAnimationValue& aValueToComposite,
+  const StyleAnimationValue& aUnderlyingValue,
+  CompositeOperation aCompositeOperation)
+{
+  switch (aCompositeOperation) {
+    case dom::CompositeOperation::Replace:
+      return aValueToComposite;
+    case dom::CompositeOperation::Add:
+      // So far nothing to do since we come to here only in case of missing
+      // keyframe, that means we have only to use the base value or the
+      // underlying value as the composited value.
+      // FIXME: Bug 1311620: Once we implement additive operation, we need to
+      // calculate it here.
+      return aUnderlyingValue;
+    case dom::CompositeOperation::Accumulate: {
+      StyleAnimationValue result(aValueToComposite);
+      return StyleAnimationValue::Accumulate(aProperty,
+                                             aUnderlyingValue,
+                                             Move(result));
+    }
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unknown compisite operation type");
+      break;
+  }
+  return StyleAnimationValue();
+}
+
+StyleAnimationValue
+KeyframeEffectReadOnly::CompositeValue(
+  nsCSSPropertyID aProperty,
+  const RefPtr<AnimValuesStyleRule>& aAnimationRule,
+  const StyleAnimationValue& aValueToComposite,
+  CompositeOperation aCompositeOperation)
+{
+  MOZ_ASSERT(mTarget, "CompositeValue should be called with target element");
+
+  StyleAnimationValue result = aValueToComposite;
+
+  if (aCompositeOperation == CompositeOperation::Replace) {
+    MOZ_ASSERT(!aValueToComposite.IsNull(),
+      "Input value should be valid in case of replace composite");
+    // Just copy the input value in case of 'Replace'.
+    return result;
+  }
+
+  // FIXME: Bug 1311257: Get the base value for the servo backend.
+  if (mDocument->IsStyledByServo()) {
+    return result;
+  }
+
+  MOZ_ASSERT(!aValueToComposite.IsNull() ||
+             aCompositeOperation == CompositeOperation::Add,
+             "InputValue should be null only if additive composite");
+
+  if (aAnimationRule->HasValue(aProperty)) {
+    // If we have already composed style for the property, we use the style
+    // as the underlying style.
+    DebugOnly<bool> success = aAnimationRule->GetValue(aProperty, result);
+    MOZ_ASSERT(success, "AnimValuesStyleRule::GetValue should not fail");
+  } else {
+    // If we are composing with composite operation that is not 'replace'
+    // and we have not composed style for the property yet, we have to get
+    // the base style for the property.
+    RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
+    result = EffectCompositor::GetBaseStyle(aProperty,
+                                            styleContext,
+                                            *mTarget->mElement,
+                                            mTarget->mPseudoType);
+    MOZ_ASSERT(!result.IsNull(), "The base style should be set");
+    SetNeedsBaseStyle(aProperty);
+  }
+
+  return CompositeValue(aProperty,
+                        aValueToComposite,
+                        result,
+                        aCompositeOperation);
+}
+
 void
 KeyframeEffectReadOnly::ComposeStyle(
   RefPtr<AnimValuesStyleRule>& aStyleRule,
   const nsCSSPropertyIDSet& aPropertiesToSkip)
 {
   ComputedTiming computedTiming = GetComputedTiming();
   mProgressOnLastCompose = computedTiming.mProgress;
   mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
 
   // If the progress is null, we don't have fill data for the current
   // time so we shouldn't animate.
   if (computedTiming.mProgress.IsNull()) {
     return;
   }
 
+  mNeedsBaseStyleSet.Empty();
+
   for (size_t propIdx = 0, propEnd = mProperties.Length();
        propIdx != propEnd; ++propIdx)
   {
     const AnimationProperty& prop = mProperties[propIdx];
 
     MOZ_ASSERT(prop.mSegments[0].mFromKey == 0.0, "incorrect first from key");
     MOZ_ASSERT(prop.mSegments[prop.mSegments.Length() - 1].mToKey == 1.0,
                "incorrect last to key");
@@ -353,18 +436,25 @@ KeyframeEffectReadOnly::ComposeStyle(
                  prop.mSegments.Length(),
                "out of array bounds");
 
     if (!aStyleRule) {
       // Allocate the style rule now that we know we have animation data.
       aStyleRule = new AnimValuesStyleRule();
     }
 
-    StyleAnimationValue fromValue = segment->mFromValue;
-    StyleAnimationValue toValue = segment->mToValue;
+    StyleAnimationValue fromValue =
+      CompositeValue(prop.mProperty, aStyleRule,
+                     segment->mFromValue,
+                     segment->mFromComposite);
+    StyleAnimationValue toValue =
+      CompositeValue(prop.mProperty, aStyleRule,
+                     segment->mToValue,
+                     segment->mToComposite);
+
     // Iteration composition for accumulate
     if (mEffectOptions.mIterationComposite ==
           IterationCompositeOperation::Accumulate &&
         computedTiming.mCurrentIteration > 0) {
       const AnimationPropertySegment& lastSegment =
         prop.mSegments.LastElement();
       // FIXME: Bug 1293492: Add a utility function to calculate both of
       // below StyleAnimationValues.
@@ -488,16 +578,20 @@ KeyframeEffectParamsFromUnion(const Opti
                                        result.mSpacingMode,
                                        result.mPacedProperty,
                                        aInvalidPacedProperty,
                                        aRv);
     // Ignore iterationComposite if the Web Animations API is not enabled,
     // then the default value 'Replace' will be used.
     if (AnimationUtils::IsCoreAPIEnabledForCaller()) {
       result.mIterationComposite = options.mIterationComposite;
+      // FIXME: Bug 1311620: We don't support additive animation yet.
+      if (options.mComposite != dom::CompositeOperation::Add) {
+        result.mComposite = options.mComposite;
+      }
     }
   }
   return result;
 }
 
 /* static */ Maybe<OwningAnimationTarget>
 KeyframeEffectReadOnly::ConvertTarget(
   const Nullable<ElementOrCSSPseudoElement>& aTarget)
@@ -634,19 +728,21 @@ KeyframeEffectReadOnly::BuildProperties(
                                              aStyleContext);
 
   if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
     KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced,
                                 mEffectOptions.mPacedProperty,
                                 computedValues, aStyleContext);
   }
 
-  result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy,
-                                                              computedValues,
-                                                              aStyleContext);
+  result =
+    KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy,
+                                                       computedValues,
+                                                       mEffectOptions.mComposite,
+                                                       aStyleContext);
 
 #ifdef DEBUG
   MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
              "Apart from the computed offset members, the keyframes array"
              " should not be modified");
 #endif
 
   mKeyframes.SwapElements(keyframesCopy);
@@ -791,34 +887,37 @@ KeyframeEffectReadOnly::GetTarget(
   }
 }
 
 static void
 CreatePropertyValue(nsCSSPropertyID aProperty,
                     float aOffset,
                     const Maybe<ComputedTimingFunction>& aTimingFunction,
                     const StyleAnimationValue& aValue,
+                    dom::CompositeOperation aComposite,
                     AnimationPropertyValueDetails& aResult)
 {
   aResult.mOffset = aOffset;
 
-  nsString stringValue;
-  DebugOnly<bool> uncomputeResult =
-    StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
-  MOZ_ASSERT(uncomputeResult, "failed to uncompute value");
-  aResult.mValue = stringValue;
+  if (!aValue.IsNull()) {
+    nsString stringValue;
+    DebugOnly<bool> uncomputeResult =
+      StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
+    MOZ_ASSERT(uncomputeResult, "failed to uncompute value");
+    aResult.mValue.Construct(stringValue);
+  }
 
   if (aTimingFunction) {
     aResult.mEasing.Construct();
     aTimingFunction->AppendToString(aResult.mEasing.Value());
   } else {
     aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
   }
 
-  aResult.mComposite = CompositeOperation::Replace;
+  aResult.mComposite = aComposite;
 }
 
 void
 KeyframeEffectReadOnly::GetProperties(
     nsTArray<AnimationPropertyDetails>& aProperties,
     ErrorResult& aRv) const
 {
   for (const AnimationProperty& property : mProperties) {
@@ -843,17 +942,17 @@ KeyframeEffectReadOnly::GetProperties(
          segmentIdx < segmentLen;
          segmentIdx++)
     {
       const AnimationPropertySegment& segment = property.mSegments[segmentIdx];
 
       binding_detail::FastAnimationPropertyValueDetails fromValue;
       CreatePropertyValue(property.mProperty, segment.mFromKey,
                           segment.mTimingFunction, segment.mFromValue,
-                          fromValue);
+                          segment.mFromComposite, fromValue);
       // We don't apply timing functions for zero-length segments, so
       // don't return one here.
       if (segment.mFromKey == segment.mToKey) {
         fromValue.mEasing.Reset();
       }
       // The following won't fail since we have already allocated the capacity
       // above.
       propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
@@ -862,17 +961,18 @@ KeyframeEffectReadOnly::GetProperties(
       // identical to the from-value from the next segment. However, we need
       // to add it if either:
       // a) this is the last segment, or
       // b) the next segment's from-value differs.
       if (segmentIdx == segmentLen - 1 ||
           property.mSegments[segmentIdx + 1].mFromValue != segment.mToValue) {
         binding_detail::FastAnimationPropertyValueDetails toValue;
         CreatePropertyValue(property.mProperty, segment.mToKey,
-                            Nothing(), segment.mToValue, toValue);
+                            Nothing(), segment.mToValue,
+                            segment.mToComposite, toValue);
         // It doesn't really make sense to have a timing function on the
         // last property value or before a sudden jump so we just drop the
         // easing property altogether.
         toValue.mEasing.Reset();
         propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
       }
     }
 
@@ -902,16 +1002,20 @@ KeyframeEffectReadOnly::GetKeyframes(JSC
     MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
                "Invalid computed offset");
     keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
     if (keyframe.mTimingFunction) {
       keyframeDict.mEasing.Truncate();
       keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
     } // else if null, leave easing as its default "linear".
 
+    if (keyframe.mComposite) {
+      keyframeDict.mComposite.Construct(keyframe.mComposite.value());
+    }
+
     JS::Rooted<JS::Value> keyframeJSValue(aCx);
     if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
       aRv.Throw(NS_ERROR_FAILURE);
       return;
     }
 
     JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
     for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
@@ -1200,17 +1304,17 @@ bool
 KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
   const nsIFrame* aFrame,
   AnimationPerformanceWarning::Type& aPerformanceWarning) const
 {
   // We currently only expect this method to be called for effects whose
   // animations are eligible for the compositor since, Animations that are
   // paused, zero-duration, finished etc. should not block other animations from
   // running on the compositor.
-  MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor());
+  MOZ_ASSERT(mAnimation && mAnimation->IsPlaying());
 
   EffectSet* effectSet =
     EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
   for (const AnimationProperty& property : mProperties) {
     // If there is a property for animations level that is overridden by
     // !important rules, it should not block other animations from running
     // on the compositor.
     // NOTE: We don't currently check for !important rules for properties that
@@ -1298,16 +1402,25 @@ CreateStyleContextForAnimationValue(nsCS
 void
 KeyframeEffectReadOnly::CalculateCumulativeChangeHint(
   nsStyleContext *aStyleContext)
 {
   mCumulativeChangeHint = nsChangeHint(0);
 
   for (const AnimationProperty& property : mProperties) {
     for (const AnimationPropertySegment& segment : property.mSegments) {
+      // In case composite operation is not 'replace', we can't throttle
+      // animations which will not cause any layout changes on invisible
+      // elements because we can't calculate the change hint for such properties
+      // until we compose it.
+      if (segment.mFromComposite != CompositeOperation::Replace ||
+          segment.mToComposite != CompositeOperation::Replace) {
+        mCumulativeChangeHint = ~nsChangeHint_Hints_CanIgnoreIfNotVisible;
+        return;
+      }
       RefPtr<nsStyleContext> fromContext =
         CreateStyleContextForAnimationValue(property.mProperty,
                                             segment.mFromValue, aStyleContext);
 
       RefPtr<nsStyleContext> toContext =
         CreateStyleContextForAnimationValue(property.mProperty,
                                             segment.mToValue, aStyleContext);
 
@@ -1414,10 +1527,35 @@ KeyframeEffectReadOnly::HasComputedTimin
   ComputedTiming computedTiming = GetComputedTiming();
   return computedTiming.mProgress != mProgressOnLastCompose ||
          (mEffectOptions.mIterationComposite ==
             IterationCompositeOperation::Accumulate &&
          computedTiming.mCurrentIteration !=
           mCurrentIterationOnLastCompose);
 }
 
+void
+KeyframeEffectReadOnly::SetNeedsBaseStyle(nsCSSPropertyID aProperty)
+{
+  for (size_t i = 0; i < LayerAnimationInfo::kRecords; i++) {
+    if (LayerAnimationInfo::sRecords[i].mProperty == aProperty) {
+      mNeedsBaseStyleSet.AddProperty(aProperty);
+      break;
+    }
+  }
+}
+
+bool
+KeyframeEffectReadOnly::NeedsBaseStyle(nsCSSPropertyID aProperty) const
+{
+  for (size_t i = 0; i < LayerAnimationInfo::kRecords; i++) {
+    if (LayerAnimationInfo::sRecords[i].mProperty == aProperty) {
+      return mNeedsBaseStyleSet.HasProperty(aProperty);
+    }
+  }
+  MOZ_ASSERT_UNREACHABLE(
+    "Expected a property that can be run on the compositor");
+
+  return false;
+}
+
 } // namespace dom
 } // namespace mozilla
--- a/dom/animation/KeyframeEffectReadOnly.h
+++ b/dom/animation/KeyframeEffectReadOnly.h
@@ -4,36 +4,35 @@
  * 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_dom_KeyframeEffectReadOnly_h
 #define mozilla_dom_KeyframeEffectReadOnly_h
 
 #include "nsChangeHint.h"
 #include "nsCSSPropertyID.h"
+#include "nsCSSPropertyIDSet.h"
 #include "nsCSSValue.h"
 #include "nsCycleCollectionParticipant.h"
 #include "nsTArray.h"
 #include "nsWrapperCache.h"
 #include "mozilla/AnimationPerformanceWarning.h"
 #include "mozilla/AnimationTarget.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/ComputedTimingFunction.h"
 #include "mozilla/EffectCompositor.h"
 #include "mozilla/KeyframeEffectParams.h"
-#include "mozilla/LayerAnimationInfo.h" // LayerAnimations::kRecords
 #include "mozilla/ServoBindingTypes.h" // RawServoDeclarationBlock and
                                        // associated RefPtrTraits
 #include "mozilla/StyleAnimationValue.h"
 #include "mozilla/dom/AnimationEffectReadOnly.h"
 #include "mozilla/dom/Element.h"
 
 struct JSContext;
 class JSObject;
-class nsCSSPropertyIDSet;
 class nsIContent;
 class nsIDocument;
 class nsIFrame;
 class nsIPresShell;
 class nsPresContext;
 
 namespace mozilla {
 
@@ -98,41 +97,49 @@ struct Keyframe
   }
 
   Keyframe& operator=(const Keyframe& aOther) = default;
   Keyframe& operator=(Keyframe&& aOther)
   {
     mOffset         = aOther.mOffset;
     mComputedOffset = aOther.mComputedOffset;
     mTimingFunction = Move(aOther.mTimingFunction);
+    mComposite      = Move(aOther.mComposite);
     mPropertyValues = Move(aOther.mPropertyValues);
     return *this;
   }
 
   Maybe<double>                 mOffset;
   static constexpr double kComputedOffsetNotSet = -1.0;
   double                        mComputedOffset = kComputedOffsetNotSet;
   Maybe<ComputedTimingFunction> mTimingFunction; // Nothing() here means
                                                  // "linear"
+  Maybe<dom::CompositeOperation> mComposite;
   nsTArray<PropertyValuePair>   mPropertyValues;
 };
 
 struct AnimationPropertySegment
 {
   float mFromKey, mToKey;
+  // NOTE: In the case that no keyframe for 0 or 1 offset is specified
+  // the unit of mFromValue or mToValue is eUnit_Null.
   StyleAnimationValue mFromValue, mToValue;
   Maybe<ComputedTimingFunction> mTimingFunction;
+  dom::CompositeOperation mFromComposite = dom::CompositeOperation::Replace;
+  dom::CompositeOperation mToComposite = dom::CompositeOperation::Replace;
 
   bool operator==(const AnimationPropertySegment& aOther) const
   {
     return mFromKey == aOther.mFromKey &&
            mToKey == aOther.mToKey &&
            mFromValue == aOther.mFromValue &&
            mToValue == aOther.mToValue &&
-           mTimingFunction == aOther.mTimingFunction;
+           mTimingFunction == aOther.mTimingFunction &&
+           mFromComposite == aOther.mFromComposite &&
+           mToComposite == aOther.mToComposite;
   }
   bool operator!=(const AnimationPropertySegment& aOther) const
   {
     return !(*this == aOther);
   }
 };
 
 struct AnimationProperty
@@ -277,16 +284,26 @@ public:
   // |aStyleContext| to resolve specified values.
   void UpdateProperties(nsStyleContext* aStyleContext);
 
   // Updates |aStyleRule| with the animation values produced by this
   // AnimationEffect for the current time except any properties contained
   // in |aPropertiesToSkip|.
   void ComposeStyle(RefPtr<AnimValuesStyleRule>& aStyleRule,
                     const nsCSSPropertyIDSet& aPropertiesToSkip);
+
+  // Composite |aValueToComposite| on |aUnderlyingValue| with
+  // |aCompositeOperation|.
+  // Returns |aValueToComposite| if |aCompositeOperation| is Replace.
+  static StyleAnimationValue CompositeValue(
+    nsCSSPropertyID aProperty,
+    const StyleAnimationValue& aValueToComposite,
+    const StyleAnimationValue& aUnderlyingValue,
+    CompositeOperation aCompositeOperation);
+
   // Returns true if at least one property is being animated on compositor.
   bool IsRunningOnCompositor() const;
   void SetIsRunningOnCompositor(nsCSSPropertyID aProperty, bool aIsRunning);
   void ResetIsRunningOnCompositor();
 
   // Returns true if this effect, applied to |aFrame|, contains properties
   // that mean we shouldn't run transform compositor animations on this element.
   //
@@ -317,16 +334,20 @@ public:
   void CalculateCumulativeChangeHint(nsStyleContext* aStyleContext);
 
   // Returns true if all of animation properties' change hints
   // can ignore painting if the animation is not visible.
   // See nsChangeHint_Hints_CanIgnoreIfNotVisible in nsChangeHint.h
   // in detail which change hint can be ignored.
   bool CanIgnoreIfNotVisible() const;
 
+  // Returns true if the effect is run on the compositor for |aProperty| and
+  // needs a base style to composite with.
+  bool NeedsBaseStyle(nsCSSPropertyID aProperty) const;
+
 protected:
   KeyframeEffectReadOnly(nsIDocument* aDocument,
                          const Maybe<OwningAnimationTarget>& aTarget,
                          AnimationEffectTimingReadOnly* aTiming,
                          const KeyframeEffectParams& aOptions);
 
   ~KeyframeEffectReadOnly() override = default;
 
@@ -381,16 +402,30 @@ protected:
   // infinite recursion.
   already_AddRefed<nsStyleContext>
   GetTargetStyleContext();
 
   // A wrapper for marking cascade update according to the current
   // target and its effectSet.
   void MarkCascadeNeedsUpdate();
 
+  // Composites |aValueToComposite| using |aCompositeOperation| onto the value
+  // for |aProperty| in |aAnimationRule|, or, if there is no suitable value in
+  // |aAnimationRule|, uses the base value for the property recorded on the
+  // target element's EffectSet.
+  StyleAnimationValue CompositeValue(
+    nsCSSPropertyID aProperty,
+    const RefPtr<AnimValuesStyleRule>& aAnimationRule,
+    const StyleAnimationValue& aValueToComposite,
+    CompositeOperation aCompositeOperation);
+
+  // Set a bit in mNeedsBaseStyleSet if |aProperty| can be run on the
+  // compositor.
+  void SetNeedsBaseStyle(nsCSSPropertyID aProperty);
+
   Maybe<OwningAnimationTarget> mTarget;
 
   KeyframeEffectParams mEffectOptions;
 
   // The specified keyframes.
   nsTArray<Keyframe>          mKeyframes;
 
   // A set of per-property value arrays, derived from |mKeyframes|.
@@ -405,16 +440,21 @@ protected:
   // this is used to detect when the current iteration is not changing
   // in the case when iterationComposite is accumulate.
   uint64_t mCurrentIterationOnLastCompose = 0;
 
   // We need to track when we go to or from being "in effect" since
   // we need to re-evaluate the cascade of animations when that changes.
   bool mInEffectOnLastAnimationTimingUpdate;
 
+  // Represents whether or not the corresponding property requires a base style
+  // to composite with. This is only set when the property is run on the
+  // compositor.
+  nsCSSPropertyIDSet mNeedsBaseStyleSet;
+
 private:
   nsChangeHint mCumulativeChangeHint;
 
   nsIFrame* GetAnimationFrame() const;
 
   bool CanThrottle() const;
   bool CanThrottleTransformChanges(nsIFrame& aFrame) const;
 
--- a/dom/animation/KeyframeUtils.cpp
+++ b/dom/animation/KeyframeUtils.cpp
@@ -267,16 +267,17 @@ struct AdditionalProperty
  * to gather data for each individual segment.
  */
 struct KeyframeValueEntry
 {
   nsCSSPropertyID mProperty;
   StyleAnimationValue mValue;
   float mOffset;
   Maybe<ComputedTimingFunction> mTimingFunction;
+  dom::CompositeOperation mComposite;
 
   struct PropertyOffsetComparator
   {
     static bool Equals(const KeyframeValueEntry& aLhs,
                        const KeyframeValueEntry& aRhs)
     {
       return aLhs.mProperty == aRhs.mProperty &&
              aLhs.mOffset == aRhs.mOffset;
@@ -459,23 +460,20 @@ KeyframeUtils::GetKeyframesFromObject(JS
   }
 
   if (aRv.Failed()) {
     MOZ_ASSERT(keyframes.IsEmpty(),
                "Should not set any keyframes when there is an error");
     return keyframes;
   }
 
-  // We currently don't support additive animation. However, Web Animations
-  // says that if you don't have a keyframe at offset 0 or 1, then you should
-  // synthesize one using an additive zero value when you go to compose style.
-  // Until we implement additive animations we just throw if we encounter any
-  // set of keyframes that would put us in that situation.
-
-  if (RequiresAdditiveAnimation(keyframes, aDocument)) {
+  // FIXME: Bug 1311257: Support missing keyframes for Servo backend.
+  if ((!AnimationUtils::IsCoreAPIEnabled() ||
+       aDocument->IsStyledByServo()) &&
+      RequiresAdditiveAnimation(keyframes, aDocument)) {
     aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
     keyframes.Clear();
   }
 
   return keyframes;
 }
 
 /* static */ void
@@ -664,16 +662,17 @@ KeyframeUtils::GetComputedKeyframeValues
   MOZ_ASSERT(result.Length() == aKeyframes.Length(), "Array length mismatch");
   return result;
 }
 
 /* static */ nsTArray<AnimationProperty>
 KeyframeUtils::GetAnimationPropertiesFromKeyframes(
   const nsTArray<Keyframe>& aKeyframes,
   const nsTArray<ComputedKeyframeValues>& aComputedValues,
+  dom::CompositeOperation aEffectComposite,
   nsStyleContext* aStyleContext)
 {
   MOZ_ASSERT(aKeyframes.Length() == aComputedValues.Length(),
              "Array length mismatch");
 
   nsTArray<KeyframeValueEntry> entries(aKeyframes.Length());
 
   const size_t len = aKeyframes.Length();
@@ -682,16 +681,18 @@ KeyframeUtils::GetAnimationPropertiesFro
     for (auto& value : aComputedValues[i]) {
       MOZ_ASSERT(frame.mComputedOffset != Keyframe::kComputedOffsetNotSet,
                  "Invalid computed offset");
       KeyframeValueEntry* entry = entries.AppendElement();
       entry->mOffset = frame.mComputedOffset;
       entry->mProperty = value.mProperty;
       entry->mValue = value.mValue;
       entry->mTimingFunction = frame.mTimingFunction;
+      entry->mComposite =
+        frame.mComposite ? frame.mComposite.value() : aEffectComposite;
     }
   }
 
   nsTArray<AnimationProperty> result;
   BuildSegmentsFromValueEntries(entries, result);
   return result;
 }
 
@@ -807,16 +808,23 @@ ConvertKeyframeSequence(JSContext* aCx,
     Keyframe* keyframe = aResult.AppendElement(fallible);
     if (!keyframe) {
       return false;
     }
     if (!keyframeDict.mOffset.IsNull()) {
       keyframe->mOffset.emplace(keyframeDict.mOffset.Value());
     }
 
+    if (keyframeDict.mComposite.WasPassed()) {
+      // FIXME: Bug 1311620: We don't support additive animation yet.
+      if (keyframeDict.mComposite.Value() != dom::CompositeOperation::Add) {
+        keyframe->mComposite.emplace(keyframeDict.mComposite.Value());
+      }
+    }
+
     ErrorResult rv;
     keyframe->mTimingFunction =
       TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, rv);
     if (rv.MaybeSetPendingException(aCx)) {
       return false;
     }
 
     // Look for additional property-values pairs on the object.
@@ -1129,16 +1137,104 @@ MarkAsComputeValuesFailureKey(PropertyVa
 static bool
 IsComputeValuesFailureKey(const PropertyValuePair& aPair)
 {
   return nsCSSProps::IsShorthand(aPair.mProperty) &&
          aPair.mValue.GetTokenStreamValue()->mPropertyID ==
            eCSSPropertyExtra_no_properties;
 }
 
+static void
+AppendInitialSegment(AnimationProperty* aAnimationProperty,
+                     const KeyframeValueEntry& aFirstEntry)
+{
+  AnimationPropertySegment* segment =
+    aAnimationProperty->mSegments.AppendElement();
+  segment->mFromKey        = 0.0f;
+  segment->mFromComposite  = dom::CompositeOperation::Add;
+  segment->mToKey          = aFirstEntry.mOffset;
+  segment->mToValue        = aFirstEntry.mValue;
+  segment->mToComposite    = aFirstEntry.mComposite;
+}
+
+static void
+AppendFinalSegment(AnimationProperty* aAnimationProperty,
+                   const KeyframeValueEntry& aLastEntry)
+{
+  AnimationPropertySegment* segment =
+    aAnimationProperty->mSegments.AppendElement();
+  segment->mFromKey        = aLastEntry.mOffset;
+  segment->mFromValue      = aLastEntry.mValue;
+  segment->mFromComposite  = aLastEntry.mComposite;
+  segment->mToKey          = 1.0f;
+  segment->mToComposite    = dom::CompositeOperation::Add;
+  segment->mTimingFunction = aLastEntry.mTimingFunction;
+}
+
+// Returns a newly created AnimationProperty if one was created to fill-in the
+// missing keyframe, nullptr otherwise (if we decided not to fill the keyframe
+// becase we don't support additive animation).
+static AnimationProperty*
+HandleMissingInitialKeyframe(nsTArray<AnimationProperty>& aResult,
+                             const KeyframeValueEntry& aEntry)
+{
+  MOZ_ASSERT(aEntry.mOffset != 0.0f,
+             "The offset of the entry should not be 0.0");
+
+  // If the preference of the core Web Animations API is not enabled, don't fill
+  // in the missing keyframe since the missing keyframe requires support for
+  // additive animation which is guarded by this pref.
+  if (!AnimationUtils::IsCoreAPIEnabled()){
+    return nullptr;
+  }
+
+  AnimationProperty* result = aResult.AppendElement();
+  result->mProperty = aEntry.mProperty;
+
+  AppendInitialSegment(result, aEntry);
+
+  return result;
+}
+
+static void
+HandleMissingFinalKeyframe(nsTArray<AnimationProperty>& aResult,
+                           const KeyframeValueEntry& aEntry,
+                           AnimationProperty* aCurrentAnimationProperty)
+{
+  MOZ_ASSERT(aEntry.mOffset != 1.0f,
+             "The offset of the entry should not be 1.0");
+
+  // If the preference of the core Web Animations API is not enabled, don't fill
+  // in the missing keyframe since the missing keyframe requires support for
+  // additive animation which is guarded by this pref.
+  if (!AnimationUtils::IsCoreAPIEnabled()){
+    // If we have already appended a new entry for the property so we have to
+    // remove it.
+    if (aCurrentAnimationProperty) {
+      aResult.RemoveElementAt(aResult.Length() - 1);
+    }
+    return;
+  }
+
+  // If |aCurrentAnimationProperty| is nullptr, that means this is the first
+  // entry for the property, we have to append a new AnimationProperty for this
+  // property.
+  if (!aCurrentAnimationProperty) {
+    aCurrentAnimationProperty = aResult.AppendElement();
+    aCurrentAnimationProperty->mProperty = aEntry.mProperty;
+
+    // If we have only one entry whose offset is neither 1 nor 0 for this
+    // property, we need to append the initial segment as well.
+    if (aEntry.mOffset != 0.0f) {
+      AppendInitialSegment(aCurrentAnimationProperty, aEntry);
+    }
+  }
+  AppendFinalSegment(aCurrentAnimationProperty, aEntry);
+}
+
 /**
  * Builds an array of AnimationProperty objects to represent the keyframe
  * animation segments in aEntries.
  */
 static void
 BuildSegmentsFromValueEntries(nsTArray<KeyframeValueEntry>& aEntries,
                               nsTArray<AnimationProperty>& aResult)
 {
@@ -1165,57 +1261,69 @@ BuildSegmentsFromValueEntries(nsTArray<K
   // offset 1, if we have multiple values for a given property at that offset,
   // since we need to retain the very first and very last value so they can
   // be used for reverse and forward filling.
   //
   // Typically, for each property in |aEntries|, we expect there to be at least
   // one KeyframeValueEntry with offset 0.0, and at least one with offset 1.0.
   // However, since it is possible that when building |aEntries|, the call to
   // StyleAnimationValue::ComputeValues might fail, this can't be guaranteed.
-  // Furthermore, since we don't yet implement additive animation and hence
-  // don't have sensible fallback behavior when these values are missing, the
-  // following loop takes care to identify properties that lack a value at
-  // offset 0.0/1.0 and drops those properties from |aResult|.
+  // Furthermore, if additive animation is disabled, the following loop takes
+  // care to identify properties that lack a value at offset 0.0/1.0 and drops
+  // those properties from |aResult|.
 
   nsCSSPropertyID lastProperty = eCSSProperty_UNKNOWN;
   AnimationProperty* animationProperty = nullptr;
 
   size_t i = 0, n = aEntries.Length();
 
   while (i < n) {
-    // Check that the last property ends with an entry at offset 1.
+    // If we've reached the end of the array of entries, synthesize a final (and
+    // initial) segment if necessary.
     if (i + 1 == n) {
-      if (aEntries[i].mOffset != 1.0f && animationProperty) {
-        aResult.RemoveElementAt(aResult.Length() - 1);
-        animationProperty = nullptr;
+      if (aEntries[i].mOffset != 1.0f) {
+        HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
+      } else if (aEntries[i].mOffset == 1.0f && !animationProperty) {
+        // If the last entry with offset 1 and no animation property, that means
+        // it is the only entry for this property so append a single segment
+        // from 0 offset to |aEntry[i].offset|.
+        Unused << HandleMissingInitialKeyframe(aResult, aEntries[i]);
       }
+      animationProperty = nullptr;
       break;
     }
 
     MOZ_ASSERT(aEntries[i].mProperty != eCSSProperty_UNKNOWN &&
                aEntries[i + 1].mProperty != eCSSProperty_UNKNOWN,
                "Each entry should specify a valid property");
 
-    // Skip properties that don't have an entry with offset 0.
+    // No keyframe for this property at offset 0.
     if (aEntries[i].mProperty != lastProperty &&
         aEntries[i].mOffset != 0.0f) {
-      // Since the entries are sorted by offset for a given property, and
-      // since we don't update |lastProperty|, we will keep hitting this
-      // condition until we change property.
-      ++i;
-      continue;
+      // If we don't support additive animation we can't fill in the missing
+      // keyframes and we should just skip this property altogether. Since the
+      // entries are sorted by offset for a given property, and since we don't
+      // update |lastProperty|, we will keep hitting this condition until we
+      // change property.
+      animationProperty = HandleMissingInitialKeyframe(aResult, aEntries[i]);
+      if (animationProperty) {
+        lastProperty = aEntries[i].mProperty;
+      } else {
+        // Skip this entry if we did not handle the missing entry.
+        ++i;
+        continue;
+      }
     }
 
-    // Drop properties that don't end with an entry with offset 1.
+    // No keyframe for this property at offset 1.
     if (aEntries[i].mProperty != aEntries[i + 1].mProperty &&
         aEntries[i].mOffset != 1.0f) {
-      if (animationProperty) {
-        aResult.RemoveElementAt(aResult.Length() - 1);
-        animationProperty = nullptr;
-      }
+      HandleMissingFinalKeyframe(aResult, aEntries[i], animationProperty);
+      // Move on to new property.
+      animationProperty = nullptr;
       ++i;
       continue;
     }
 
     // Starting from i, determine the next [i, j] interval from which to
     // generate a segment.
     size_t j;
     if (aEntries[i].mOffset == 0.0f && aEntries[i + 1].mOffset == 0.0f) {
@@ -1266,16 +1374,18 @@ BuildSegmentsFromValueEntries(nsTArray<K
     // Now generate the segment.
     AnimationPropertySegment* segment =
       animationProperty->mSegments.AppendElement();
     segment->mFromKey   = aEntries[i].mOffset;
     segment->mToKey     = aEntries[j].mOffset;
     segment->mFromValue = aEntries[i].mValue;
     segment->mToValue   = aEntries[j].mValue;
     segment->mTimingFunction = aEntries[i].mTimingFunction;
+    segment->mFromComposite = aEntries[i].mComposite;
+    segment->mToComposite = aEntries[j].mComposite;
 
     i = j;
   }
 }
 
 /**
  * Converts a JS object representing a property-indexed keyframe into
  * an array of Keyframe objects.
@@ -1302,56 +1412,70 @@ GetKeyframeListFromPropertyIndexedKeyfra
   // get its explicit dictionary members.
   dom::binding_detail::FastBasePropertyIndexedKeyframe keyframeDict;
   if (!keyframeDict.Init(aCx, aValue, "BasePropertyIndexedKeyframe argument",
                          false)) {
     aRv.Throw(NS_ERROR_FAILURE);
     return;
   }
 
+  Maybe<dom::CompositeOperation> composite;
+  if (keyframeDict.mComposite.WasPassed()) {
+    // FIXME: Bug 1311620: We don't support additive animation yet.
+    if (keyframeDict.mComposite.Value() != dom::CompositeOperation::Add) {
+      composite.emplace(keyframeDict.mComposite.Value());
+    }
+  }
+
   Maybe<ComputedTimingFunction> easing =
     TimingParams::ParseEasing(keyframeDict.mEasing, aDocument, aRv);
   if (aRv.Failed()) {
     return;
   }
 
   // Get all the property--value-list pairs off the object.
   JS::Rooted<JSObject*> object(aCx, &aValue.toObject());
   nsTArray<PropertyValuesPair> propertyValuesPairs;
   if (!GetPropertyValuesPairs(aCx, object, ListAllowance::eAllow,
                               propertyValuesPairs)) {
     aRv.Throw(NS_ERROR_FAILURE);
     return;
   }
 
+  bool isServoBackend = aDocument->IsStyledByServo();
+
   // Create a set of keyframes for each property.
   nsCSSParser parser(aDocument->CSSLoader());
   nsClassHashtable<nsFloatHashKey, Keyframe> processedKeyframes;
   for (const PropertyValuesPair& pair : propertyValuesPairs) {
     size_t count = pair.mValues.Length();
     if (count == 0) {
       // No animation values for this property.
       continue;
     }
-    if (count == 1) {
-      // We don't support additive values and so can't support an
-      // animation that goes from the underlying value to this
-      // specified value.  Throw an exception until we do support this.
+
+    // If we only have one value, we should animate from the underlying value
+    // using additive animation--however, we don't support additive animation
+    // for Servo backend (bug 1311257) or when the core animation API pref is
+    // switched off.
+    if ((!AnimationUtils::IsCoreAPIEnabled() || isServoBackend) &&
+        count == 1) {
       aRv.Throw(NS_ERROR_DOM_ANIM_MISSING_PROPS_ERR);
       return;
     }
 
     size_t n = pair.mValues.Length() - 1;
     size_t i = 0;
 
     for (const nsString& stringValue : pair.mValues) {
       double offset = i++ / double(n);
       Keyframe* keyframe = processedKeyframes.LookupOrAdd(offset);
       if (keyframe->mPropertyValues.IsEmpty()) {
         keyframe->mTimingFunction = easing;
+        keyframe->mComposite = composite;
         keyframe->mComputedOffset = offset;
       }
       keyframe->mPropertyValues.AppendElement(
         MakePropertyValuePair(pair.mProperty, stringValue, parser, aDocument));
     }
   }
 
   aResult.SetCapacity(processedKeyframes.Count());
--- a/dom/animation/KeyframeUtils.h
+++ b/dom/animation/KeyframeUtils.h
@@ -122,23 +122,27 @@ public:
    *
    * @param aKeyframes The input keyframes.
    * @param aComputedValues The computed keyframe values (as returned by
    *   GetComputedKeyframeValues) used to fill in the individual
    *   AnimationPropertySegment objects. Although these values could be
    *   calculated from |aKeyframes|, passing them in as a separate parameter
    *   allows the result of GetComputedKeyframeValues to be re-used both
    *   here and in ApplySpacing.
+   * @param aEffectComposite The composite operation specified on the effect.
+   *   For any keyframes in |aKeyframes| that do not specify a composite
+   *   operation, this value will be used.
    * @param aStyleContext The style context to calculate the style difference.
    * @return The set of animation properties. If an error occurs, the returned
    *   array will be empty.
    */
   static nsTArray<AnimationProperty> GetAnimationPropertiesFromKeyframes(
     const nsTArray<Keyframe>& aKeyframes,
     const nsTArray<ComputedKeyframeValues>& aComputedValues,
+    dom::CompositeOperation aEffectComposite,
     nsStyleContext* aStyleContext);
 
   /**
    * Check if the property or, for shorthands, one or more of
    * its subproperties, is animatable.
    *
    * @param aProperty The property to check.
    * @return true if |aProperty| is animatable.
--- a/dom/animation/test/chrome.ini
+++ b/dom/animation/test/chrome.ini
@@ -10,8 +10,9 @@ support-files =
 # over HTTP
 [chrome/test_animation_observers.html]
 [chrome/test_animation_performance_warning.html]
 [chrome/test_animation_properties.html]
 [chrome/test_generated_content_getAnimations.html]
 [chrome/test_observers_for_sync_api.html]
 [chrome/test_restyles.html]
 [chrome/test_running_on_compositor.html]
+[chrome/test_simulate_compute_values_failure.html]
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_animation_properties.html
@@ -28,30 +28,21 @@ function assert_properties_equal(actual,
   assert_equals(actual.length, expected.length);
 
   var compareProperties = (a, b) =>
     a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
 
   var sortedActual   = actual.sort(compareProperties);
   var sortedExpected = expected.sort(compareProperties);
 
-  // We want to serialize the values in the following form:
-  //
-  //  { offset: 0, easing: linear, composite: replace, value: 5px }, ...
-  //
-  // So that we can just compare strings and, in the failure case,
-  // easily see where the differences lie.
-  var serializeMember = value => {
-    return typeof value === 'undefined' ? '<not set>' : value;
-  }
   var serializeValues = values =>
     values.map(value =>
       '{ ' +
         [ 'offset', 'value', 'easing', 'composite' ].map(
-          member => `${member}: ${serializeMember(value[member])}`
+          member => `${member}: ${value[member]}`
         ).join(', ') +
       ' }')
     .join(', ');
 
   for (var i = 0; i < sortedActual.length; i++) {
     assert_equals(sortedActual[i].property,
                   sortedExpected[i].property,
                   'CSS property name should match');
@@ -697,291 +688,124 @@ var gTests = [
 
   // ---------------------------------------------------------------------
   //
   // Tests for properties that parse correctly but which we fail to
   // convert to computed values.
   //
   // ---------------------------------------------------------------------
 
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial keyframe',
-    frames:   [ { margin: '5px', simulateComputeValuesFailure: true },
-                { margin: '5px' } ],
-    expected: [  ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial keyframe where we have enough values to create'
-              + ' a final segment',
-    frames:   [ { margin: '5px', simulateComputeValuesFailure: true },
-                { margin: '5px' },
-                { margin: '5px' } ],
-    expected: [  ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial overlapping keyframes (first in series of two)',
-    frames:   [ { margin: '5px', offset: 0,
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', offset: 0 },
+  { desc:     'a missing property in initial keyframe',
+    frames:   [ { },
                 { margin: '5px' } ],
     expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
+                  values: [ value(0, undefined, 'add', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial overlapping keyframes (second in series of two)',
-    frames:   [ { margin: '5px', offset: 0 },
-                { margin: '5px', offset: 0,
-                  simulateComputeValuesFailure: true },
-                { margin: '5px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
+                  values: [ value(0, undefined, 'add', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
+                  values: [ value(0, undefined, 'add', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
+                  values: [ value(0, undefined, 'add', 'linear'),
                             value(1, '5px', 'replace') ] } ]
   },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial overlapping keyframes (second in series of three)',
-    frames:   [ { margin: '5px', offset: 0 },
-                { margin: '5px', offset: 0,
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', offset: 0 },
-                { margin: '5px' } ],
+  { desc:     'a missing property in final keyframe',
+    frames:   [ { margin: '5px' },
+                { } ],
     expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace'),
-                            value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace'),
-                            value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace'),
-                            value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace'),
-                            value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final keyframe',
-    frames:   [ { margin: '5px' },
-                { margin: '5px', simulateComputeValuesFailure: true } ],
-    expected: [  ]
+                  values: [ value(0, '5px', 'replace', 'linear'),
+                            value(1, undefined, 'add') ] } ]
   },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final keyframe where it forms the last segment in the series',
+  { desc:     'a missing property in final keyframe where it forms the last'
+              + ' segment in the series',
     frames:   [ { margin: '5px' },
-                { margin: '5px',
-                  marginLeft: '5px',
+                { marginLeft: '5px',
                   marginRight: '5px',
-                  marginBottom: '5px',
-                  // margin-top sorts last and only it will be missing since
-                  // the other longhand components are specified
-                  simulateComputeValuesFailure: true } ],
+                  marginBottom: '5px' } ],
     expected: [ { property: 'margin-bottom',
                   values: [ value(0, '5px', 'replace', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'margin-left',
                   values: [ value(0, '5px', 'replace', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'margin-right',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final keyframe where we have enough values to create'
-              + ' an initial segment',
-    frames:   [ { margin: '5px' },
-                { margin: '5px' },
-                { margin: '5px', simulateComputeValuesFailure: true } ],
-    expected: [  ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final overlapping keyframes (first in series of two)',
-    frames:   [ { margin: '5px' },
-                { margin: '5px', offset: 1,
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', offset: 1 } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
                             value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
+                { property: 'margin-top',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final overlapping keyframes (second in series of two)',
-    frames:   [ { margin: '5px' },
-                { margin: '5px', offset: 1 },
-                { margin: '5px', offset: 1,
-                  simulateComputeValuesFailure: true } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
+                            value(1, undefined, 'add') ] } ]
   },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final overlapping keyframes (second in series of three)',
-    frames:   [ { margin: '5px' },
-                { margin: '5px', offset: 1 },
-                { margin: '5px', offset: 1,
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', offset: 1 } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' intermediate keyframe',
-    frames:   [ { margin: '5px' },
-                { margin: '5px', simulateComputeValuesFailure: true },
-                { margin: '5px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] } ]
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial keyframe along with other values',
-    // simulateComputeValuesFailure only applies to shorthands so we can set
-    // it on the same keyframe and it will only apply to |margin| and not
-    // |left|.
-    frames:   [ { margin: '77%', left: '10px',
-                  simulateComputeValuesFailure: true },
+  { desc:     'a missing property in initial keyframe along with other values',
+    frames:   [ {                left: '10px' },
                 { margin: '5px', left: '20px' } ],
     expected: [ { property: 'left',
                   values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ],
+                            value(1, '20px', 'replace') ] },
+                { property: 'margin-top',
+                  values: [ value(0, undefined, 'add', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-right',
+                  values: [ value(0, undefined, 'add', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-bottom',
+                  values: [ value(0, undefined, 'add', 'linear'),
+                            value(1, '5px', 'replace') ] },
+                { property: 'margin-left',
+                  values: [ value(0, undefined, 'add', 'linear'),
+                            value(1, '5px', 'replace') ] } ]
   },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' initial keyframe along with other values where those'
-              + ' values sort after the property with missing values',
-    frames:   [ { margin: '77%', right: '10px',
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', right: '20px' } ],
-    expected: [ { property: 'right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ],
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final keyframe along with other values',
+  { desc:     'a missing property in final keyframe along with other values',
     frames:   [ { margin: '5px', left: '10px' },
-                { margin: '5px', left: '20px',
-                  simulateComputeValuesFailure: true } ],
+                {                left: '20px' } ],
     expected: [ { property: 'left',
                   values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ],
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' final keyframe along with other values where those'
-              + ' values sort after the property with missing values',
-    frames:   [ { margin: '5px', right: '10px' },
-                { margin: '5px', right: '20px',
-                  simulateComputeValuesFailure: true } ],
-    expected: [ { property: 'right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ],
-  },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' an intermediate keyframe along with other values',
-    frames:   [ { margin: '5px', left: '10px' },
-                { margin: '5px', left: '20px',
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', left: '30px' } ],
-    expected: [ { property: 'margin-top',
+                            value(1, '20px', 'replace') ] },
+                { property: 'margin-top',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-right',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-bottom',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
+                            value(1, undefined, 'add') ] },
                 { property: 'margin-left',
                   values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'left',
-                  values: [ value(0,   '10px', 'replace', 'linear'),
-                            value(0.5, '20px', 'replace', 'linear'),
-                            value(1,   '30px', 'replace') ] } ]
+                            value(1, undefined, 'add') ] } ]
+  },
+  { desc:     'missing properties in both of initial and final keyframe',
+    frames:   [ { left: '5px', offset: 0.5 } ],
+    expected: [ { property: 'left',
+                  values: [ value(0,   undefined, 'add',     'linear'),
+                            value(0.5, '5px',       'replace', 'linear'),
+                            value(1,   undefined, 'add') ] } ]
   },
-  { desc:     'a property that can\'t be resolved to computed values in'
-              + ' an intermediate keyframe by itself',
-    frames:   [ { margin: '5px', left: '10px' },
-                { margin: '5px',
-                  simulateComputeValuesFailure: true },
-                { margin: '5px', left: '30px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '5px', 'replace', 'linear'),
-                            value(1, '5px', 'replace') ] },
-                { property: 'left',
-                  values: [ value(0,   '10px', 'replace', 'linear'),
-                            value(1,   '30px', 'replace') ] } ]
+  { desc:     'missing propertes in both of initial and final keyframe along'
+              + 'with other values',
+    frames:   [ { left:  '5px',  offset: 0 },
+                { right: '5px',  offset: 0.5 },
+                { left:  '10px', offset: 1 } ],
+    expected: [ { property: 'left',
+                  values: [ value(0, '5px',  'replace', 'linear'),
+                            value(1, '10px', 'replace') ] },
+                { property: 'right',
+                  values: [ value(0,   undefined, 'add',     'linear'),
+                            value(0.5, '5px',     'replace', 'linear'),
+                            value(1,   undefined, 'add') ] } ]
   },
 ];
 
 gTests.forEach(function(subtest) {
   test(function(t) {
     var div = addDiv(t);
     var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
     assert_properties_equal(animation.effect.getProperties(),
--- a/dom/animation/test/chrome/test_restyles.html
+++ b/dom/animation/test/chrome/test_restyles.html
@@ -804,12 +804,52 @@ waitForAllPaints(function() {
          '!important rule begins running on the compositor even if the ' +
          '!important rule had been dropped before the target element was ' +
          'removed');
 
       yield ensureElementRemoval(div);
     }
   );
 
+  // Tests that additive animations don't throttle at all.
+  add_task(function* no_throttling_animations_out_of_view_element() {
+    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+      return;
+    }
+
+    var div = addDiv(null, { style: 'transform: translateY(-400px);' });
+    var animation =
+      div.animate([{ visibility: 'visible' }], 100 * MS_PER_SEC);
+
+    yield animation.ready;
+
+    var markers = yield observeStyling(5);
+
+    is(markers.length, 5,
+       'Discrete animation has has no keyframe whose offset is 0 or 1 in an ' +
+       'out-of-view element should not be throttled');
+    yield ensureElementRemoval(div);
+  });
+
+  // Counter part of the above test.
+  add_task(function* no_restyling_discrete_animations_out_of_view_element() {
+    if (!SpecialPowers.getBoolPref('dom.animations.offscreen-throttling')) {
+      return;
+    }
+
+    var div = addDiv(null, { style: 'transform: translateY(-400px);' });
+    var animation =
+      div.animate({ visibility: ['visible', 'hidden'] }, 100 * MS_PER_SEC);
+
+    yield animation.ready;
+
+    var markers = yield observeStyling(5);
+
+    is(markers.length, 0,
+       'Discrete animation running on the main-thread in an out-of-view ' +
+       'element should never cause restyles');
+    yield ensureElementRemoval(div);
+  });
+
 });
 
 </script>
 </body>
copy from dom/animation/test/chrome/test_animation_properties.html
copy to dom/animation/test/chrome/test_simulate_compute_values_failure.html
--- a/dom/animation/test/chrome/test_animation_properties.html
+++ b/dom/animation/test/chrome/test_simulate_compute_values_failure.html
@@ -1,31 +1,21 @@
 <!doctype html>
 <head>
 <meta charset=utf-8>
-<title>Bug 1254419 - Test the values returned by
-       KeyframeEffectReadOnly.getProperties()</title>
+<title>Bug 1276688 - Test for properties that parse correctly but which we fail
+       to convert to computed values</title>
 <script type="application/javascript" src="../testharness.js"></script>
 <script type="application/javascript" src="../testharnessreport.js"></script>
 <script type="application/javascript" src="../testcommon.js"></script>
 </head>
 <body>
-<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254419"
-  target="_blank">Mozilla Bug 1254419</a>
+<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1276688"
+  target="_blank">Mozilla Bug 1276688</a>
 <div id="log"></div>
-<style>
-
-:root {
-  --var-100px: 100px;
-  --var-100px-200px: 100px 200px;
-}
-div {
-  font-size: 10px; /* For calculating em-based units */
-}
-</style>
 <script>
 'use strict';
 
 function assert_properties_equal(actual, expected) {
   assert_equals(actual.length, expected.length);
 
   var compareProperties = (a, b) =>
     a.property == b.property ? 0 : (a.property < b.property ? -1 : 1);
@@ -63,643 +53,16 @@ function assert_properties_equal(actual,
 }
 
 // Shorthand for constructing a value object
 function value(offset, value, composite, easing) {
   return { offset: offset, value: value, easing: easing, composite: composite };
 }
 
 var gTests = [
-
-  // ---------------------------------------------------------------------
-  //
-  // Tests for property-indexed specifications
-  //
-  // ---------------------------------------------------------------------
-
-  { desc:     'a one-property two-value property-indexed specification',
-    frames:   { left: ['10px', '20px'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ]
-  },
-  { desc:     'a one-shorthand-property two-value property-indexed'
-              + ' specification',
-    frames:   { margin: ['10px', '10px 20px 30px 40px'] },
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '10px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] } ]
-  },
-  { desc:     'a two-property (one shorthand and one of its longhand'
-              + ' components) two-value property-indexed specification',
-    frames:   { marginTop: ['50px', '60px'],
-                margin: ['10px', '10px 20px 30px 40px'] },
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '50px', 'replace', 'linear'),
-                            value(1, '60px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] } ]
-  },
-  { desc:     'a two-property property-indexed specification with different'
-              + ' numbers of values',
-    frames:   { left: ['10px', '20px', '30px'],
-                top: ['40px', '50px'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.5, '20px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '40px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] } ]
-  },
-  { desc:     'a property-indexed specification with an invalid value',
-    frames:   { left: ['10px', '20px', '30px', '40px', '50px'],
-                top:  ['15px', '25px', 'invalid', '45px', '55px'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.25, '20px', 'replace', 'linear'),
-                            value(0.5, '30px', 'replace', 'linear'),
-                            value(0.75, '40px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '15px', 'replace', 'linear'),
-                            value(0.25, '25px', 'replace', 'linear'),
-                            value(0.75, '45px', 'replace', 'linear'),
-                            value(1, '55px', 'replace') ] } ]
-  },
-  { desc:     'a one-property two-value property-indexed specification that'
-              + ' needs to stringify its values',
-    frames:   { opacity: [0, 1] },
-    expected: [ { property: 'opacity',
-                  values: [ value(0, '0', 'replace', 'linear'),
-                            value(1, '1', 'replace') ] } ]
-  },
-  { desc:     'a property-indexed keyframe where a lesser shorthand precedes'
-              + ' a greater shorthand',
-    frames:   { borderLeft: [ '1px solid rgb(1, 2, 3)',
-                              '2px solid rgb(4, 5, 6)' ],
-                border:     [ '3px dotted rgb(7, 8, 9)',
-                              '4px dashed rgb(10, 11, 12)' ] },
-    expected: [ { property: 'border-bottom-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-left-color',
-                  values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
-                            value(1, 'rgb(4, 5, 6)', 'replace') ] },
-                { property: 'border-right-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-top-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-bottom-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-left-width',
-                  values: [ value(0, '1px', 'replace', 'linear'),
-                            value(1, '2px', 'replace') ] },
-                { property: 'border-right-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-top-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-bottom-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-left-style',
-                  values: [ value(0, 'solid', 'replace', 'linear'),
-                            value(1, 'solid', 'replace') ] },
-                { property: 'border-right-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-top-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-image-outset',
-                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
-                            value(1, '0 0 0 0', 'replace') ] },
-                { property: 'border-image-repeat',
-                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
-                            value(1, 'stretch stretch', 'replace') ] },
-                { property: 'border-image-slice',
-                  values: [ value(0, '100% 100% 100% 100%',
-                                  'replace', 'linear'),
-                            value(1, '100% 100% 100% 100%', 'replace') ] },
-                { property: 'border-image-source',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: 'border-image-width',
-                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
-                            value(1, '1 1 1 1', 'replace') ] },
-                { property: '-moz-border-bottom-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-left-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-right-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-top-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] } ]
-  },
-  { desc:     'a property-indexed keyframe where a greater shorthand precedes'
-              + ' a lesser shorthand',
-    frames:   { border:     [ '3px dotted rgb(7, 8, 9)',
-                              '4px dashed rgb(10, 11, 12)' ],
-                borderLeft: [ '1px solid rgb(1, 2, 3)',
-                              '2px solid rgb(4, 5, 6)' ] },
-    expected: [ { property: 'border-bottom-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-left-color',
-                  values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
-                            value(1, 'rgb(4, 5, 6)', 'replace') ] },
-                { property: 'border-right-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-top-color',
-                  values: [ value(0, 'rgb(7, 8, 9)', 'replace', 'linear'),
-                            value(1, 'rgb(10, 11, 12)', 'replace') ] },
-                { property: 'border-bottom-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-left-width',
-                  values: [ value(0, '1px', 'replace', 'linear'),
-                            value(1, '2px', 'replace') ] },
-                { property: 'border-right-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-top-width',
-                  values: [ value(0, '3px', 'replace', 'linear'),
-                            value(1, '4px', 'replace') ] },
-                { property: 'border-bottom-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-left-style',
-                  values: [ value(0, 'solid', 'replace', 'linear'),
-                            value(1, 'solid', 'replace') ] },
-                { property: 'border-right-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-top-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-image-outset',
-                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
-                            value(1, '0 0 0 0', 'replace') ] },
-                { property: 'border-image-repeat',
-                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
-                            value(1, 'stretch stretch', 'replace') ] },
-                { property: 'border-image-slice',
-                  values: [ value(0, '100% 100% 100% 100%',
-                                  'replace', 'linear'),
-                            value(1, '100% 100% 100% 100%', 'replace') ] },
-                { property: 'border-image-source',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: 'border-image-width',
-                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
-                            value(1, '1 1 1 1', 'replace') ] },
-                { property: '-moz-border-bottom-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-left-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-right-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-top-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] } ]
-  },
-
-  // ---------------------------------------------------------------------
-  //
-  // Tests for keyframe sequences
-  //
-  // ---------------------------------------------------------------------
-
-  { desc:     'a keyframe sequence specification with repeated values at'
-              + ' offset 0/1 with different easings',
-    frames:   [ { offset: 0.0, left: '100px', easing: 'ease' },
-                { offset: 0.0, left: '200px', easing: 'ease' },
-                { offset: 0.5, left: '300px', easing: 'linear' },
-                { offset: 1.0, left: '400px', easing: 'ease-out' },
-                { offset: 1.0, left: '500px', easing: 'step-end' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '100px', 'replace'),
-                            value(0, '200px', 'replace', 'ease'),
-                            value(0.5, '300px', 'replace', 'linear'),
-                            value(1, '400px', 'replace'),
-                            value(1, '500px', 'replace') ] } ]
-  },
-  { desc:     'a one-property two-keyframe sequence',
-    frames:   [ { offset: 0, left: '10px' },
-                { offset: 1, left: '20px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] } ]
-  },
-  { desc:     'a two-property two-keyframe sequence',
-    frames:   [ { offset: 0, left: '10px', top: '30px' },
-                { offset: 1, left: '20px', top: '40px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '30px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] } ]
-  },
-  { desc:     'a one shorthand property two-keyframe sequence',
-    frames:   [ { offset: 0, margin: '10px' },
-                { offset: 1, margin: '20px 30px 40px 50px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '20px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] } ]
-  },
-  { desc:     'a two-property (a shorthand and one of its component longhands)'
-              + ' two-keyframe sequence',
-    frames:   [ { offset: 0, margin: '10px', marginTop: '20px' },
-                { offset: 1, marginTop: '70px',
-                             margin: '30px 40px 50px 60px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '20px', 'replace', 'linear'),
-                            value(1, '70px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '60px', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence with duplicate values for a given interior'
-              + ' offset',
-    frames:   [ { offset: 0.0, left: '10px' },
-                { offset: 0.5, left: '20px' },
-                { offset: 0.5, left: '30px' },
-                { offset: 0.5, left: '40px' },
-                { offset: 1.0, left: '50px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.5, '20px', 'replace'),
-                            value(0.5, '40px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence with duplicate values for offsets 0 and 1',
-    frames:   [ { offset: 0, left: '10px' },
-                { offset: 0, left: '20px' },
-                { offset: 0, left: '30px' },
-                { offset: 1, left: '40px' },
-                { offset: 1, left: '50px' },
-                { offset: 1, left: '60px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace'),
-                            value(0, '30px', 'replace', 'linear'),
-                            value(1, '40px', 'replace'),
-                            value(1, '60px', 'replace') ] } ]
-  },
-  { desc:     'a two-property four-keyframe sequence',
-    frames:   [ { offset: 0, left: '10px' },
-                { offset: 0, top: '20px' },
-                { offset: 1, top: '30px' },
-                { offset: 1, left: '40px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '40px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '20px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] } ]
-  },
-  { desc:     'a one-property keyframe sequence with some omitted offsets',
-    frames:   [ { offset: 0.00, left: '10px' },
-                { offset: 0.25, left: '20px' },
-                { left: '30px' },
-                { left: '40px' },
-                { offset: 1.00, left: '50px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.25, '20px', 'replace', 'linear'),
-                            value(0.5, '30px', 'replace', 'linear'),
-                            value(0.75, '40px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] } ]
-  },
-  { desc:     'a two-property keyframe sequence with some omitted offsets',
-    frames:   [ { offset: 0.00, left: '10px', top: '20px' },
-                { offset: 0.25, left: '30px' },
-                { left: '40px' },
-                { left: '50px', top: '60px' },
-                { offset: 1.00, left: '70px', top: '80px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.25, '30px', 'replace', 'linear'),
-                            value(0.5, '40px', 'replace', 'linear'),
-                            value(0.75, '50px', 'replace', 'linear'),
-                            value(1, '70px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '20px', 'replace', 'linear'),
-                            value(0.75, '60px', 'replace', 'linear'),
-                            value(1, '80px', 'replace') ] } ]
-  },
-  { desc:     'a one-property keyframe sequence with all omitted offsets',
-    frames:   [ { left: '10px' },
-                { left: '20px' },
-                { left: '30px' },
-                { left: '40px' },
-                { left: '50px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(0.25, '20px', 'replace', 'linear'),
-                            value(0.5, '30px', 'replace', 'linear'),
-                            value(0.75, '40px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence with different easing values, but the'
-              + ' same easing value for a given offset',
-    frames:   [ { offset: 0.0, easing: 'ease',     left: '10px'},
-                { offset: 0.0, easing: 'ease',     top: '20px'},
-                { offset: 0.5, easing: 'linear',   left: '30px' },
-                { offset: 0.5, easing: 'linear',   top: '40px' },
-                { offset: 1.0, easing: 'step-end', left: '50px' },
-                { offset: 1.0, easing: 'step-end', top: '60px' } ],
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'ease'),
-                            value(0.5, '30px', 'replace', 'linear'),
-                            value(1, '50px', 'replace') ] },
-                { property: 'top',
-                  values: [ value(0, '20px', 'replace', 'ease'),
-                            value(0.5, '40px', 'replace', 'linear'),
-                            value(1, '60px', 'replace') ] } ]
-  },
-  { desc:     'a one-property two-keyframe sequence that needs to'
-              + ' stringify its values',
-    frames:   [ { offset: 0, opacity: 0 },
-                { offset: 1, opacity: 1 } ],
-    expected: [ { property: 'opacity',
-                  values: [ value(0, '0', 'replace', 'linear'),
-                            value(1, '1', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence where shorthand precedes longhand',
-    frames:   [ { offset: 0, margin: '10px', marginRight: '20px' },
-                { offset: 1, margin: '30px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '20px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence where longhand precedes shorthand',
-    frames:   [ { offset: 0, marginRight: '20px', margin: '10px' },
-                { offset: 1, margin: '30px' } ],
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '20px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '30px', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence where lesser shorthand precedes greater'
-              + ' shorthand',
-    frames:   [ { offset: 0, borderLeft: '1px solid rgb(1, 2, 3)',
-                             border: '2px dotted rgb(4, 5, 6)' },
-                { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
-    expected: [ { property: 'border-bottom-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-left-color',
-                  values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-right-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-top-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-bottom-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-left-width',
-                  values: [ value(0, '1px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-right-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-top-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-bottom-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-left-style',
-                  values: [ value(0, 'solid', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-right-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-top-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-image-outset',
-                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
-                            value(1, '0 0 0 0', 'replace') ] },
-                { property: 'border-image-repeat',
-                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
-                            value(1, 'stretch stretch', 'replace') ] },
-                { property: 'border-image-slice',
-                  values: [ value(0, '100% 100% 100% 100%',
-                                  'replace', 'linear'),
-                            value(1, '100% 100% 100% 100%', 'replace') ] },
-                { property: 'border-image-source',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: 'border-image-width',
-                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
-                            value(1, '1 1 1 1', 'replace') ] },
-                { property: '-moz-border-bottom-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-left-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-right-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-top-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] } ]
-  },
-  { desc:     'a keyframe sequence where greater shorthand precedes'
-              + ' lesser shorthand',
-    frames:   [ { offset: 0, border: '2px dotted rgb(4, 5, 6)',
-                             borderLeft: '1px solid rgb(1, 2, 3)' },
-                { offset: 1, border: '3px dashed rgb(7, 8, 9)' } ],
-    expected: [ { property: 'border-bottom-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-left-color',
-                  values: [ value(0, 'rgb(1, 2, 3)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-right-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-top-color',
-                  values: [ value(0, 'rgb(4, 5, 6)', 'replace', 'linear'),
-                            value(1, 'rgb(7, 8, 9)', 'replace') ] },
-                { property: 'border-bottom-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-left-width',
-                  values: [ value(0, '1px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-right-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-top-width',
-                  values: [ value(0, '2px', 'replace', 'linear'),
-                            value(1, '3px', 'replace') ] },
-                { property: 'border-bottom-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-left-style',
-                  values: [ value(0, 'solid', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-right-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-top-style',
-                  values: [ value(0, 'dotted', 'replace', 'linear'),
-                            value(1, 'dashed', 'replace') ] },
-                { property: 'border-image-outset',
-                  values: [ value(0, '0 0 0 0', 'replace', 'linear'),
-                            value(1, '0 0 0 0', 'replace') ] },
-                { property: 'border-image-repeat',
-                  values: [ value(0, 'stretch stretch', 'replace', 'linear'),
-                            value(1, 'stretch stretch', 'replace') ] },
-                { property: 'border-image-slice',
-                  values: [ value(0, '100% 100% 100% 100%',
-                                  'replace', 'linear'),
-                            value(1, '100% 100% 100% 100%', 'replace') ] },
-                { property: 'border-image-source',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: 'border-image-width',
-                  values: [ value(0, '1 1 1 1', 'replace', 'linear'),
-                            value(1, '1 1 1 1', 'replace') ] },
-                { property: '-moz-border-bottom-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-left-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-right-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] },
-                { property: '-moz-border-top-colors',
-                  values: [ value(0, 'none', 'replace', 'linear'),
-                            value(1, 'none', 'replace') ] } ]
-  },
-
-  // ---------------------------------------------------------------------
-  //
-  // Tests for unit conversion
-  //
-  // ---------------------------------------------------------------------
-
-  { desc:     'em units are resolved to px values',
-    frames:   { left: ['10em', '20em'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '100px', 'replace', 'linear'),
-                            value(1, '200px', 'replace') ] } ]
-  },
-  { desc:     'calc() expressions are resolved to the equivalent units',
-    frames:   { left: ['calc(10em + 10px)', 'calc(10em + 10%)'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, 'calc(110px)', 'replace', 'linear'),
-                            value(1, 'calc(100px + 10%)', 'replace') ] } ]
-  },
-
-  // ---------------------------------------------------------------------
-  //
-  // Tests for CSS variable handling conversion
-  //
-  // ---------------------------------------------------------------------
-
-  { desc:     'CSS variables are resolved to their corresponding values',
-    frames:   { left: ['10px', 'var(--var-100px)'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '100px', 'replace') ] } ]
-  },
-  { desc:     'CSS variables in calc() expressions are resolved',
-    frames:   { left: ['10px', 'calc(var(--var-100px) / 2 - 10%)'] },
-    expected: [ { property: 'left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, 'calc(50px + -10%)', 'replace') ] } ]
-  },
-  { desc:     'CSS variables in shorthands are resolved to their corresponding'
-              + ' values',
-    frames:   { margin: ['10px', 'var(--var-100px-200px)'] },
-    expected: [ { property: 'margin-top',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '100px', 'replace') ] },
-                { property: 'margin-right',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '200px', 'replace') ] },
-                { property: 'margin-bottom',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '100px', 'replace') ] },
-                { property: 'margin-left',
-                  values: [ value(0, '10px', 'replace', 'linear'),
-                            value(1, '200px', 'replace') ] } ]
-  },
-
   // ---------------------------------------------------------------------
   //
   // Tests for properties that parse correctly but which we fail to
   // convert to computed values.
   //
   // ---------------------------------------------------------------------
 
   { desc:     'a property that can\'t be resolved to computed values in'
@@ -975,19 +338,27 @@ var gTests = [
                   values: [ value(0, '5px', 'replace', 'linear'),
                             value(1, '5px', 'replace') ] },
                 { property: 'left',
                   values: [ value(0,   '10px', 'replace', 'linear'),
                             value(1,   '30px', 'replace') ] } ]
   },
 ];
 
-gTests.forEach(function(subtest) {
-  test(function(t) {
-    var div = addDiv(t);
-    var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
-    assert_properties_equal(animation.effect.getProperties(),
-                            subtest.expected);
-  }, subtest.desc);
-});
+setup({explicit_done: true});
 
+SpecialPowers.pushPrefEnv(
+  { set: [["dom.animations-api.core.enabled", false]] },
+  function() {
+    gTests.forEach(function(subtest) {
+      test(function(t) {
+        var div = addDiv(t);
+        var animation = div.animate(subtest.frames, 100 * MS_PER_SEC);
+        assert_properties_equal(animation.effect.getProperties(),
+                                subtest.expected);
+      }, subtest.desc);
+    });
+
+    done();
+  }
+);
 </script>
 </body>
--- a/dom/animation/test/mochitest.ini
+++ b/dom/animation/test/mochitest.ini
@@ -39,26 +39,28 @@ support-files =
   document-timeline/file_document-timeline.html
   mozilla/file_cubic_bezier_limits.html
   mozilla/file_deferred_start.html
   mozilla/file_disabled_properties.html
   mozilla/file_disable_animations_api_core.html
   mozilla/file_discrete-animations.html
   mozilla/file_document-timeline-origin-time-range.html
   mozilla/file_hide_and_show.html
-  mozilla/file_partial_keyframes.html
   mozilla/file_spacing_property_order.html
   mozilla/file_transform_limits.html
   mozilla/file_transition_finish_on_compositor.html
   mozilla/file_underlying-discrete-value.html
   mozilla/file_set-easing.html
   style/file_animation-seeking-with-current-time.html
   style/file_animation-seeking-with-start-time.html
   style/file_animation-setting-effect.html
   style/file_animation-setting-spacing.html
+  style/file_composite.html
+  style/file_missing-keyframe.html
+  style/file_missing-keyframe-on-compositor.html
   testcommon.js
 
 [css-animations/test_animations-dynamic-changes.html]
 [css-animations/test_animation-cancel.html]
 [css-animations/test_animation-computed-timing.html]
 [css-animations/test_animation-currenttime.html]
 [css-animations/test_animation-finish.html]
 [css-animations/test_animation-finished.html]
@@ -93,19 +95,21 @@ support-files =
 [document-timeline/test_request_animation_frame.html]
 [mozilla/test_cubic_bezier_limits.html]
 [mozilla/test_deferred_start.html]
 [mozilla/test_disable_animations_api_core.html]
 [mozilla/test_disabled_properties.html]
 [mozilla/test_discrete-animations.html]
 [mozilla/test_document-timeline-origin-time-range.html]
 [mozilla/test_hide_and_show.html]
-[mozilla/test_partial_keyframes.html]
 [mozilla/test_set-easing.html]
 [mozilla/test_spacing_property_order.html]
 [mozilla/test_transform_limits.html]
 [mozilla/test_transition_finish_on_compositor.html]
 skip-if = toolkit == 'android'
 [mozilla/test_underlying-discrete-value.html]
 [style/test_animation-seeking-with-current-time.html]
 [style/test_animation-seeking-with-start-time.html]
 [style/test_animation-setting-effect.html]
 [style/test_animation-setting-spacing.html]
+[style/test_composite.html]
+[style/test_missing-keyframe.html]
+[style/test_missing-keyframe-on-compositor.html]
--- a/dom/animation/test/mozilla/file_disable_animations_api_core.html
+++ b/dom/animation/test/mozilla/file_disable_animations_api_core.html
@@ -20,11 +20,43 @@ test(function(t) {
   // check that style value is not affected by iterationComposite.
   anim.currentTime = 200 * MS_PER_SEC;
   assert_equals(getComputedStyle(div).marginLeft, '0px',
     'Animated style should not be accumulated when the Web Animations API is ' +
     'not enabled even if accumulate is specified in the constructor');
 }, 'iterationComposite should not affect at all if the Web Animations API ' +
    'is not enabled');
 
+// Tests for cases we don't handle and should throw an exception for in case
+// the Web Animation API is disabled.
+var gTests = [
+  { desc: "single Keyframe with no offset",
+    keyframes: [{ left: "100px" }] },
+  { desc: "multiple Keyframes with missing 0% Keyframe",
+    keyframes: [{ left: "100px", offset: 0.25 },
+                { left: "200px", offset: 0.50 },
+                { left: "300px", offset: 1.00 }] },
+  { desc: "multiple Keyframes with missing 100% Keyframe",
+    keyframes: [{ left: "100px", offset: 0.00 },
+                { left: "200px", offset: 0.50 },
+                { left: "300px", offset: 0.75 }] },
+  { desc: "multiple Keyframes with missing properties on first Keyframe",
+    keyframes: [{ left: "100px", offset: 0.0 },
+                { left: "200px", top: "200px", offset: 0.5 },
+                { left: "300px", top: "300px", offset: 1.0 }] },
+  { desc: "multiple Keyframes with missing properties on last Keyframe",
+    keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
+                { left: "200px", top: "200px", offset: 0.5 },
+                { left: "300px", offset: 1.0 }] },
+];
+
+gTests.forEach(function(subtest) {
+  test(function(t) {
+    var div = addDiv(t);
+    assert_throws("NotSupportedError", function() {
+      div.animate(subtest.keyframes, 100 * MS_PER_SEC);
+    });
+  }, "Element.animate() throws with " + subtest.desc);
+});
+
 done();
 </script>
 </body>
deleted file mode 100644
--- a/dom/animation/test/mozilla/file_partial_keyframes.html
+++ /dev/null
@@ -1,41 +0,0 @@
-<!doctype html>
-<meta charset=utf-8>
-<script src="../testcommon.js"></script>
-<body>
-<script>
-'use strict';
-
-// Tests for cases we currently don't handle and should throw an exception for.
-var gTests = [
-  { desc: "single Keyframe with no offset",
-    keyframes: [{ left: "100px" }] },
-  { desc: "multiple Keyframes with missing 0% Keyframe",
-    keyframes: [{ left: "100px", offset: 0.25 },
-                { left: "200px", offset: 0.50 },
-                { left: "300px", offset: 1.00 }] },
-  { desc: "multiple Keyframes with missing 100% Keyframe",
-    keyframes: [{ left: "100px", offset: 0.00 },
-                { left: "200px", offset: 0.50 },
-                { left: "300px", offset: 0.75 }] },
-  { desc: "multiple Keyframes with missing properties on first Keyframe",
-    keyframes: [{ left: "100px", offset: 0.0 },
-                { left: "200px", top: "200px", offset: 0.5 },
-                { left: "300px", top: "300px", offset: 1.0 }] },
-  { desc: "multiple Keyframes with missing properties on last Keyframe",
-    keyframes: [{ left: "100px", top: "200px", offset: 0.0 },
-                { left: "200px", top: "200px", offset: 0.5 },
-                { left: "300px", offset: 1.0 }] },
-];
-
-gTests.forEach(function(subtest) {
-  test(function(t) {
-    var div = addDiv(t);
-    assert_throws("NotSupportedError", function() {
-      new KeyframeEffectReadOnly(div, subtest.keyframes);
-    });
-  }, "KeyframeEffectReadOnly constructor throws with " + subtest.desc);
-});
-
-done();
-</script>
-</body>
deleted file mode 100644
--- a/dom/animation/test/mozilla/test_partial_keyframes.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<!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.core.enabled", true]]},
-  function() {
-    window.open("file_partial_keyframes.html");
-  });
-</script>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/file_composite.html
@@ -0,0 +1,109 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+  /* Element needs geometry to be eligible for layerization */
+  width: 100px;
+  height: 100px;
+  background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+    !SpecialPowers.getBoolPref(
+      'layers.offmainthreadcomposition.async-animations')) {
+  // If OMTA is disabled, nothing to run.
+  done();
+}
+
+function waitForPaintsFlushed() {
+  return new Promise(function(resolve, reject) {
+    waitForAllPaintsFlushed(resolve);
+  });
+}
+
+promise_test(t => {
+  // Without this, the first test case fails on Android.
+  return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: ['translateX(0px)', 'translateX(200px)'],
+                composite: 'accumulate' },
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Transform value at 50%');
+  });
+}, 'Accumulate onto the base value');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  div.animate({ transform: ['translateX(100px)', 'translateX(200px)'],
+                composite: 'replace' },
+              100 * MS_PER_SEC);
+  div.animate({ transform: ['translateX(0px)', 'translateX(100px)'],
+                composite: 'accumulate' },
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Transform value at 50%');
+  });
+}, 'Accumulate onto an underlying animation value');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate([{ transform: 'translateX(100px)', composite: 'accumulate' },
+               { transform: 'translateX(300px)', composite: 'replace' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Transform value at 50s');
+  });
+}, 'Composite when mixing accumulate and replace');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate([{ transform: 'translateX(100px)', composite: 'replace' },
+               { transform: 'translateX(300px)' }],
+              { duration: 100 * MS_PER_SEC, composite: 'accumulate' });
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Transform value at 50%');
+  });
+}, 'Composite specified on a keyframe overrides the composite mode of the ' +
+   'effect');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/file_missing-keyframe-on-compositor.html
@@ -0,0 +1,476 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<script src="/tests/SimpleTest/paint_listener.js"></script>
+<style>
+div {
+  /* Element needs geometry to be eligible for layerization */
+  width: 100px;
+  height: 100px;
+  background-color: white;
+}
+</style>
+<body>
+<script>
+'use strict';
+
+if (!SpecialPowers.DOMWindowUtils.layerManagerRemote ||
+    !SpecialPowers.getBoolPref(
+      'layers.offmainthreadcomposition.async-animations')) {
+  // If OMTA is disabled, nothing to run.
+  done();
+}
+
+function waitForPaintsFlushed() {
+  return new Promise(function(resolve, reject) {
+    waitForAllPaintsFlushed(resolve);
+  });
+}
+
+// Note that promise tests run in sequence so this ensures the document is
+// loaded before any of the other tests run.
+promise_test(t => {
+  // Without this, the first test case fails on Android.
+  return waitForDocumentLoad();
+}, 'Ensure document has been loaded');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0.1' });
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.1',
+                  'The initial opacity value should be the base value');
+  });
+}, 'Initial opacity value for animation with no no keyframe at offset 0');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0.1' });
+  div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.5',
+                  'The initial opacity value should be the value of ' +
+                  'lower-priority animation value');
+  });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'opacity: 0.1; transition: opacity 100s linear' });
+  getComputedStyle(div).opacity;
+
+  div.style.opacity = '0.5';
+  div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.1',
+                  'The initial opacity value should be the initial value of ' +
+                  'the transition');
+  });
+}, 'Initial opacity value for animation with no keyframe at offset 0 when ' +
+   'there is a transition on the same property');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0' });
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.5',
+                  'Opacity value at 50% should be composed onto the base ' +
+                  'value');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% ');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'opacity: 0' });
+  div.animate({ opacity: [ 0.5, 0.5 ] }, 100 * MS_PER_SEC);
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.75', // (0.5 + 1) * 0.5
+                  'Opacity value at 50% should be composed onto the value ' +
+                  'of middle of lower-priority animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'opacity: 0; transition: opacity 100s linear' });
+  getComputedStyle(div).opacity;
+
+  div.style.opacity = '0.5';
+  div.animate([{ offset: 0, opacity: 1 }], 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    assert_equals(opacity, '0.625', // ((0 + 0.5) * 0.5 + 1) * 0.5
+                  'Opacity value at 50% should be composed onto the value ' +
+                  'of middle of transition');
+  });
+}, 'Opacity value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a transition on the same property');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.pause();
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority paused animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a paused underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 0.5, 1 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.playbackRate = 0;
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority zero playback rate animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation = div.animate({ opacity: [ 1, 0.5 ] }, 100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ opacity: 1 }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.effect.timing.duration = 0;
+    lowerAnimation.effect.timing.fill = 'forwards';
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var opacity =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'opacity');
+    // The underlying value is the value that is filling forwards state of the
+    // lowerAnimation, that is 0.5.
+    // (0.5 + 1.0) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 0.75.
+    assert_equals(opacity, '0.75',
+                  'Composed opacity value should be composed onto the value ' +
+                  'of lower-priority zero active duration animation');
+  });
+}, 'Opacity value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero active duration underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: 'translateX(200px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+      'The initial transform value should be the base value');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: [ 'translateX(200px)', 'translateX(300px)' ] },
+              100 * MS_PER_SEC);
+  div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'The initial transform value should be lower-priority animation value');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'transform: translateX(100px);' +
+                       'transition: transform 100s linear' });
+  getComputedStyle(div).transform;
+
+  div.style.transform = 'translateX(200px)';
+  div.animate({ transform: 'translateX(400px)' }, 100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 100, 0)',
+      'The initial transform value should be the initial value of the ' +
+      'transition');
+  });
+}, 'Initial transform value for animation with no keyframe at offset 0 when ' +
+   'there is a transition');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate([{ offset: 0, transform: 'translateX(200pX)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 150, 0)',
+      'Transform value at 50% should be the base value');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50%');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t, { style: 'transform: translateX(100px)' });
+  div.animate({ transform: [ 'translateX(200px)', 'translateX(200px)' ] },
+              100 * MS_PER_SEC);
+  div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'The final transform value should be the base value');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a lower-priority animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div =
+    addDiv(t, { style: 'transform: translateX(100px);' +
+                       'transition: transform 100s linear' });
+  getComputedStyle(div).transform;
+
+  div.style.transform = 'translateX(200px)';
+  div.animate([{ offset: 0, transform: 'translateX(300px)' }],
+              100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+                                                   // (150px + 300px) * 0.5
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+      'The final transform value should be the final value of the transition');
+  });
+}, 'Transform value for animation with no keyframe at offset 1 at 50% when ' +
+   'there is a transition');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.pause();
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 100px.
+    // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority paused animation');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a paused underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                100 * MS_PER_SEC);
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    lowerAnimation.playbackRate = 0;
+    return waitForPaintsFlushed();
+  }).then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // The underlying value is the value that is staying at 0ms of the
+    // lowerAnimation, that is 100px.
+    // (100px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 200px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 200, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority zero playback rate animation');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a zero playback rate underlying animation');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    // We need to wait for a paint so that we can send the state of the lower
+    // animation that is actually finished at this point.
+    return waitForPaintsFlushed();
+  }).then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (200px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 250px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  endDelay: -5 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    // We need to wait for a paint just like the above test.
+    return waitForPaintsFlushed();
+  }).then(() => {
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (150px + 300px) * (50 * MS_PER_SEC / 100 * MS_PER_SEC) = 225px.
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 225, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards and negative endDelay');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards and negative ' +
+   'endDelay');
+
+promise_test(t => {
+  useTestRefreshMode(t);
+
+  var div = addDiv(t);
+  var lowerAnimation =
+    div.animate({ transform: [ 'translateX(100px)', 'translateX(200px)' ] },
+                { duration: 10 * MS_PER_SEC,
+                  endDelay: 100 * MS_PER_SEC,
+                  fill: 'forwards' });
+  var higherAnimation = div.animate({ transform: 'translateX(300px)' },
+                                    100 * MS_PER_SEC);
+
+  return waitForPaintsFlushed().then(() => {
+    SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(50 * MS_PER_SEC);
+
+    var transform =
+      SpecialPowers.DOMWindowUtils.getOMTAStyle(div, 'transform');
+    // (200px + 300px) * 0.5
+    assert_matrix_equals(transform, 'matrix(1, 0, 0, 1, 250, 0)',
+      'Composed transform value should be composed onto the value of ' +
+      'lower-priority animation with fill:forwards during positive endDelay');
+  });
+}, 'Transform value for animation with no keyframe at offset 0 at 50% when ' +
+   'composed onto a underlying animation with fill:forwards during positive ' +
+   'endDelay');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/file_missing-keyframe.html
@@ -0,0 +1,94 @@
+<!doctype html>
+<meta charset=utf-8>
+<script src="../testcommon.js"></script>
+<body>
+<script>
+'use strict';
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px' });
+  div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+  assert_equals(getComputedStyle(div).marginLeft, '100px',
+                'The initial margin-left value should be the base value');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0');
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px' });
+  div.animate([{ offset: 0, marginLeft: '200px' },
+               { offset: 1, marginLeft: '300px' }],
+              100 * MS_PER_SEC);
+  div.animate([{ marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+  assert_equals(getComputedStyle(div).marginLeft, '200px',
+                'The initial margin-left value should be the initial value ' +
+                'of lower-priority animation');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+   'is that of lower-priority animations');
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px;' +
+                               'transition: margin-left 100s -50s linear'});
+  flushComputedStyle(div);
+
+  div.style.marginLeft = '200px';
+  flushComputedStyle(div);
+
+  div.animate([{ marginLeft: '300px' }], 100 * MS_PER_SEC);
+
+  assert_equals(getComputedStyle(div).marginLeft, '150px',
+                'The initial margin-left value should be the initial value ' +
+                'of the transition');
+}, 'Initial margin-left value for an animation with no keyframe at offset 0 ' +
+   'is that of transition');
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px' });
+  var animation = div.animate([{ offset: 0, marginLeft: '200px' }],
+                              100 * MS_PER_SEC);
+
+  animation.currentTime = 50 * MS_PER_SEC;
+  assert_equals(getComputedStyle(div).marginLeft, '150px',
+                'The margin-left value at 50% should be the base value');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1');
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px' });
+  var lowerAnimation = div.animate([{ offset: 0, marginLeft: '200px' },
+                                    { offset: 1, marginLeft: '300px' }],
+                                   100 * MS_PER_SEC);
+  var higherAnimation = div.animate([{ offset: 0, marginLeft: '400px' }],
+                                    100 * MS_PER_SEC);
+
+  lowerAnimation.currentTime = 50 * MS_PER_SEC;
+  higherAnimation.currentTime = 50 * MS_PER_SEC;
+                                                 // (250px + 400px) * 0.5
+  assert_equals(getComputedStyle(div).marginLeft, '325px',
+                'The margin-left value at 50% should be additive value of ' +
+                'lower-priority animation and higher-priority animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+   'is that of lower-priority animations');
+
+test(t => {
+  var div = addDiv(t, { style: 'margin-left: 100px;' +
+                               'transition: margin-left 100s linear' });
+  flushComputedStyle(div);
+
+  div.style.marginLeft = '300px';
+  flushComputedStyle(div);
+
+  div.animate([{ offset: 0, marginLeft: '200px' }], 100 * MS_PER_SEC);
+
+  div.getAnimations().forEach(animation => {
+    animation.currentTime = 50 * MS_PER_SEC;
+  });
+                                                 // (200px + 200px) * 0.5
+  assert_equals(getComputedStyle(div).marginLeft, '200px',
+                'The margin-left value at 50% should be additive value of ' +
+                'of the transition and animation');
+}, 'margin-left value at 50% for an animation with no keyframe at offset 1 ' +
+   'is that of transition');
+
+done();
+</script>
+</body>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/test_composite.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.core.enabled', true]] },
+  function() {
+    window.open('file_composite.html');
+  });
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe-on-compositor.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.core.enabled', true]] },
+  function() {
+    window.open('file_missing-keyframe-on-compositor.html');
+  });
+</script>
+</html>
new file mode 100644
--- /dev/null
+++ b/dom/animation/test/style/test_missing-keyframe.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.core.enabled', true]] },
+  function() {
+    window.open('file_missing-keyframe.html');
+  });
+</script>
+</html>
--- a/dom/animation/test/testcommon.js
+++ b/dom/animation/test/testcommon.js
@@ -24,16 +24,41 @@ var TIME_PRECISION = 0.0005; // ms
 /*
  * Allow implementations to substitute an alternative method for comparing
  * times based on their precision requirements.
  */
 function assert_times_equal(actual, expected, description) {
   assert_approx_equals(actual, expected, TIME_PRECISION, description);
 }
 
+/*
+ * Compare matrix string like 'matrix(1, 0, 0, 1, 100, 0)'.
+ * This function allows error, 0.01, because on Android when we are scaling down
+ * the document, it results in some errors.
+ */
+function assert_matrix_equals(actual, expected, description) {
+  var matrixRegExp = /^matrix\((.+),(.+),(.+),(.+),(.+),(.+)\)/;
+  assert_regexp_match(actual, matrixRegExp,
+    'Actual value should be a matrix')
+  assert_regexp_match(expected, matrixRegExp,
+    'Expected value should be a matrix');
+
+  var actualMatrixArray = actual.match(matrixRegExp).slice(1).map(Number);
+  var expectedMatrixArray = expected.match(matrixRegExp).slice(1).map(Number);
+
+  assert_equals(actualMatrixArray.length, expectedMatrixArray.length,
+    'Array lengths should be equal (got \'' + expected + '\' and \'' + actual +
+    '\'): ' + description);
+  for (var i = 0; i < actualMatrixArray.length; i++) {
+    assert_approx_equals(actualMatrixArray[i], expectedMatrixArray[i], 0.01,
+      'Matrix array should be equal (got \'' + expected + '\' and \'' + actual +
+      '\'): ' + description);
+  }
+}
+
 /**
  * Appends a div to the document body and creates an animation on the div.
  * NOTE: This function asserts when trying to create animations with durations
  * shorter than 100s because the shorter duration may cause intermittent
  * failures.  If you are not sure how long it is suitable, use 100s; it's
  * long enough but shorter than our test framework timeout (330s).
  * If you really need to use shorter durations, use animate() function directly.
  *
@@ -168,17 +193,18 @@ function flushComputedStyle(elem) {
 
 if (opener) {
   for (var funcName of ["async_test", "assert_not_equals", "assert_equals",
                         "assert_approx_equals", "assert_less_than",
                         "assert_less_than_equal", "assert_greater_than",
                         "assert_between_inclusive",
                         "assert_true", "assert_false",
                         "assert_class_string", "assert_throws",
-                        "assert_unreached", "promise_test", "test"]) {
+                        "assert_unreached", "assert_regexp_match",
+                        "promise_test", "test"]) {
     window[funcName] = opener[funcName].bind(opener);
   }
 
   window.EventWatcher = opener.EventWatcher;
 
   function done() {
     opener.add_completion_callback(function() {
       self.close();
@@ -201,16 +227,39 @@ function setupSynchronousObserver(t, tar
    });
   t.add_cleanup(() => {
     observer.disconnect();
   });
   observer.observe(target, { animations: true, subtree: subtree });
   return observer;
 }
 
+/*
+ * Returns a promise that is resolved when the document has finished loading.
+ */
+function waitForDocumentLoad() {
+  return new Promise(function(resolve, reject) {
+    if (document.readyState === "complete") {
+      resolve();
+    } else {
+      window.addEventListener("load", resolve);
+    }
+  });
+}
+
+/*
+ * Enters test refresh mode, and restores the mode when |t| finishes.
+ */
+function useTestRefreshMode(t) {
+  SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0);
+  t.add_cleanup(() => {
+    SpecialPowers.DOMWindowUtils.restoreNormalRefresh();
+  });
+}
+
 /**
  * Returns true if off-main-thread animations.
  */
 function isOMTAEnabled() {
   const OMTAPrefKey = 'layers.offmainthreadcomposition.async-animations';
   return SpecialPowers.DOMWindowUtils.layerManagerRemote &&
          SpecialPowers.getBoolPref(OMTAPrefKey);
 }
--- a/dom/html/HTMLMediaElement.cpp
+++ b/dom/html/HTMLMediaElement.cpp
@@ -541,40 +541,43 @@ public:
                             AudioChannel aChannel)
     : mOwner(aOwner)
     , mAudioChannel(aChannel)
     , mAudioChannelVolume(1.0)
     , mPlayingThroughTheAudioChannel(false)
     , mAudioCapturedByWindow(false)
     , mSuspended(nsISuspendedTypes::NONE_SUSPENDED)
     , mIsOwnerAudible(IsOwnerAudible())
+    , mIsShutDown(false)
   {
     MOZ_ASSERT(mOwner);
     MaybeCreateAudioChannelAgent();
   }
 
   void
   UpdateAudioChannelPlayingState(bool aForcePlaying = false)
   {
+    MOZ_ASSERT(!mIsShutDown);
     bool playingThroughTheAudioChannel =
       aForcePlaying || IsPlayingThroughTheAudioChannel();
 
     if (playingThroughTheAudioChannel != mPlayingThroughTheAudioChannel) {
       if (!MaybeCreateAudioChannelAgent()) {
         return;
       }
 
       mPlayingThroughTheAudioChannel = playingThroughTheAudioChannel;
       NotifyAudioChannelAgent(mPlayingThroughTheAudioChannel);
     }
   }
 
   void
   NotifyPlayStarted()
   {
+    MOZ_ASSERT(!mIsShutDown);
     // Reset the suspend type because the media element might be paused by
     // audio channel before calling play(). eg. paused by Fennec media control,
     // but resumed it from page.
     SetSuspended(nsISuspendedTypes::NONE_SUSPENDED);
     UpdateAudioChannelPlayingState();
   }
 
   NS_IMETHODIMP
@@ -640,73 +643,92 @@ public:
       AudioCaptureStreamChangeIfNeeded();
     }
     return NS_OK;
   }
 
   void
   AudioCaptureStreamChangeIfNeeded()
   {
+    MOZ_ASSERT(!mIsShutDown);
     if (!IsPlayingStarted()) {
       return;
     }
 
     if (!mOwner->HasAudio()) {
       return;
     }
 
     mOwner->AudioCaptureStreamChange(mAudioCapturedByWindow);
   }
 
   void
   NotifyAudioPlaybackChanged(AudibleChangedReasons aReason)
   {
+    MOZ_ASSERT(!mIsShutDown);
     if (!IsPlayingStarted()) {
       return;
     }
 
     bool newAudibleState = IsOwnerAudible();
     if (mIsOwnerAudible == newAudibleState) {
       return;
     }
 
     mIsOwnerAudible = newAudibleState;
     mAudioChannelAgent->NotifyStartedAudible(mIsOwnerAudible, aReason);
   }
 
   bool
   IsPlaybackBlocked()
   {
+    MOZ_ASSERT(!mIsShutDown);
     // If the tab hasn't been activated yet, the media element in that tab can't
     // be playback now until the tab goes to foreground first time or user clicks
     // the unblocking tab icon.
     if (!IsTabActivated()) {
       // Even we haven't start playing yet, we still need to notify the audio
       // channe system because we need to receive the resume notification later.
       UpdateAudioChannelPlayingState(true /* force to start */);
       return true;
     }
 
     return false;
   }
 
+  void
+  Shutdown()
+  {
+    MOZ_ASSERT(!mIsShutDown);
+    if (mAudioChannelAgent) {
+      mAudioChannelAgent->NotifyStoppedPlaying();
+      mAudioChannelAgent = nullptr;
+    }
+    mIsShutDown = true;
+  }
+
   float
   GetEffectiveVolume() const
   {
+    MOZ_ASSERT(!mIsShutDown);
     return mOwner->Volume() * mAudioChannelVolume;
   }
 
   SuspendTypes
   GetSuspendType() const
   {
+    MOZ_ASSERT(!mIsShutDown);
     return mSuspended;
   }
 
 private:
-  ~AudioChannelAgentCallback() {};
+  ~AudioChannelAgentCallback()
+  {
+    MOZ_ASSERT(mIsShutDown);
+  };
 
   bool
   MaybeCreateAudioChannelAgent()
   {
     if (mAudioChannelAgent) {
       return true;
     }
 
@@ -954,16 +976,17 @@ private:
   // be resumed when the page is active. See bug647429 for more details.
   // - SUSPENDED_STOP_DISPOSABLE
   // When we permanently lost platform audio focus, we should stop playing
   // and stop the audio channel agent. MediaElement can only be restarted by
   // play().
   SuspendTypes mSuspended;
   // True if media element is audible for users.
   bool mIsOwnerAudible;
+  bool mIsShutDown;
 };
 
 NS_IMPL_CYCLE_COLLECTION_CLASS(HTMLMediaElement::AudioChannelAgentCallback)
 
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(HTMLMediaElement::AudioChannelAgentCallback)
   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioChannelAgent)
 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
 
@@ -1267,16 +1290,19 @@ NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_IN
     tmp->EndSrcMediaStreamPlayback();
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcAttrStream)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSrcMediaSource)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourcePointer)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mLoadBlockedDoc)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mSourceLoadCandidate)
+  if (tmp->mAudioChannelWrapper) {
+    tmp->mAudioChannelWrapper->Shutdown();
+  }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioChannelWrapper)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mErrorSink->mError)
   for (uint32_t i = 0; i < tmp->mOutputStreams.Length(); ++i) {
     NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputStreams[i].mStream)
   }
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mPlayed)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mTextTrackManager)
   NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioTrackList)
@@ -3515,16 +3541,21 @@ HTMLMediaElement::~HTMLMediaElement()
 
   NS_ASSERTION(MediaElementTableCount(this, mLoadingSrc) == 0,
     "Destroyed media element should no longer be in element table");
 
   if (mChannelLoader) {
     mChannelLoader->Cancel();
   }
 
+  if (mAudioChannelWrapper) {
+    mAudioChannelWrapper->Shutdown();
+    mAudioChannelWrapper = nullptr;
+  }
+
   WakeLockRelease();
 }
 
 void HTMLMediaElement::StopSuspendingAfterFirstFrame()
 {
   mAllowSuspendAfterFirstFrame = false;
   if (!mSuspendedAfterFirstFrame)
     return;
--- a/dom/webidl/KeyframeEffect.webidl
+++ b/dom/webidl/KeyframeEffect.webidl
@@ -40,17 +40,17 @@ interface KeyframeEffectReadOnly : Anima
   // We use object instead of ComputedKeyframe so that we can put the
   // property-value pairs on the object.
   [Throws] sequence<object> getKeyframes();
 };
 
 // Non-standard extensions
 dictionary AnimationPropertyValueDetails {
   required double             offset;
-  required DOMString          value;
+           DOMString          value;
            DOMString          easing;
   required CompositeOperation composite;
 };
 
 dictionary AnimationPropertyDetails {
   required DOMString                               property;
   required boolean                                 runningOnCompositor;
            DOMString                               warning;
--- a/gfx/layers/Layers.cpp
+++ b/gfx/layers/Layers.cpp
@@ -418,16 +418,38 @@ CreateCSSValueList(const InfallibleTArra
   }
   if (aFunctions.Length() == 0) {
     result = new nsCSSValueList();
     result->mValue.SetNoneValue();
   }
   return new nsCSSValueSharedList(result.forget());
 }
 
+static StyleAnimationValue
+ToStyleAnimationValue(const Animatable& aAnimatable)
+{
+  StyleAnimationValue result;
+
+  switch (aAnimatable.type()) {
+    case Animatable::TArrayOfTransformFunction: {
+      const InfallibleTArray<TransformFunction>& transforms =
+        aAnimatable.get_ArrayOfTransformFunction();
+      result.SetTransformValue(CreateCSSValueList(transforms));
+      break;
+    }
+    case Animatable::Tfloat:
+      result.SetFloatValue(aAnimatable.get_float());
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported type");
+  }
+
+  return result;
+}
+
 void
 Layer::SetAnimations(const AnimationArray& aAnimations)
 {
   MOZ_LAYERS_LOG_IF_SHADOWABLE(this, ("Layer::Mutated(%p) SetAnimations", this));
 
   mAnimations = aAnimations;
   mAnimationData.Clear();
   for (uint32_t i = 0; i < mAnimations.Length(); i++) {
@@ -441,50 +463,40 @@ Layer::SetAnimations(const AnimationArra
         break;
       case dom::FillMode::Backwards:
         animation.fillMode() = static_cast<uint8_t>(dom::FillMode::Both);
         break;
       default:
         break;
     }
 
+    if (animation.baseStyle().type() != BaseAnimationStyle::Tnull_t) {
+      mBaseAnimationStyle =
+        ToStyleAnimationValue(animation.baseStyle().get_Animatable());
+    }
+
     AnimData* data = mAnimationData.AppendElement();
     InfallibleTArray<Maybe<ComputedTimingFunction>>& functions =
       data->mFunctions;
     const InfallibleTArray<AnimationSegment>& segments = animation.segments();
     for (uint32_t j = 0; j < segments.Length(); j++) {
       TimingFunction tf = segments.ElementAt(j).sampleFn();
 
       Maybe<ComputedTimingFunction> ctf =
         AnimationUtils::TimingFunctionToComputedTimingFunction(tf);
       functions.AppendElement(ctf);
     }
 
     // Precompute the StyleAnimationValues that we need if this is a transform
     // animation.
     InfallibleTArray<StyleAnimationValue>& startValues = data->mStartValues;
     InfallibleTArray<StyleAnimationValue>& endValues = data->mEndValues;
-    for (uint32_t j = 0; j < segments.Length(); j++) {
-      const AnimationSegment& segment = segments[j];
-      StyleAnimationValue* startValue = startValues.AppendElement();
-      StyleAnimationValue* endValue = endValues.AppendElement();
-      if (segment.endState().type() == Animatable::TArrayOfTransformFunction) {
-        const InfallibleTArray<TransformFunction>& startFunctions =
-          segment.startState().get_ArrayOfTransformFunction();
-        startValue->SetTransformValue(CreateCSSValueList(startFunctions));
-
-        const InfallibleTArray<TransformFunction>& endFunctions =
-          segment.endState().get_ArrayOfTransformFunction();
-        endValue->SetTransformValue(CreateCSSValueList(endFunctions));
-      } else {
-        NS_ASSERTION(segment.endState().type() == Animatable::Tfloat,
-                     "Unknown Animatable type");
-        startValue->SetFloatValue(segment.startState().get_float());
-        endValue->SetFloatValue(segment.endState().get_float());
-      }
+    for (const AnimationSegment& segment : segments) {
+      startValues.AppendElement(ToStyleAnimationValue(segment.startState()));
+      endValues.AppendElement(ToStyleAnimationValue(segment.endState()));
     }
   }
 
   Mutated();
 }
 
 void
 Layer::StartPendingAnimations(const TimeStamp& aReadyTime)
@@ -492,18 +504,20 @@ Layer::StartPendingAnimations(const Time
   ForEachNode<ForwardIterator>(
       this,
       [&aReadyTime](Layer *layer)
       {
         bool updated = false;
         for (size_t animIdx = 0, animEnd = layer->mAnimations.Length();
              animIdx < animEnd; animIdx++) {
           Animation& anim = layer->mAnimations[animIdx];
-          if (anim.startTime().IsNull()) {
-            anim.startTime() = aReadyTime - anim.initialCurrentTime();
+
+          // If the animation is play-pending, resolve the start time.
+          if (anim.startTime().IsNull() && !anim.isNotPlaying()) {
+            anim.startTime() = aReadyTime - anim.holdTime() + anim.delay();
             updated = true;
           }
         }
         if (updated) {
           layer->Mutated();
         }
       });
 }
--- a/gfx/layers/Layers.h
+++ b/gfx/layers/Layers.h
@@ -1413,16 +1413,21 @@ public:
   AnimationArray& GetAnimations() { return mAnimations; }
   InfallibleTArray<AnimData>& GetAnimationData() { return mAnimationData; }
 
   uint64_t GetAnimationGeneration() { return mAnimationGeneration; }
   void SetAnimationGeneration(uint64_t aCount) { mAnimationGeneration = aCount; }
 
   bool HasTransformAnimation() const;
 
+  StyleAnimationValue GetBaseAnimationStyle() const
+  {
+    return mBaseAnimationStyle;
+  }
+
   /**
    * Returns the local transform for this layer: either mTransform or,
    * for shadow layers, GetShadowBaseTransform(), in either case with the
    * pre- and post-scales applied.
    */
   gfx::Matrix4x4 GetLocalTransform();
 
   /**
@@ -1933,16 +1938,18 @@ protected:
   // If this layer is used for OMTA, then this counter is used to ensure we
   // stay in sync with the animation manager
   uint64_t mAnimationGeneration;
 #ifdef MOZ_DUMP_PAINTING
   nsTArray<nsCString> mExtraDumpInfo;
 #endif
   // Store display list log.
   nsCString mDisplayListLog;
+
+  StyleAnimationValue mBaseAnimationStyle;
 };
 
 /**
  * A Layer which we can paint into. It is a conceptually
  * infinite surface, but each PaintedLayer has an associated "valid region"
  * of contents that it is currently storing, which is finite. PaintedLayer
  * implementations can store content between paints.
  *
--- a/gfx/layers/composite/AsyncCompositionManager.cpp
+++ b/gfx/layers/composite/AsyncCompositionManager.cpp
@@ -571,29 +571,45 @@ AsyncCompositionManager::AlignFixedAndSt
       AlignFixedAndStickyLayers(aTransformedSubtreeRoot, child, aTransformScrollId,
           aPreviousTransformForRoot, newTransform, aFixedLayerMargins, aClipPartsCache);
     }
   }
 
   return;
 }
 
-static void
-SampleValue(float aPortion, Animation& aAnimation,
-            const StyleAnimationValue& aStart, const StyleAnimationValue& aEnd,
-            const StyleAnimationValue& aLastValue, uint64_t aCurrentIteration,
-            Animatable* aValue, Layer* aLayer)
+struct StyleAnimationValueCompositePair {
+  StyleAnimationValue mValue;
+  dom::CompositeOperation mComposite;
+};
+
+static StyleAnimationValue
+SampleValue(float aPortion, const Animation& aAnimation,
+            const StyleAnimationValueCompositePair& aStart,
+            const StyleAnimationValueCompositePair& aEnd,
+            const StyleAnimationValue& aLastValue,
+            uint64_t aCurrentIteration,
+            const StyleAnimationValue& aUnderlyingValue)
 {
-  NS_ASSERTION(aStart.GetUnit() == aEnd.GetUnit() ||
-               aStart.GetUnit() == StyleAnimationValue::eUnit_None ||
-               aEnd.GetUnit() == StyleAnimationValue::eUnit_None,
+  NS_ASSERTION(aStart.mValue.GetUnit() == aEnd.mValue.GetUnit() ||
+               aStart.mValue.GetUnit() == StyleAnimationValue::eUnit_None ||
+               aEnd.mValue.GetUnit() == StyleAnimationValue::eUnit_None,
                "Must have same unit");
 
-  StyleAnimationValue startValue = aStart;
-  StyleAnimationValue endValue = aEnd;
+  StyleAnimationValue startValue =
+    dom::KeyframeEffectReadOnly::CompositeValue(aAnimation.property(),
+                                                aStart.mValue,
+                                                aUnderlyingValue,
+                                                aStart.mComposite);
+  StyleAnimationValue endValue =
+    dom::KeyframeEffectReadOnly::CompositeValue(aAnimation.property(),
+                                                aEnd.mValue,
+                                                aUnderlyingValue,
+                                                aEnd.mComposite);
+
   // Iteration composition for accumulate
   if (static_cast<dom::IterationCompositeOperation>
         (aAnimation.iterationComposite()) ==
           dom::IterationCompositeOperation::Accumulate &&
       aCurrentIteration > 0) {
     // FIXME: Bug 1293492: Add a utility function to calculate both of
     // below StyleAnimationValues.
     startValue =
@@ -611,76 +627,112 @@ SampleValue(float aPortion, Animation& a
   StyleAnimationValue interpolatedValue;
   // This should never fail because we only pass transform and opacity values
   // to the compositor and they should never fail to interpolate.
   DebugOnly<bool> uncomputeResult =
     StyleAnimationValue::Interpolate(aAnimation.property(),
                                      startValue, endValue,
                                      aPortion, interpolatedValue);
   MOZ_ASSERT(uncomputeResult, "could not uncompute value");
-
-  if (aAnimation.property() == eCSSProperty_opacity) {
-    *aValue = interpolatedValue.GetFloatValue();
-    return;
-  }
+  return interpolatedValue;
+}
 
-  nsCSSValueSharedList* interpolatedList =
-    interpolatedValue.GetCSSValueSharedListValue();
-
-  TransformData& data = aAnimation.data().get_TransformData();
-  nsPoint origin = data.origin();
-  // we expect all our transform data to arrive in device pixels
-  Point3D transformOrigin = data.transformOrigin();
-  nsDisplayTransform::FrameTransformProperties props(interpolatedList,
-                                                     transformOrigin);
+static void
+ApplyAnimatedValue(Layer* aLayer,
+                   nsCSSPropertyID aProperty,
+                   const AnimationData& aAnimationData,
+                   const StyleAnimationValue& aValue)
+{
+  HostLayer* layerCompositor = aLayer->AsHostLayer();
+  switch (aProperty) {
+    case eCSSProperty_opacity: {
+      MOZ_ASSERT(aValue.GetUnit() == StyleAnimationValue::eUnit_Float,
+                 "Interpolated value for opacity should be float");
+      layerCompositor->SetShadowOpacity(aValue.GetFloatValue());
+      layerCompositor->SetShadowOpacitySetByAnimation(true);
+      break;
+    }
+    case eCSSProperty_transform: {
+      MOZ_ASSERT(aValue.GetUnit() == StyleAnimationValue::eUnit_Transform,
+                 "The unit of interpolated value for transform should be "
+                 "transform");
+      nsCSSValueSharedList* list = aValue.GetCSSValueSharedListValue();
 
-  // If our parent layer is a perspective layer, then the offset into reference
-  // frame coordinates is already on that layer. If not, then we need to ask
-  // for it to be added here.
-  uint32_t flags = 0;
-  if (!aLayer->GetParent() || !aLayer->GetParent()->GetTransformIsPerspective()) {
-    flags = nsDisplayTransform::OFFSET_BY_ORIGIN;
-  }
+      const TransformData& transformData = aAnimationData.get_TransformData();
+      nsPoint origin = transformData.origin();
+      // we expect all our transform data to arrive in device pixels
+      Point3D transformOrigin = transformData.transformOrigin();
+      nsDisplayTransform::FrameTransformProperties props(list,
+                                                         transformOrigin);
+
+      // If our parent layer is a perspective layer, then the offset into reference
+      // frame coordinates is already on that layer. If not, then we need to ask
+      // for it to be added here.
+      uint32_t flags = 0;
+      if (!aLayer->GetParent() ||
+          !aLayer->GetParent()->GetTransformIsPerspective()) {
+        flags = nsDisplayTransform::OFFSET_BY_ORIGIN;
+      }
 
-  Matrix4x4 transform =
-    nsDisplayTransform::GetResultingTransformMatrix(props, origin,
-                                                    data.appUnitsPerDevPixel(),
-                                                    flags, &data.bounds());
+      Matrix4x4 transform =
+        nsDisplayTransform::GetResultingTransformMatrix(props, origin,
+                                                        transformData.appUnitsPerDevPixel(),
+                                                        flags, &transformData.bounds());
 
-  InfallibleTArray<TransformFunction> functions;
-  functions.AppendElement(TransformMatrix(transform));
-  *aValue = functions;
+      if (ContainerLayer* c = aLayer->AsContainerLayer()) {
+        transform.PostScale(c->GetInheritedXScale(), c->GetInheritedYScale(), 1);
+      }
+      layerCompositor->SetShadowBaseTransform(transform);
+      layerCompositor->SetShadowTransformSetByAnimation(true);
+      break;
+    }
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unhandled animated property");
+  }
 }
 
 static bool
 SampleAnimations(Layer* aLayer, TimeStamp aPoint)
 {
   bool activeAnimations = false;
 
   ForEachNode<ForwardIterator>(
       aLayer,
       [&activeAnimations, &aPoint] (Layer* layer)
       {
         AnimationArray& animations = layer->GetAnimations();
+        if (animations.IsEmpty()) {
+          return;
+        }
+
         InfallibleTArray<AnimData>& animationData = layer->GetAnimationData();
 
+        StyleAnimationValue animationValue = layer->GetBaseAnimationStyle();
+        bool hasInEffectAnimations = false;
+
         // Process in order, since later animations override earlier ones.
         for (size_t i = 0, iEnd = animations.Length(); i < iEnd; ++i) {
           Animation& animation = animations[i];
           AnimData& animData = animationData[i];
 
           activeAnimations = true;
 
-          MOZ_ASSERT(!animation.startTime().IsNull(),
-                     "Failed to resolve start time of pending animations");
-          TimeDuration elapsedDuration =
-            (aPoint - animation.startTime()).MultDouble(animation.playbackRate());
+          MOZ_ASSERT(!animation.startTime().IsNull() ||
+                     animation.isNotPlaying(),
+                     "Failed to resolve start time of play-pending animations");
+          // If the animation is not currently playing , e.g. paused or
+          // finished, then use the hold time to stay at the same position.
+          TimeDuration elapsedDuration = animation.isNotPlaying()
+            ? animation.holdTime()
+            : (aPoint - animation.startTime())
+                .MultDouble(animation.playbackRate());
           TimingParams timing;
           timing.mDuration.emplace(animation.duration());
           timing.mDelay = animation.delay();
+          timing.mEndDelay = animation.endDelay();
           timing.mIterations = animation.iterations();
           timing.mIterationStart = animation.iterationStart();
           timing.mDirection =
             static_cast<dom::PlaybackDirection>(animation.direction());
           timing.mFill = static_cast<dom::FillMode>(animation.fillMode());
           timing.mFunction =
             AnimationUtils::TimingFunctionToComputedTimingFunction(
               animation.easingFunction());
@@ -707,45 +759,64 @@ SampleAnimations(Layer* aLayer, TimeStam
             (computedTiming.mProgress.Value() - segment->startPortion()) /
             (segment->endPortion() - segment->startPortion());
 
           double portion =
             ComputedTimingFunction::GetPortion(animData.mFunctions[segmentIndex],
                                                positionInSegment,
                                            computedTiming.mBeforeFlag);
 
+          StyleAnimationValueCompositePair from {
+            animData.mStartValues[segmentIndex],
+            static_cast<dom::CompositeOperation>(segment->startComposite())
+          };
+          StyleAnimationValueCompositePair to {
+            animData.mEndValues[segmentIndex],
+            static_cast<dom::CompositeOperation>(segment->endComposite())
+          };
           // interpolate the property
-          Animatable interpolatedValue;
-          SampleValue(portion, animation,
-                      animData.mStartValues[segmentIndex],
-                      animData.mEndValues[segmentIndex],
-                      animData.mEndValues.LastElement(),
-                      computedTiming.mCurrentIteration,
-                      &interpolatedValue, layer);
-          HostLayer* layerCompositor = layer->AsHostLayer();
-          switch (animation.property()) {
-          case eCSSProperty_opacity:
-          {
-            layerCompositor->SetShadowOpacity(interpolatedValue.get_float());
-            layerCompositor->SetShadowOpacitySetByAnimation(true);
-            break;
+          animationValue = SampleValue(portion,
+                                       animation,
+                                       from, to,
+                                       animData.mEndValues.LastElement(),
+                                       computedTiming.mCurrentIteration,
+                                       animationValue);
+          hasInEffectAnimations = true;
+        }
+
+#ifdef DEBUG
+        // Sanity check that all of animation data are the same.
+        const AnimationData& lastData = animations.LastElement().data();
+        for (const Animation& animation : animations) {
+          const AnimationData& data = animation.data();
+          MOZ_ASSERT(data.type() == lastData.type(),
+                     "The type of AnimationData should be the same");
+          if (data.type() == AnimationData::Tnull_t) {
+            continue;
           }
-          case eCSSProperty_transform:
-          {
-            Matrix4x4 matrix = interpolatedValue.get_ArrayOfTransformFunction()[0].get_TransformMatrix().value();
-            if (ContainerLayer* c = layer->AsContainerLayer()) {
-              matrix.PostScale(c->GetInheritedXScale(), c->GetInheritedYScale(), 1);
-            }
-            layerCompositor->SetShadowBaseTransform(matrix);
-            layerCompositor->SetShadowTransformSetByAnimation(true);
-            break;
-          }
-          default:
-            NS_WARNING("Unhandled animated property");
-          }
+
+          MOZ_ASSERT(data.type() == AnimationData::TTransformData);
+          const TransformData& transformData = data.get_TransformData();
+          const TransformData& lastTransformData = lastData.get_TransformData();
+          MOZ_ASSERT(transformData.origin() == lastTransformData.origin() &&
+                     transformData.transformOrigin() ==
+                       lastTransformData.transformOrigin() &&
+                     transformData.bounds() == lastTransformData.bounds() &&
+                     transformData.appUnitsPerDevPixel() ==
+                       lastTransformData.appUnitsPerDevPixel(),
+                     "All of members of TransformData should be the same");
+        }
+#endif
+        // If all of animations are 
+        if (hasInEffectAnimations) {
+          Animation& animation = animations.LastElement();
+          ApplyAnimatedValue(layer,
+                             animation.property(),
+                             animation.data(),
+                             animationValue);
         }
       });
   return activeAnimations;
 }
 
 static bool
 SampleAPZAnimations(const LayerMetricsWrapper& aLayer, TimeStamp aSampleTime)
 {
--- a/gfx/layers/ipc/LayersMessages.ipdlh
+++ b/gfx/layers/ipc/LayersMessages.ipdlh
@@ -155,21 +155,28 @@ union TransformFunction {
   TransformMatrix;
 };
 
 union Animatable {
   float;
   TransformFunction[];
 };
 
+union BaseAnimationStyle {
+  null_t;
+  Animatable;
+};
+
 struct AnimationSegment {
   Animatable startState;
   Animatable endState;
   float startPortion;
   float endPortion;
+  uint8_t startComposite;
+  uint8_t endComposite;
   TimingFunction sampleFn;
 };
 
 // Transforms need extra information to correctly convert the list of transform
 // functions to a Matrix4x4 that can be applied directly to the layer.
 struct TransformData {
   // the origin of the frame being transformed in app units
   nsPoint origin;
@@ -182,21 +189,27 @@ struct TransformData {
 union AnimationData {
   null_t;
   TransformData;
 };
 
 struct Animation {
   TimeStamp startTime;
   TimeDuration delay;
-  // The value of the animation's current time at the moment it was created.
-  // For animations that are waiting to start, their startTime will be null.
-  // Once the animation is ready to start, we calculate an appropriate value
-  // of startTime such that we begin playback from initialCurrentTime.
-  TimeDuration initialCurrentTime;
+  TimeDuration endDelay;
+  // The value of the animation's current time at the moment it was sent to the
+  // compositor.  This value will be used for below cases:
+  // 1) Animations that are play-pending. Initially these animations will have a
+  //    null |startTime|. Once the animation is read to start (i.e. painting has
+  //    finished), we calculate an appropriate value of |startTime| such that
+  //    playback begins from |holdTime|.
+  // 2) Not playing animations (e.g. paused and finished animations). In this
+  //   case the |holdTime| represents the current time the animation will
+  //   maintain.
+  TimeDuration holdTime;
   TimeDuration duration;
   // For each frame, the interpolation point is computed based on the
   // startTime, the direction, the duration, and the current time.
   // The segments must uniquely cover the portion from 0.0 to 1.0
   AnimationSegment[] segments;
   // Number of times to repeat the animation, including positive infinity.
   // Values <= 0 mean the animation will not play (although events are still
   // dispatched on the main thread).
@@ -207,16 +220,24 @@ struct Animation {
   // This uses dom::FillMode.
   uint8_t fillMode;
   nsCSSPropertyID property;
   AnimationData data;
   float playbackRate;
   // This is used in the transformed progress calculation.
   TimingFunction easingFunction;
   uint8_t iterationComposite;
+  // True if the animation has a fixed current time (e.g. paused and
+  // forward-filling animations).
+  bool isNotPlaying;
+  // The base style that animations should composite with. This is only set for
+  // animations with a composite mode of additive or accumulate, and only for
+  // the first animation in the set (i.e. the animation that is lowest in the
+  // stack). In all other cases the value is null_t.
+  BaseAnimationStyle baseStyle;
 };
 
 // Change a layer's attributes
 struct CommonLayerAttributes {
   IntRect layerBounds;
   LayerIntRegion visibleRegion;
   EventRegions eventRegions;
   TransformMatrix transform;
--- a/layout/base/nsLayoutUtils.cpp
+++ b/layout/base/nsLayoutUtils.cpp
@@ -510,16 +510,29 @@ GetSuitableScale(float aMaxScale, float 
     // fraction of a second before we delayerize the composited texture it has
     // a better chance of being pixel aligned and composited without resampling
     // (avoiding visually clunky delayerization).
     return aMaxScale;
   }
   return std::max(std::min(aMaxScale, displayVisibleRatio), aMinScale);
 }
 
+static inline void
+UpdateMinMaxScale(const nsIFrame* aFrame,
+                  const StyleAnimationValue& aValue,
+                  gfxSize& aMinScale,
+                  gfxSize& aMaxScale)
+{
+  gfxSize size = aValue.GetScaleValue(aFrame);
+  aMaxScale.width = std::max<float>(aMaxScale.width, size.width);
+  aMaxScale.height = std::max<float>(aMaxScale.height, size.height);
+  aMinScale.width = std::min<float>(aMinScale.width, size.width);
+  aMinScale.height = std::min<float>(aMinScale.height, size.height);
+}
+
 static void
 GetMinAndMaxScaleForAnimationProperty(const nsIFrame* aFrame,
                                       nsTArray<RefPtr<dom::Animation>>&
                                         aAnimations,
                                       gfxSize& aMaxScale,
                                       gfxSize& aMinScale)
 {
   for (dom::Animation* anim : aAnimations) {
@@ -529,29 +542,38 @@ GetMinAndMaxScaleForAnimationProperty(co
     // not yet finished or which are filling forwards).
     MOZ_ASSERT(anim->IsRelevant());
 
     dom::KeyframeEffectReadOnly* effect =
       anim->GetEffect() ? anim->GetEffect()->AsKeyframeEffect() : nullptr;
     MOZ_ASSERT(effect, "A playing animation should have a keyframe effect");
     for (size_t propIdx = effect->Properties().Length(); propIdx-- != 0; ) {
       const AnimationProperty& prop = effect->Properties()[propIdx];
-      if (prop.mProperty == eCSSProperty_transform) {
-        for (uint32_t segIdx = prop.mSegments.Length(); segIdx-- != 0; ) {
-          const AnimationPropertySegment& segment = prop.mSegments[segIdx];
-          gfxSize from = segment.mFromValue.GetScaleValue(aFrame);
-          aMaxScale.width = std::max<float>(aMaxScale.width, from.width);
-          aMaxScale.height = std::max<float>(aMaxScale.height, from.height);
-          aMinScale.width = std::min<float>(aMinScale.width, from.width);
-          aMinScale.height = std::min<float>(aMinScale.height, from.height);
-          gfxSize to = segment.mToValue.GetScaleValue(aFrame);
-          aMaxScale.width = std::max<float>(aMaxScale.width, to.width);
-          aMaxScale.height = std::max<float>(aMaxScale.height, to.height);
-          aMinScale.width = std::min<float>(aMinScale.width, to.width);
-          aMinScale.height = std::min<float>(aMinScale.height, to.height);
+      if (prop.mProperty != eCSSProperty_transform) {
+        continue;
+      }
+
+      // We need to factor in the scale of the base style if the base style
+      // will be used on the compositor.
+      if (effect->NeedsBaseStyle(prop.mProperty)) {
+        EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+        StyleAnimationValue baseStyle =
+          effects->GetBaseStyle(prop.mProperty);
+        MOZ_ASSERT(!baseStyle.IsNull(), "The base value should be set");
+        UpdateMinMaxScale(aFrame, baseStyle, aMinScale, aMaxScale);
+      }
+
+      for (const AnimationPropertySegment& segment : prop.mSegments) {
+        // In case of add or accumulate composite, StyleAnimationValue does
+        // not have a valid value.
+        if (segment.mFromComposite == dom::CompositeOperation::Replace) {
+          UpdateMinMaxScale(aFrame, segment.mFromValue, aMinScale, aMaxScale);
+        }
+        if (segment.mToComposite == dom::CompositeOperation::Replace) {
+          UpdateMinMaxScale(aFrame, segment.mToValue, aMinScale, aMaxScale);
         }
       }
     }
   }
 }
 
 gfxSize
 nsLayoutUtils::ComputeSuitableScaleForAnimation(const nsIFrame* aFrame,
--- a/layout/painting/nsDisplayList.cpp
+++ b/layout/painting/nsDisplayList.cpp
@@ -381,31 +381,84 @@ ToTimingFunction(const Maybe<ComputedTim
                                               spline->X2(), spline->Y2()));
   }
 
   uint32_t type = aCTF->GetType() == nsTimingFunction::Type::StepStart ? 1 : 2;
   return TimingFunction(StepFunction(aCTF->GetSteps(), type));
 }
 
 static void
+SetAnimatable(nsCSSPropertyID aProperty,
+              const StyleAnimationValue& aAnimationValue,
+              nsIFrame* aFrame,
+              const TransformReferenceBox& aRefBox,
+              layers::Animatable& aAnimatable)
+{
+  MOZ_ASSERT(aFrame);
+
+  switch (aProperty) {
+    case eCSSProperty_opacity:
+      if (!aAnimationValue.IsNull()) {
+        aAnimatable = aAnimationValue.GetFloatValue();
+      } else {
+        aAnimatable = 0.0;
+      }
+      break;
+    case eCSSProperty_transform:
+      aAnimatable = InfallibleTArray<TransformFunction>();
+      if (!aAnimationValue.IsNull()) {
+        nsCSSValueSharedList* list =
+          aAnimationValue.GetCSSValueSharedListValue();
+        TransformReferenceBox refBox(aFrame);
+        AddTransformFunctions(list->mHead,
+                              aFrame->StyleContext(),
+                              aFrame->PresContext(),
+                              refBox,
+                              aAnimatable.get_ArrayOfTransformFunction());
+      }
+      break;
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported property");
+  }
+}
+
+static void
+SetBaseAnimationStyle(nsCSSPropertyID aProperty,
+                      nsIFrame* aFrame,
+                      const TransformReferenceBox& aRefBox,
+                      layers::BaseAnimationStyle& aBaseStyle)
+{
+  MOZ_ASSERT(aFrame);
+
+  EffectSet* effects = EffectSet::GetEffectSet(aFrame);
+  StyleAnimationValue baseValue = effects->GetBaseStyle(aProperty);
+  MOZ_ASSERT(!baseValue.IsNull(),
+             "The base value should be already there");
+
+  layers::Animatable animatable;
+  SetAnimatable(aProperty, baseValue, aFrame, aRefBox, animatable);
+  aBaseStyle = animatable;
+}
+
+static void
 AddAnimationForProperty(nsIFrame* aFrame, const AnimationProperty& aProperty,
                         dom::Animation* aAnimation, Layer* aLayer,
                         AnimationData& aData, bool aPending)
 {
   MOZ_ASSERT(aLayer->AsContainerLayer(), "Should only animate ContainerLayer");
   MOZ_ASSERT(aAnimation->GetEffect(),
              "Should not be adding an animation without an effect");
   MOZ_ASSERT(!aAnimation->GetCurrentOrPendingStartTime().IsNull() ||
+             !aAnimation->IsPlaying() ||
              (aAnimation->GetTimeline() &&
               aAnimation->GetTimeline()->TracksWallclockTime()),
-             "Animation should either have a resolved start time or "
-             "a timeline that tracks wallclock time");
-  nsStyleContext* styleContext = aFrame->StyleContext();
-  nsPresContext* presContext = aFrame->PresContext();
-  TransformReferenceBox refBox(aFrame);
+             "If the animation has an unresolved start time it should either"
+             " be static (so we don't need a start time) or else have a"
+             " timeline capable of converting TimeStamps (so we can calculate"
+             " one later");
 
   layers::Animation* animation =
     aPending ?
     aLayer->AddAnimationForNextTransaction() :
     aLayer->AddAnimation();
 
   const TimingParams& timing = aAnimation->GetEffect()->SpecifiedTiming();
 
@@ -433,55 +486,66 @@ AddAnimationForProperty(nsIFrame* aFrame
 
   const ComputedTiming computedTiming =
     aAnimation->GetEffect()->GetComputedTiming();
   Nullable<TimeDuration> startTime = aAnimation->GetCurrentOrPendingStartTime();
   animation->startTime() = startTime.IsNull()
                            ? TimeStamp()
                            : aAnimation->GetTimeline()->
                               ToTimeStamp(startTime.Value());
-  animation->initialCurrentTime() = aAnimation->GetCurrentTime().Value()
-                                    - timing.mDelay;
+  animation->holdTime() = aAnimation->GetCurrentTime().Value();
+
   animation->delay() = timing.mDelay;
+  animation->endDelay() = timing.mEndDelay;
   animation->duration() = computedTiming.mDuration;
   animation->iterations() = computedTiming.mIterations;
   animation->iterationStart() = computedTiming.mIterationStart;
   animation->direction() = static_cast<uint8_t>(timing.mDirection);
   animation->fillMode() = static_cast<uint8_t>(computedTiming.mFill);
   animation->property() = aProperty.mProperty;
   animation->playbackRate() = aAnimation->PlaybackRate();
   animation->data() = aData;
   animation->easingFunction() = ToTimingFunction(timing.mFunction);
   animation->iterationComposite() =
     static_cast<uint8_t>(aAnimation->GetEffect()->
                          AsKeyframeEffect()->IterationComposite());
+  animation->isNotPlaying() = !aAnimation->IsPlaying();
+
+  TransformReferenceBox refBox(aFrame);
+
+  // If the animation is additive or accumulates, we need to pass its base value
+  // to the compositor.
+  if (aAnimation->GetEffect()->AsKeyframeEffect()->
+        NeedsBaseStyle(aProperty.mProperty)) {
+    SetBaseAnimationStyle(aProperty.mProperty,
+                          aFrame, refBox,
+                          animation->baseStyle());
+  } else {
+    animation->baseStyle() = null_t();
+  }
 
   for (uint32_t segIdx = 0; segIdx < aProperty.mSegments.Length(); segIdx++) {
     const AnimationPropertySegment& segment = aProperty.mSegments[segIdx];
 
     AnimationSegment* animSegment = animation->segments().AppendElement();
-    if (aProperty.mProperty == eCSSProperty_transform) {
-      animSegment->startState() = InfallibleTArray<TransformFunction>();
-      animSegment->endState() = InfallibleTArray<TransformFunction>();
-
-      nsCSSValueSharedList* list =
-        segment.mFromValue.GetCSSValueSharedListValue();
-      AddTransformFunctions(list->mHead, styleContext, presContext, refBox,
-                            animSegment->startState().get_ArrayOfTransformFunction());
-
-      list = segment.mToValue.GetCSSValueSharedListValue();
-      AddTransformFunctions(list->mHead, styleContext, presContext, refBox,
-                            animSegment->endState().get_ArrayOfTransformFunction());
-    } else if (aProperty.mProperty == eCSSProperty_opacity) {
-      animSegment->startState() = segment.mFromValue.GetFloatValue();
-      animSegment->endState() = segment.mToValue.GetFloatValue();
-    }
+    SetAnimatable(aProperty.mProperty,
+                  segment.mFromValue,
+                  aFrame, refBox,
+                  animSegment->startState());
+    SetAnimatable(aProperty.mProperty,
+                  segment.mToValue,
+                  aFrame, refBox,
+                  animSegment->endState());
 
     animSegment->startPortion() = segment.mFromKey;
     animSegment->endPortion() = segment.mToKey;
+    animSegment->startComposite() =
+      static_cast<uint8_t>(segment.mFromComposite);
+    animSegment->endComposite() =
+      static_cast<uint8_t>(segment.mToComposite);
     animSegment->sampleFn() = ToTimingFunction(segment.mTimingFunction);
   }
 }
 
 static void
 AddAnimationsForProperty(nsIFrame* aFrame, nsCSSPropertyID aProperty,
                          nsTArray<RefPtr<dom::Animation>>& aAnimations,
                          Layer* aLayer, AnimationData& aData,
@@ -492,17 +556,17 @@ AddAnimationsForProperty(nsIFrame* aFram
              "inconsistent property flags");
 
   DebugOnly<EffectSet*> effects = EffectSet::GetEffectSet(aFrame);
   MOZ_ASSERT(effects);
 
   // Add from first to last (since last overrides)
   for (size_t animIdx = 0; animIdx < aAnimations.Length(); animIdx++) {
     dom::Animation* anim = aAnimations[animIdx];
-    if (!anim->IsPlayableOnCompositor()) {
+    if (!anim->IsRelevant()) {
       continue;
     }
 
     dom::KeyframeEffectReadOnly* keyframeEffect =
       anim->GetEffect() ? anim->GetEffect()->AsKeyframeEffect() : nullptr;
     MOZ_ASSERT(keyframeEffect,
                "A playing animation should have a keyframe effect");
     const AnimationProperty* property =
--- a/layout/style/test/file_animations_effect_timing_enddelay.html
+++ b/layout/style/test/file_animations_effect_timing_enddelay.html
@@ -96,18 +96,18 @@ addAsyncAnimTest(function *() {
   var [ div ] = new_div("");
   var animation = div.animate(
     [ { transform: 'translate(0px)' }, { transform: 'translate(100px)' } ],
     { duration: 1000, endDelay: 1000, fill: 'forwards' });
   yield waitForPaints();
 
   advance_clock(1500);
   yield waitForPaints();
-  omta_is(div, "transform", { tx: 100 }, RunningOn.MainThread,
-          "The end delay is performed on the main thread");
+  omta_is(div, "transform", { tx: 100 }, RunningOn.Compositor,
+          "The end delay is performed on the compositor thread");
 
   done_div();
 });
 
 addAsyncAnimTest(function *() {
   var [ div ] = new_div("");
   var animation = div.animate(
     [ { transform: 'translate(0px)' }, { transform: 'translate(100px)' } ],
--- a/layout/style/test/test_animations_omta.html
+++ b/layout/style/test/test_animations_omta.html
@@ -1894,17 +1894,17 @@ addAsyncAnimTest(function *() {
  * Bug 1004365 - zero-duration animations
  */
 
 addAsyncAnimTest(function *() {
   new_div("transform: translate(0, 200px); animation: anim4 0s 1s both");
   listen();
   yield waitForPaintsFlushed();
   advance_clock(0);
-  omta_is("transform", { ty: 0 }, RunningOn.MainThread,
+  omta_is("transform", { ty: 0 }, RunningOn.Compositor,
           "transform during backwards fill of zero-duration animation");
   advance_clock(2000); // Skip over animation
   yield waitForPaints();
   omta_is("transform", { ty: 100 }, RunningOn.MainThread,
           "transform during backwards fill of zero-duration animation");
   check_events([{ type: 'animationstart', target: gDiv,
                   animationName: 'anim4', elapsedTime: 0,
                   pseudoElement: "" },
@@ -1949,17 +1949,17 @@ addAsyncAnimTest(function *() {
 // We do however still want to test with an infinite repeat count and zero
 // duration to ensure this does not confuse the screening of OMTA animations.
 addAsyncAnimTest(function *() {
   new_div("transform: translate(0, 200px); " +
           "animation: anim4 0s 1s both infinite");
   listen();
   yield waitForPaintsFlushed();
   advance_clock(0);
-  omta_is("transform", { ty: 0 }, RunningOn.MainThread,
+  omta_is("transform", { ty: 0 }, RunningOn.Compositor,
           "transform during backwards fill of infinitely repeating " +
           "zero-duration animation");
   advance_clock(2000);
   yield waitForPaints();
   omta_is("transform", { ty: 100 }, RunningOn.MainThread,
           "transform during forwards fill of infinitely repeating " +
           "zero-duration animation");
   check_events([{ type: 'animationstart', target: gDiv,
--- a/testing/web-platform/meta/MANIFEST.json
+++ b/testing/web-platform/meta/MANIFEST.json
@@ -39572,16 +39572,22 @@
           }
         ],
         "web-animations/animation-model/animation-types/spacing-keyframes-transform.html": [
           {
             "path": "web-animations/animation-model/animation-types/spacing-keyframes-transform.html",
             "url": "/web-animations/animation-model/animation-types/spacing-keyframes-transform.html"
           }
         ],
+        "web-animations/animation-model/combining-effects/effect-composition.html": [
+          {
+            "path": "web-animations/animation-model/combining-effects/effect-composition.html",
+            "url": "/web-animations/animation-model/combining-effects/effect-composition.html"
+          }
+        ],
         "web-animations/interfaces/KeyframeEffect/copy-contructor.html": [
           {
             "path": "web-animations/interfaces/KeyframeEffect/copy-contructor.html",
             "url": "/web-animations/interfaces/KeyframeEffect/copy-contructor.html"
           }
         ],
         "web-animations/interfaces/KeyframeEffectReadOnly/copy-contructor.html": [
           {
--- a/testing/web-platform/meta/web-animations/interfaces/Animatable/animate.html.ini
+++ b/testing/web-platform/meta/web-animations/interfaces/Animatable/animate.html.ini
@@ -1,41 +1,9 @@
 [animate.html]
   type: testharness
-  [Element.animate() accepts a one property one value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a one property one non-array value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a one property two value property-indexed keyframes specification where the first value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a one property two value property-indexed keyframes specification where the second value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a one property one keyframe sequence]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a single keyframe sequence with omitted offsets]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
   [Element.animate() accepts a keyframe sequence with different composite values, but the same composite value for a given offset]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291468
 
-  [Element.animate() accepts a two property keyframe sequence where one property is missing from the first keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Element.animate() accepts a two property keyframe sequence where one property is missing from the last keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
   [Element.animate() accepts a single keyframe sequence with string offset]
     expected: FAIL
 
--- a/testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/constructor.html.ini
+++ b/testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/constructor.html.ini
@@ -5,84 +5,17 @@
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291468
 
   [composite values are parsed correctly when passed to the KeyframeEffectReadOnly constructor in regular keyframes]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291468
 
   [composite values are parsed correctly when passed to the KeyframeEffectReadOnly constructor in KeyframeTimingOptions]
     expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a one property one value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a one property one value property-indexed keyframes specification roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a one property one non-array value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a one property one non-array value property-indexed keyframes specification roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a one property two value property-indexed keyframes specification where the first value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a one property two value property-indexed keyframes specification where the first value is invalid roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a one property two value property-indexed keyframes specification where the second value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a one property two value property-indexed keyframes specification where the second value is invalid roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a one property one keyframe sequence]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a one property one keyframe sequence roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a single keyframe sequence with omitted offsets]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a single keyframe sequence with omitted offsets roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
+    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1311620 # This needs additive animation
 
   [a KeyframeEffectReadOnly can be constructed with a keyframe sequence with different composite values, but the same composite value for a given offset]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291468
 
-  [a KeyframeEffectReadOnly can be constructed with a two property keyframe sequence where one property is missing from the first keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a two property keyframe sequence where one property is missing from the first keyframe roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly can be constructed with a two property keyframe sequence where one property is missing from the last keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [a KeyframeEffectReadOnly constructed with a two property keyframe sequence where one property is missing from the last keyframe roundtrips]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
   [a KeyframeEffectReadOnly can be constructed with a single keyframe sequence with string offset]
     expected: FAIL
 
-  [a KeyframeEffectReadOnly constructed with a single keyframe sequence with string offset roundtrips]
-    expected: FAIL
-
--- a/testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/setKeyframes.html.ini
+++ b/testing/web-platform/meta/web-animations/interfaces/KeyframeEffect/setKeyframes.html.ini
@@ -1,39 +1,9 @@
 [setKeyframes.html]
   type: testharness
-  [Keyframes can be replaced with a one property one value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Keyframes can be replaced with a one property one non-array value property-indexed keyframes specification]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Keyframes can be replaced with a one property two value property-indexed keyframes specification where the first value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Keyframes can be replaced with a one property two value property-indexed keyframes specification where the second value is invalid]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Keyframes can be replaced with a one property one keyframe sequence]
-    expected: FAIL
-
-  [Keyframes can be replaced with a single keyframe sequence with omitted offsets]
-    expected: FAIL
-
   [Keyframes can be replaced with a keyframe sequence with different composite values, but the same composite value for a given offset]
     expected: FAIL
     bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1291468
 
-  [Keyframes can be replaced with a two property keyframe sequence where one property is missing from the first keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
-  [Keyframes can be replaced with a two property keyframe sequence where one property is missing from the last keyframe]
-    expected: FAIL
-    bug: https://bugzilla.mozilla.org/show_bug.cgi?id=1216844
-
   [Keyframes can be replaced with a single keyframe sequence with string offset]
     expected: FAIL
 
new file mode 100644
--- /dev/null
+++ b/testing/web-platform/tests/web-animations/animation-model/combining-effects/effect-composition.html
@@ -0,0 +1,68 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test for effect composition</title>
+<link rel="help" href="https://w3c.github.io/web-animations/#effect-composition">
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script src="../../testcommon.js"></script>
+<div id="log"></div>
+<script>
+'use strict';
+
+test(function(t) {
+  var div = createDiv(t);
+  div.style.marginLeft = '10px';
+  var anim =
+    div.animate({ marginLeft: ['0px', '10px'], composite: 'accumulate' }, 100);
+
+  anim.currentTime = 50;
+  assert_equals(getComputedStyle(div).marginLeft, '15px',
+    'Animated margin-left style at 50%');
+}, 'Accumulate onto the base value');
+
+test(function(t) {
+  var div = createDiv(t);
+  var anims = [];
+  anims.push(div.animate({ marginLeft: ['10px', '20px'],
+                           composite: 'replace' },
+                         100));
+  anims.push(div.animate({ marginLeft: ['0px', '10px'],
+                           composite: 'accumulate' },
+                         100));
+
+  anims.forEach(function(anim) {
+    anim.currentTime = 50;
+  });
+
+  assert_equals(getComputedStyle(div).marginLeft, '20px',
+    'Animated style at 50%');
+}, 'Accumulate onto an underlying animation value');
+
+test(function(t) {
+  var div = createDiv(t);
+  div.style.marginLeft = '10px';
+  var anim =
+    div.animate([{ marginLeft: '10px', composite: 'accumulate' },
+                 { marginLeft: '30px', composite: 'replace' }],
+                100);
+
+  anim.currentTime = 50;
+  assert_equals(getComputedStyle(div).marginLeft, '25px',
+    'Animated style at 50%');
+}, 'Composite when mixing accumulate and replace');
+
+test(function(t) {
+  var div = createDiv(t);
+  div.style.marginLeft = '10px';
+  var anim =
+    div.animate([{ marginLeft: '10px', composite: 'replace' },
+                 { marginLeft: '20px' }],
+                { duration: 100 , composite: 'accumulate' });
+
+  anim.currentTime = 50;
+  assert_equals(getComputedStyle(div).marginLeft, '20px',
+    'Animated style at 50%');
+}, 'Composite specified on a keyframe overrides the composite mode of the ' +
+   'effect');
+
+</script>
--- a/testing/web-platform/tests/web-animations/resources/keyframe-utils.js
+++ b/testing/web-platform/tests/web-animations/resources/keyframe-utils.js
@@ -177,17 +177,17 @@ var gPropertyIndexedKeyframesTests = [
                left: "10px" },
              { offset: null, computedOffset: 1, easing: "linear",
                left: "invalid" }] },
 ];
 
 var gKeyframeSequenceTests = [
   { desc:   "a one property one keyframe sequence",
     input:  [{ offset: 1, left: "10px" }],
-    output: [{ offset: null, computedOffset: 1, easing: "linear",
+    output: [{ offset: 1, computedOffset: 1, easing: "linear",
                left: "10px" }] },
   { desc:   "a one property two keyframe sequence",
     input:  [{ offset: 0, left: "10px" },
              { offset: 1, left: "20px" }],
     output: [{ offset: 0, computedOffset: 0, easing: "linear", left: "10px" },
              { offset: 1, computedOffset: 1, easing: "linear", left: "20px" }]
   },
   { desc:   "a two property two keyframe sequence",